loader 基础

  1. loaderwebpack 中用于处理模块的转换器;
  2. 它们可以将文件从一种格式转换为另一种格式,比如将 TypeScript 转换为 JavaScript,将 SCSS 转换为 CSS 等;使用 loader 可以使 webpack 处理各种类型的文件,而不仅仅是 JavaScript
  3. webpack 做的事情,仅仅是分析出各种模块的依赖关系,然后形成资源列表,最终打包生成到指定的文件中;更多的功能需要借助 webpack loaderswebpack plugins 完成;
  4. loader 本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串返回loader 函数将在模块解析的过程中被调用,从而得到最终的源码;

基础使用

  1. loader 就像是一个翻译员,能把源文件 经过转化后 输出新的结果,并且一个文件还可以链式地经过多个翻译员翻译;

  2. 以处理 SCSS 文件为例:

    1. SCSS 源代码会先交给 sass-loaderSCSS 转换成 CSS;​
    2. sass-loader 输出的 CSS 交给 css-loader 处理,找出 CSS 中依赖的资源、压缩 CSS 等;​
    3. css-loader 输出的 CSS 交给 style-loader 处理,转换成通过脚本加载的 JavaScript 代码;​
  3. 可以看出以上的处理过程需要有顺序的链式执行,先 sass-loadercss-loaderstyle-loader

  4. 以上处理的 webpack 相关配置如下:

    module.exports = {
        module: {
            rules: [
                {
                    test: /\.scss$/,
                    use: [
                        'style-loader',
                        {
                            loader: 'css-loader',
                            options: {
                                minimize: true,
                            },
                        },
                        'sass-loader',
                    ],
                },
            ],
        },
    };
    

loader 的职责

  1. 由上面的例子可以看出:

    1. 一个 loader 的职责是单一的,只需要完成一种转换;
    2. 如果一个源文件需要经历多步转换才能正常使用,就通过多个 loader 去转换;
    3. 在调用多个 loader 去转换一个文件时,每个 loader 会链式的顺序执行,第一个 loader 将会拿到需处理的原内容,上一个 loader 处理后的结果会传给下一个接着处理,最后的 loader 将处理后的最终结果返回给 webpack
  2. 所以,开发一个 loader 时,请保持其职责的单一性,只需关心输入和输出;

常用 loader 盘点

  1. babel-loader:将 ES6+ 代码转换为 ES5

    module.exports = {
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env'],
              },
            },
          },
        ],
      },
    };
    
  2. css-loader 和 style-loadercss-loader 解析 CSS 文件,style-loaderCSS 插入到 DOM

    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/,
            use: ['style-loader', 'css-loader'],
          },
        ],
      },
    };
    
  3. sass-loader:将 SCSS 文件转换为 CSS

    module.exports = {
      module: {
        rules: [
          {
            test: /\.scss$/,
            use: ['style-loader', 'css-loader', 'sass-loader'],
          },
        ],
      },
    };
    
  4. file-loader:处理文件(如图片、字体),并返回其 URL

    module.exports = {
      module: {
        rules: [
          {
            test: /\.(png|jpg|gif|svg)$/,
            use: [
              {
                loader: 'file-loader',
                options: {
                  name: '[name].[hash].[ext]',
                  outputPath: 'images/',
                },
              },
            ],
          },
        ],
      },
    };
    
  5. url-loader:类似于 file-loader,但在文件小于设定的限制时返回 Data URL

    module.exports = {
      module: {
        rules: [
          {
            test: /\.(png|jpg|gif|svg)$/,
            use: [
              {
                loader: 'url-loader',
                options: {
                  limit: 8192,
                  name: '[name].[hash].[ext]',
                  outputPath: 'images/',
                },
              },
            ],
          },
        ],
      },
    };
    
  6. html-loader:导入 HTML 文件,并处理其中的资源 (如 img 标签中的 src)

    module.exports = {
      module: {
        rules: [
          {
            test: /\.html$/,
            use: ['html-loader'],
          },
        ],
      },
    };
    
  7. ts-loader:将 TypeScript 转换为 JavaScript

    module.exports = {
      module: {
        rules: [
          {
            test: /\.ts$/,
            use: 'ts-loader',
            exclude: /node_modules/,
          },
        ],
      },
      resolve: {
        extensions: ['.ts', '.js'],
      },
    };
    
  8. vue-loader:处理 Vue 单文件组件

    const { VueLoaderPlugin } = require('vue-loader');
    
    module.exports = {
      module: {
        rules: [
          {
            test: /\.vue$/,
            use: 'vue-loader',
          },
        ],
      },
      plugins: [
        new VueLoaderPlugin(),
      ],
    };
    
  9. postcss-loader:使用 PostCSS 处理 CSS 文件

    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/,
            use: ['style-loader', 'css-loader', 'postcss-loader'],
          },
        ],
      },
    };
    

