webpack再回首loader

116 阅读2分钟

Webpack 是一个前端资源构建工具,一个模块打包器,核心是处理字符串。Webpack 通过配置找到入口及其所有的依赖文件,然后经过各种 Loader 的转换处理,并且通过 Plugin 的方式广播出特定的事件,Plugin 监听事件并执行相应的逻辑,而且可以调用 Webpack 提供的 api 改变 Webpack 最终产出的代码。

Webpack 打包流程

loader

loader(模块转换器):用于把模块原内容按照需求转换成新内容

         在实际开发过程中,webpack 默认只能打包处理以 js 后缀名结尾的模块。其他非 js 后缀名结尾的模块,webpack 默认处理不了,需要调用 loader 加载器才可以正常打包,否则会报错!

loader加载器的作用:协助 webpack 打包处理特定的文件模块。比如:

  • css-loader 可以打包处理.css相关的文件
  • less-loader 可以打包处理.less相关的文件
  • babel-loader 可以打包处理 webpack 无法处理的高级 JS 语法

调用过程

webpack 入口文件 webpack.js ,根据配置文件 设置配置的options

options = new WebpackOptionsDefaulter().process(options);compiler = new Compiler(options.context);compiler.options = options;

WebpackOptionsDefaulter 加载 默认配置

// WebpackOptionsDefaulter.js

this.set("module.defaultRules", "make", options => [
    {
        type: "javascript/auto",
        resolve: {}
},
    {
        test: /\.mjs$/i,
        type: "javascript/esm",
        resolve: {
            mainFields: options.target === "web" ||
                options.target === "webworker" ||
                options.target === "electron-renderer" ?
                ["browser", "main"] :
                ["main"]
        }
},
    {
        test: /\.json$/i,
        type: "json"
},
    {
        test: /\.wasm$/i,
        type: "webassembly/experimental"
}
]);
//...


this.set("optimization.splitChunks.cacheGroups.default", {
    automaticNamePrefix: "",
    reuseExistingChunk: true,
    minChunks: 2,
    priority: -20
});
this.set("optimization.splitChunks.cacheGroups.vendors", {
    automaticNamePrefix: "vendors",
    test: /[\\/]node_modules[\\/]/,
    priority: -10
});

其中,test 表示匹配的文件类型,use 表示对应要调用的 loader
注意:

  • use 数组中指定的 loader 顺序是固定的
  • 多个 loader 的调用顺序是:从后往前调用

一组 loader 的执行有两个阶段:Pitching 阶段 和 Normal 阶段,类似于js中的事件捕获、冒泡

       webpack 的 loader-runner 会按正序(从左到右) require 每个 loader,把这个 loader 的模块导出函数 和 pitch函数都存到 loaderContext 对象上,然后执行该 loader 的 pitch 方法(如果有的话);如果一组 loader 的 pitch 都没有返回值,就开始 

Normal阶段:**反向(从右到左)**执行 loader 的导出函数,依次进行模块源码的转换,直到拿到最后的处理结果;

但是当 Pitching 阶段某个 loader 的 pitch 有返回值,那么就会跳过剩余未读取的 loader,直接进入执行 loader 的环节。从前一个 require 的 loader 开始执行,pitch 的返回值即是传入的第一个参数。除了 pitch 有返回的那个 loader,倒序执行已经 require 的每个 loader。

原理可参考:浅析 webpack 打包流程(原理) 二 之【执行 loader 阶段,初始化模块 module,并用 loader 倒序转译】部分

实现一个loader

loader承担的是翻译官的职责,利用其弥补了让webpack只能理解JavaScript和JSON文件的问题,从而可以处理其它类型的文件,所以loader对webpack的重要性不言而喻,所以学习构建一个loader是学习webpack的必经之路。在学习编写一个loader之前,要明确一下loader的职责:其职责是单一的,只需要完成一种转换。下面将逐步阐述选择loader开发中的几个关键点并实现一个loader。

同步LOADER

同步loader指的是同步的返回转换后的内容。由于是在Node.js这样的单线程环境,所以转换过程会阻塞整个构建,构建缓慢,不适用于耗时较长的环境中。对于同步loader,主要有两种方法返回转换后的内容:return和this.callback.

利用return可直接返回转换后结果

module.exports = function (source, map, meta) {
    // ...
    // output为处理后结果
    return output;
}

this.callback
该方法相比于return更加灵活,其参数主要有四个

this.callback(
    err: Error | null,
    content: string | Buffer,
    sourceMap ? : SourceMap,
    meta ? : any
);

1)第一个参数为无法转换原内容,Webpack会返回一个Error。
(2)第二个参数即为经过转换后的内容(为输出的内容)。
(3)指与编译后代码所映射的源代码,便于调试。为了在此loader中获取该sourceMap,则需要在创建的webpack做一下配置(以js为例,babel-loader会将基础ES6语法进行转换为ES5,通过devtool可以开启source-map):

// webpack.config.js
module.exports = {
    // ...
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    'test-loader', // 该loader即为自己构建的loader
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: [
                                '@babel/preset-env'
                            ]
                        }
                    }
                ]
            }
        ]
    },
    devtool: 'eval-source-map',
}

可以是任何东西,输出该参数,即可在下一个loader中获取并使用,例如通过各loader之间共享通用的AST,加速编译时间。

利用this.callback可返回传递多参数的结果。

module.exports = function (source, map, meta) {
    // 处理后获得的结果output
    const output = dealOperation(source);
    this.callback(null, output, map, meta);
}

异步LOADER

同步loader只适合于计算量小,速度快的场景,但是对于计量量大、耗时比较长的场景(例如网络请求),使用同步loader会阻塞整个构建过程,导致构建速度变慢,采用异步loader即可避免该问题。对于异步loader,使用this.async()可以获取到callback函数,该函数参数和同步loader中this.callback参数一致。

module.exports = function (content, map, meta) {
    // 获取callback函数
    const callback = this.async();
    // 用setTimeout模拟该异步过程
    setTimeout(() => {
        // 处理后获得的结果output
        const output = dealOperation(source);
        callback(null, output, map, meta);
    }, 100)
}