webpack loader

99 阅读3分钟

loader原理

webpack本身只能识别js模块,loader能够帮助webpack将不同类型的文件转换成webpack能识别的模块。
官方文档: webpack.docschina.org/api/loaders

1、loader分类

. pre 前置loader
. normal 普通loader
. inline 内联loader
. post 后置loader

2、执行顺序

pre > normal > inline > post
相同优先级的执行顺序:从右到左、从下到上

enforce 可以指定loader类型

module: {
    rules: [
      // 执行顺序 loader1 > loader2 > loader3
      {
        enforce: 'pre', // 指定loader类型
        test: /\.js$/,
        use: 'loader1'
      },
      {
        // 没有enforce 是 normal loader
        test: /\.js$/,
        use: 'loader2'
      },
      {
        enforce: 'post',
        test: /\.js$/,
        use: 'loader3'
      },
    ]
  },
3、使用loader的方式

. 配置方式: 在webpack.config.js中指定loader(pre、normal、post loader)
. 内联方式: 在每个import语句中显示指定loader (inline loader)

4、inline loader

用法: 多个loader用 ! 连接, ? 后面是给css-loader传递的参数

用style-loader和css-loader去处理 ./style.css
import Styles from 'style-loader!css-loader?modules!./style.css

inline loader可以通过添加不同前缀,跳过其它类型的loader
1、 ! 跳过 normal loader

import Styles from '!style-loader!css-loader?modules!./style.css

2、 -! 跳过 pre和normal loader

import Styles from '-!style-loader!css-loader?modules!./style.css

3、 !! 跳过 pre、normal、post loader

import Styles from '!!style-loader!css-loader?modules!./style.css

开发一个loader

image.png

package.json
{
  "name": "loader",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^5.5.0",
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.0"
  }
}

入口文件 src/main.js
console.log('hello main')
模板文件 public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>loader</title>
</head>
<body>
  
</body>
</html>
第一个loader loaders/test-loader.js
/**
 * loader就是一个函数
 * 当webpack解析资源时,会调用相应的loader去处理
 * loader接收到文件内容作为参数,返回内容出去
 * @param {*} content 接收到的内容
 * @param {*} map sourcemap相关的
 * @param {*} meta 其它loader传过来的数据
 * @returns 处理后的内容
 */
module.exports = function(content, map, meta) {
  console.log(content, 'content')
  return content;
}
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'js/[name].js',
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: './loaders/test-loader.js'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './public/index.html')
    }),
  ],
  mode: 'development'
}

npx webpack执行打包命令

loader分类

同步loader
// 同步loader
// module.exports = function(content) {
//   console.log(content, 'content')
//   return content;
// }
// 同步laoder的第二种写法
module.exports = function(content, map, meta) {
  console.log('test1')
  /**
   * param1: err 是否有错误
   * param2: content 处理后的内容
   * param3: map source-map(继续传递source-map)
   * param4: meta 给先一个loader传递的参数
   */
  this.callback(null, content, map, meta);
  // 同步laoder中不能进行异步操作
  // setTimeout(() => {
  //   this.callback(null, content, map, meta);
  // }, 1000)
}
异步loader

loader内部的异步执行完后,再执行下一个loader

// 异步loader
module.exports = function(content, map, meta) {
  const callback = this.async();
  setTimeout(() => {
    console.log('test2')
    callback(null, content, map, meta)
  }, 1000);
}
raw loader

接收到的内容是二进制数据,用来处理图片等的内容

// raw loader 接收到的content内容是buffer数据(二进制数据)
// module.exports = function(content) {
//   console.log(content); // 二进制数据
//   return content;
// }
// module.exports.raw = true;

// raw loader 另一种写法
function test3Loader(content) {
  return content;
}
test3Loader.raw = true;
module.exports = test3Loader;
pitch loader

loader上带有pitch方法的叫pitchloader。

// pitch loader 
// loader的pitch方法会在loader执行前调用
// 比如use[loader1, loader2, loader3], 3个loader上都有pitch方法
// 调用pitch方法调用顺序为 pitch1 > pitch2 > pitch3 > loader3 > loader2 > loader1

// 如果pitch方法内有return; 则后面的pitch不会执行,紧接着会执行上一个nomal loader(这种机制叫熔断机制)
// 例如 loader2的pitch方法内有return,则执行顺序为 pitch1 > pitch2 > loader1
module.exports = function(content) {
  console.log('normal loader 1')
  return content;
}

module.exports.pitch = function() {
  console.log('pitch loader 1')
  // return 'result'
}

loader api

loader是一个函数,在函数内可以通过this访问loader的api

this.async 
含义:异步回调loader,返回this.callback
用法:const callback = this.async();

this.callback
含义:可以同步或异步调用的返回多个结果的函数
用法:this.callback(error, content, sourceMap?, meta?);

this.getOptions(schema)
含义: 获取loader的options
用法: this.getOptions(schema); schema是一个验证规则,如果option不符合验证规则们就会报错

this.emitFile
含义:产生一个文件
用法: this.emitFil(name, content, sourceMap)

this.utils.contextify
含义: 返回一个相对路径
用法: this.utils.contextify(context, request);

this.utils.absolutify
含义:返回一个绝对路径
用法: this.utils.absolutify(context, request);

api文档: https://webpack.docschina.org/api/loaders