loader 进阶

loader 实现原理

  1. loader 是一个导出为函数的 Node.js 模块;函数接收源代码作为参数,并返回转换后的代码;loader 之间通过链式调用,前一个 loader 的输出作为下一个 loader 的输入;以下是 loader 的执行步骤:​

    1. 资源文件:首先 webpack 从入口开始读取资源文件;
    2. 匹配规则:根据配置中的规则,找到对应的 loader;​
    3. 链式调用:loader 按照配置中的顺序链式调用,将处理后的结果传递给下一个 loader;​
    4. 返回结果:最终结果返回给 webpack,由 webpack 生成最终的 bundle 文件;​
  2. 以下是一个更详细的 loader 示例,它不仅转换字符串,还通过 webpack 提供的 loader API 与其他 loader 进行交互:

    JavaScript
    JavaScript
    // custom-loader.js
    const loaderUtils = require('loader-utils');
    
    module.exports = function (source) {
      const options = loaderUtils.getOptions(this);
      const transformedSource = source.replace(/console\.log\(/g, `console.log('${options.prefix}: ' + `);
      return transformedSource;
    };
    
    // 在 webpack 配置中使用
    module.exports = {
      module: {
        rules: [
          {
            test: /\.js$/,
            use: {
              loader: path.resolve(__dirname, 'custom-loader.js'),
              options: {
                prefix: 'DEBUG',
              },
            },
          },
        ],
      },
    };
    

获得 loader 的 options

  1. 在处理 SCSS 文件的 webpack 配置中,给 css-loader 传了 options 参数,以控制 css-loader

  2. 如何在自己编写的 loader 中获取到用户传入的 options 呢?需要这样做:

    const loaderUtils = require('loader-utils');​
    ​
    module.exports = function(source) {​
      const options = loaderUtils.getOptions(this);​
      return source;​
    };
    

返回其他结果

  1. 上面的 loader 都只是返回了原内容转换后的内容,但有些场景下还需要返回除了内容之外的东西;

  2. 例如以 babel-loader 转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码;

  3. 为了把 Source Map 也一起随着 ES5 代码返回给 webpack,可以这样写:​

    module.exports = function(source) {​
      this.callback(null, source, sourceMaps);​
      return;​
    };
    

同步与异步

  1. loader 有同步和异步之分,上面介绍的 loader 都是同步的 loader,因为它们的转换流程都是同步的,转换完成后再返回结果;但在有些场景下转换的步骤只能是异步完成的,例如需要通过网络请求才能得出结果,如果采用同步的方式网络请求就会阻塞整个构建,导致构建非常缓慢;

  2. 在转换步骤是异步时,可以这样:

    module.exports = function (source) {
      // 告诉 Webpack​ 本次转换是异步的,Loader 会在 callback 中回调结果
      var callback = this.async();
    
      someAsyncOperation(source, function (err, result, sourceMaps, ast) {
        // 通过 callback 返回异步执行后的结果​
        callback(err, result, sourceMaps, ast);
      });
    };
    

处理二进制数据

  1. 在默认的情况下,webpack 传给 loader 的原内容都是 UTF-8 格式编码的字符串;

  2. 但有些场景下 loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就需要 webpackloader 传入二进制格式的数据,为此,需要这样编写 loader

    module.exports = function (source) {
      // 在 exports.raw === true 时,Webpack 传给 loader 的 source 是 Buffer 类型的​
      source instanceof Buffer === true;
    
      // loader 返回的类型也可以是 Buffer 类型的​
      // 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果​
      return source;
    };
    
    // 通过 exports.raw 属性告诉 webpack 该 loader 是否需要二进制数据​
    module.exports.raw = true;
    

缓存加速

  1. 在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢;为此,webpack 会默认缓存所有 loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时,是不会重新调用对应的 loader 去执行转换操作的;

  2. 如果想让 webpack 不缓存该 loader 的处理结果,可以这样:

    module.exports = function (source) {
      this.cacheable(false); // 关闭该 loader 的缓存功能​
      return source;
    };
    

其他 loader API

  1. 除了以上提到的在 loader 中能调用的 webpack API 外,还存在以下常用 API

  2. 其它没有提到的 API 可以去 webpack 官网 查看;

api 描述
this.context 当前处理文件的所在目录,假如当前 loader 处理的文件是 /src/main.js,则 this.context 就等于 /src
this.resource 当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1
this.resourcePath 当前处理文件的路径,例如 /src/main.js
this.resourceQuery 当前处理文件的 querystring
this.target 等于 webpack 配置中的 Target
this.loadModule loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时,就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果
this.resolve require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))
this.addDependency 给当前处理文件添加其依赖的文件,以便在其依赖的文件发生变化时,会重新调用 loader 处理该文件;使用方法为 addDependency(file: string)
this.addContextDependency addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中;使用方法为 addContextDependency(directory: string)
this.clearDependencies 清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()
this.emitFile 输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {...})

加载本地 loader

  1. 在开发 loader 的过程中,为了测试编写的 loader 是否能正常工作,需要把它配置到 webpack 中后,才可能会调用该 loader

  2. 上文中使用的 loader 都是通过 npm 安装的,要使用 loader 时会直接使用 loader 的名称,代码如下:

    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/,
            use: ['style-loader'],
          },
        ],
      },
    };
    
  3. 如果还采取以上的方法去使用本地开发的 loader 将会很麻烦,因为需要确保编写的 loader 的源码是在 node_modules 目录下;为此需要先把编写的 loader 发布到 npm 仓库后再安装到本地项目使用;

使用 npm link​

  1. npm link 专门用于开发和调试本地 npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 npm 模块;由于是通过软链接的方式实现的,编辑了本地的 npm 模块代码,在项目中也能使用到编辑后的代码;

  2. 完成 npm link 的步骤如下:​

    1. 确保正在开发的本地 npm 模块 (也就是正在开发的 loaderpackage.json 已经正确配置好;​
    2. 在本地 npm 模块根目录下执行 npm link,把本地模块注册到全局;​
    3. 在项目根目录下执行 npm link loader-name,把第 2 步注册到全局的本地 npm 模块链接到项目的 node_modules 下,其中的 loader-name 是指在第 1 步中的 package.json 文件中配置的模块名称;​
  3. 链接好 loader 到项目后就可以像使用一个真正的 npm 模块一样使用本地的 loader 了;

使用 resolveLoader​

  1. 为了让 webpack 加载放在本地项目中的 loader 需要修改 resolveLoader.modules

  2. 假如本地的 loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:

    module.exports = {
        resolveLoader: {
            // 去哪些目录下寻找 Loader,有先后顺序之分​
            modules: ['node_modules', './loaders/'],
        },
    };
    
  3. 加上以上配置后,webpack 会先去 node_modules 项目下寻找 loader,如果找不到,会再去 ./loaders/ 目录下寻找;

手写 loader

手写 style-loader

JavaScript
CSS
JavaScript
JavaScript
// index.js
var content = require("./assets/index.css");

console.log(content); // css 的源码字符串
/* assets/index.css */
body {
    background: #333;
    color     : #fff;
}
// loaders/style-loader.js
module.exports = function (sourceCode) {
    var code = `
        var style = document.createElement("style");
        style.innerHTML = \`${sourceCode}\`;
        document.head.appendChild(style);
        module.exports = \`${sourceCode}\`;
    `;
    return code;
}
// webpack.config.js
module.exports = {
    mode: "development",
    devtool: "source-map",
    module: {
        rules: [{
            test: /\.css$/,
            use: ["./loaders/style-loader"]
        }]
    }
}

手写 img-loader

JavaScript
JavaScript
JavaScript
// index.js
var src = require("./assets/webpack.png");
console.log(src);
var img = document.createElement("img");
img.src = src;
document.body.appendChild(img);
// loaders/img-loader.js
var loaderUtil = require("loader-utils"); // 用于获取 webpack 中配置的 options 中的参数

/***
 * 将 buffer 数组转成base64 字符串
 * @param {*} buffer 
 * @returns 
 */
function getBase64(buffer) {
    return "data:image/png;base64," + buffer.toString("base64");
}

/***
 * 获取文件名字
 * @param {*} buffer 
 * @param {*} name [contenthash].[ext]
 * @returns 
 */
function getFilePath(buffer, name) {
    // 根据 [单文件 buffer] 生成 hash
    var filename = loaderUtil.interpolateName(this, name, { content: buffer });
    this.emitFile(filename, buffer); // 向最终的文件列表中增加文件
    return filename;
}

function loader(buffer) {
    // 获取原始数据 buffer
    console.log("文件数据大小:(字节)", buffer.byteLength);

    var { limit = 1000, filename = "[contenthash].[ext]" } = loaderUtil.getOptions(this);
    if (buffer.byteLength >= limit) {
        var content = getFilePath.call(this, buffer, filename);
    } else {
        var content = getBase64(buffer);
    }

    return `module.exports = \`${content}\``;
}


// 设置为 true 后,loader 返回的就是原始数据,而不是字符串了
loader.raw = true;
module.exports = loader;
// webpack.config.js
module.exports = {
    mode: "development",
    devtool: "source-map",
    module: {
        rules: [
            {
                test: /\.(png)|(jpg)|(gif)$/, 
                use: [
                    {
                        loader: "./loaders/img-loader.js",
                        options: {
                            limit: 3000, // 3000 字节以上使用图片,3000 字节以内使用 base64
                            filename: "img-[contenthash:5].[ext]"
                    }
                }]
            }
        ]
    }
}

面试题

最后的输出结果

  1. 题目代码:

    JavaScript
    JavaScript
    JavaScript
    JavaScript
    JavaScript
    JavaScript
    // index.js
    require("./a.js"); // a.js 中内容为空
    
    // loaders/loader1.js
    module.exports = function (sourceCode) {
        console.log("loader1");
        return sourceCode;
    }
    
    // loaders/loader2.js
    module.exports = function (sourceCode) {
        console.log("loader2");
        return sourceCode;
    }
    
    // loaders/loader3.js
    module.exports = function (sourceCode) {
        console.log("loader3");
        return sourceCode;
    }
    
    // loaders/loader4.js
    module.exports = function (sourceCode) {
        console.log("loader4");
        return sourceCode;
    }
    
    // webpack.config.js
    module.exports = {
        mode: "development",
        module: {
            rules: [
                {
                    test: /index\.js$/, //正则表达式,匹配模块的路径
                    use: ["./loaders/loader1", "./loaders/loader2"] //匹配到了之后,使用哪些加载器
                }, //规则1
                {
                    test: /\.js$/, //正则表达式,匹配模块的路径
                    use: ["./loaders/loader3", "./loaders/loader4"] //匹配到了之后,使用哪些加载器
                } //规则2
            ], //模块的匹配规则
        }
    }
    
  2. 输出:看 chunk 中解析模块的更详细流程

    // index.js
    loader4
    loader3
    loader2
    loader1
    
    // a.js
    loader4
    loader3
    
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. loader 基础
    1. 1.1. 基础使用
    2. 1.2. loader 的职责
    3. 1.3. 常用 loader 盘点
  2. 2. loader 进阶
    1. 2.1. loader 实现原理
    2. 2.2. 获得 loader 的 options
    3. 2.3. 返回其他结果
    4. 2.4. 同步与异步
    5. 2.5. 处理二进制数据
    6. 2.6. 缓存加速
    7. 2.7. 其他 loader API
  3. 3. 加载本地 loader
    1. 3.1. 使用 npm link​
    2. 3.2. 使用 resolveLoader​
  4. 4. 手写 loader
    1. 4.1. 手写 style-loader
    2. 4.2. 手写 img-loader
  5. 5. 面试题
    1. 5.1. 最后的输出结果