自定义clean-log-loader

module.exports = function(content) {
  // 清除文件中的console.log(xxx)
  return content.replace(/console\.log\(.*\);?/g,'');
}
使用clean-log-loader
module: {
    rules: [
      {
        test: /\.js$/,
        // use: ['./loaders/test-loader.js']
        loader: './loaders/clean-log-loader.js'
      }
    ]
}

自定义banner-loader

banner-loader/index.js

const schema = require('./shcema.json')
module.exports = function(content) {
  // 在内容前面增加上作者名字, 作者名字由options(参数)传入
  // schema: 对options的验证规则
  // schema符合JSON Schema的规则
  const options = this.getOptions(schema);
  const prefix = `
  /**
   * author: ${options.author}
   */
  `;
  return prefix + content;
}

banner-loader/schema.json

{
  "type": "object", // 类型为 对象
  "properties": { // 有哪些属性
    "author": { // author属性
      "type": "string"  // 类型为 字符串
    }
  },
  "additionalProperties": false // 是否允许追加属性
}

使用此loader

module: {
    rules: [
      {
        test: /\.js$/,
        loader: './loaders/banner-loader',
        options: {
          author: '老王',
          // age: 18 不能新增字段,不然会报错
        }
      }
    ]
  }

自定义babel-loader

我们用babel的@babel/core和@babel/preset-env

npm i @babel/core @babel/preset-env -D

babel-loader/index.js

const schema = require('./shcema.json');
const babel = require("@babel/core");
// https://babel.docschina.org/docs/en/babel-core/
module.exports = function(content) {
  // 异步loader
  // babel-loader
  const callback = this.async();
  const options = this.getOptions(schema);
  // 使用babel对代码进行编译
  babel.transform(content, options, function(err, result) {
    if(err) callback(err);
    else callback(null, result.code);
  });
}

babel-loader/schema.json

{
  "type": "object",
  "properties": {
    "presets": {
      "type": "array"
    }
  },
  "additionalProperties": true
}

使用

module: {
    rules: [
      {
        test: /\.js$/,
        loader: './loaders/babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    ]
  }

自定义file-loader

file-loader/index.js

npm i loader-utils - D

// 借助webpack中的loader-utils中的
const loaderUtils = require('loader-utils');

module.exports = function(content) {
  // file-loader
  // 把资原封不动的输出出去
  // 处理的都是图片、字体等buffer数据
  // 步骤
  // 1:根据文件内容生成带hash值的文件名
  const interpolatedName = loaderUtils.interpolateName(
    this,
    "[hash].[ext][query]",
    { content }
  );
  console.log(interpolatedName, 'interpolatedName==='); // 4e1e5120a3ecc13d.png
  // 2:将文件输出出去
  this.emitFile(interpolatedName, content);
  // 3.返回,module.exports = "文件路径(文件名)"
  return `module.exports = "${interpolatedName}"`
}
module.exports.raw = true;

使用:

{
    test: /\.(png|jpe?g|gif)$/,
    loader: './loaders/file-loader',
    type: 'javascript/auto', // 禁止webpack默认处理这些资源,只让自己的file-loader生效
}

自定义style-loader

style-loader/index.js

module.exports = function(content) {
  /**
   * 1.直接使用style-loader,只能处理样式,不能处理样式中引入的其它资源,比如图片
   * use: ['./loaders/style-loader']
   * 2.借助css-loader解决样式中引入的其它资源的问题
   * use: ['./loaders/style-loader', 'css-loader']
   * 产生的问题是css暴露的是一段js代码,style-loader需要执行js代码,得到返回值,再动态创建style标签,放到页面上
   * 不好操作
   * 3.style-loader使用pitch loader的用法解决
   */
  // const script = `
  //   const styleEl = document.createElement('style');
  //   styleEl.innerHTML = ${JSON.stringify(content)};
  //   document.head.appendChild(styleEl);
  // `;
  // return script;
}

module.exports.pitch = function(remainingRequest){
  // remainingRequest剩下还需要处理的loader
  console.log(remainingRequest, 'remainingRequest==='); // c:root\loader\node_modules\css-loader\dist\cjs.js!c:root\loader\src\css\index.css
  // 如果拿到css-loader的处理结果,再让style-loader去处理就解决了问题
  // 1.将remainingRequest中的绝对路径改成相对路径(因为后面只能通过相对路径操作)
  // 希望: ..\..\node_modules\css-loader\dist\cjs.js!.\index.css
  const relativePath = remainingRequest.split('!').map(absolutePath => {
    // this.context当前loader的目录
    // 处理成相对路径 (absolutePath相对于this.context)
    return this.utils.contextify(this.context, absolutePath);
  }).join('!');
  console.log(relativePath, 'relativePath==='); // ../../node_modules/css-loader/dist/cjs.js!./index.css
  // 2.引入css-loader处理后的资源(!! 代表终止后面loader的执行)
  // 3.创建style标签,将内容插入到页面生效
  const script = `
    import style from "!!${relativePath}";
    const styleEl = document.createElement('style');
    styleEl.innerHTML = style;
    document.head.appendChild(styleEl);
  `;
  // 终止后面loader执行
  return script;
}

使用:

{
    test: /\.css$/,
    use: ['./loaders/style-loader', 'css-loader']
}