webpack5源码解析

2,128 阅读11分钟

Tapable

这是webpack里的核心工具,webpack里的很多对象都继承了它(比如compiler)。它暴露了tap,tapAsync,tapPromise方法,插件可以用这些方法,在整个webpack的构建周期注入自定义构建。

Hook类

tapable导出的钩子类:

const {
    SyncHook,
    SyncBailHook,
    AsyncParallelHook,
    AsyncSeriesHook
} = require("tapable");

事件回调的运行逻辑:

类型描述
Basic基础类型,单纯的调用注册的事件回调,不关心内部运行逻辑
Bail保险类型,当一个事件回调在运行时,返回值不是undefined,停止后面事件回调的执行
Waterfall瀑布类型,如果当前执行的事件回调的返回值不为undefined,那么就把下一个事件回调的第一个参数,替换为当前返回值
Loop循环类型,如果当前执行的事件回调的返回值不为undefined,递归调用注册事件直到没有返回值

触发事件的方式:

类型描述
SyncSync开头的钩子,只能用tap方法注册事件回调,这类事件回调会同步执行。
AsyncParallelAsync开头的钩子,只能用callAsyncpromise方法触发回调,AsyncParallel并行执行回调
AsyncSeries串行执行回调

注册事件回调

tap,tapAsync,tapPromise

const {SyncHook} = require('tapable')
const hooks = {
    aaa: new SyncHook()
}
// 注册事件回调的方法,例如 tap,它们的第一个参数可以是事件回调的名字,也可以是配置对象
hooks.aaa.tap('hookname', () => {console.log(1)})
// stage:这个属性的类型是数字,数字越大事件回调执行的越晚
hooks.aaa.tap({name: 'secendHookName', stage: 20}, () => {console.log(2)})
// before:在哪个钩子名称之前执行
hooks.aaa.tap({name: 'thirdHookName', before: 'secendHookName'}, () => {console.log(3)})
hooks.aaa.call()
// 打印顺序就是:1 3 2

触发事件

call,callAsync,promise.then

拦截器

可以对钩子的注册、调用的触发进行监听

const { SyncHook } = require('tapable');
const hook = new SyncHook();

hook.intercept({
  // 注册时执行
  register(tap) {
    console.log('register', tap);
    return tap;
  },
  // 触发事件时执行
  call(...args) {
    console.log('call', args);
  },
  // 在 call 拦截器之后执行
  loop(...args) {
    console.log('loop', args);
  },
  // 事件回调调用前执行
  tap(tap) {
    console.log('tap', tap);
  },
});

流程源码解析

从webpack的main入口进去,会看到webpack要求必须要安装webpack-cli,因为,webpack把关于命令行的version、help等命令都交给cli处理,关键的build和watch命令,才真正的在webpack代码中处理。

处理build或watch都需要createCompiler,生成compiler实例。然后运行compiler.watch()compiler.run()

compiler = createCompiler(webpackOptions);
// 创建compiler实例
const { compiler, watch, watchOptions } = create();
    if (watch) {
	compiler.watch(watchOptions, callback);
    } else {
            // 运行compiler.run,来call所有注册的钩子回调
            compiler.run((err, stats) => {
                    compiler.close(err2 => {
                            callback(err || err2, stats);
                    });
            });
    }

截屏2021-08-14 下午9.08.45.png

createCompiler流程解析

先上图, 截屏2021-08-14 下午9.21.22.png 在createCompiler中,

  1. 首先webpack会整合配置参数。
  2. 优化node环境,比如node的fs模块,webpack专门用了graceful-fs包封装,以及日志打印,文件监听,文件读取缓存等。
  3. 然后,webpack便开始注册插件的钩子函数了,包括用户传入的插件,以及自己的内置插件。此处要结合我上文所说的tabpable一起来看。
  4. 调用了compiler的几个钩子函数。 此处比较重要的地方就是new WebpackOptionsApply().process(options, compiler);,这几乎贯穿了webpack打包的整个生命周期,因为内置插件都在这里注册了,后面讲到整个编译流程还会详细讲到这里。
const createCompiler = rawOptions => {
    // 获取webpack配置信息
    const options = getNormalizedWebpackOptions(rawOptions);
    applyWebpackOptionsBaseDefaults(options);
    // 创建compiler实例
    const compiler = new Compiler(options.context);
    // 给实例的options赋值
    compiler.options = options;
    // 
    new NodeEnvironmentPlugin({
        infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    // 执行webpack插件的apply方法
    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }
    // 给用户没有配置的options,赋上默认值
    applyWebpackOptionsDefaults(options);
    // 执行与配置环境相关钩子
    compiler.hooks.environment.call();
    // 执行after环境钩子
    compiler.hooks.afterEnvironment.call();
    // 这是个关键函数,开始根据用户配置,处理webapck的内置插件逻辑
    // 插件都是注册回调事件,真正调用要到compiler.run函数
    // 也就是说,到此所有的钩子函数都注册好了
    new WebpackOptionsApply().process(options, compiler);
    // 执行初始化钩子
    compiler.hooks.initialize.call();
    return compiler;
};

构建逻辑

compiler.run

其实,如下run函数结束后,整个编译流程就结束了,但是里面的逻辑真的是山路十八弯,这可以说是灵活的代码,就难免会丧失一些可读性吧。此处盗用一张别人的图,感觉把逻辑总结的很到位。webpack编译大概就是如下流程。我会针对这个图,再展开讲,它到底是出现在webpack源码中的哪个地方,以及是如何调用的整个流程。

image.png

run(callback) {
    if (this.running) {
        return callback(new ConcurrentCompilationError());
    }
    
    let logger;
    
    const finalCallback = (err, stats) => {
        if (logger) logger.time("beginIdle");
        this.idle = true;
        this.cache.beginIdle();
        this.idle = true;
        if (logger) logger.timeEnd("beginIdle");
        this.running = false;
        if (err) {
            this.hooks.failed.call(err);
        }
        if (callback !== undefined) callback(err, stats);
        this.hooks.afterDone.call(stats);
    };

    const startTime = Date.now();

    this.running = true;
    // 编译结束后的回调
    const onCompiled = (err, compilation) => {
        if (err) return finalCallback(err);
        // 同步执行(SyncBailHook)shouldEmit钩子注册的所有回调函数
        //(保险类型,当一个事件回调在运行时,返回值不是`undefined`,停止后面事件回调的执行)
        // 不需要输出文件
        if (this.hooks.shouldEmit.call(compilation) === false) {
            compilation.startTime = startTime;
            compilation.endTime = Date.now();
            const stats = new Stats(compilation);
            // 执行done(AsyncSeriesHook)钩子
            this.hooks.done.callAsync(stats, err => {
                    if (err) return finalCallback(err);
                    return finalCallback(null, stats);
            });
            return;
        }

        process.nextTick(() => {
            logger = compilation.getLogger("webpack.Compiler");
            logger.time("emitAssets");
            // 输出文件
            this.emitAssets(compilation, err => {
                    logger.timeEnd("emitAssets");
                    if (err) return finalCallback(err);

                    if (compilation.hooks.needAdditionalPass.call()) {
                        compilation.needAdditionalPass = true;

                        compilation.startTime = startTime;
                        compilation.endTime = Date.now();
                        logger.time("done hook");
                        const stats = new Stats(compilation);
                        this.hooks.done.callAsync(stats, err => {
                            logger.timeEnd("done hook");
                            if (err) return finalCallback(err);

                            this.hooks.additionalPass.callAsync(err => {
                                if (err) return finalCallback(err);
                                    this.compile(onCompiled);
                                });
                            });
                            return;
                        }

                        logger.time("emitRecords");
                        this.emitRecords(err => {
                            logger.timeEnd("emitRecords");
                            if (err) return finalCallback(err);

                            compilation.startTime = startTime;
                            compilation.endTime = Date.now();
                            logger.time("done hook");
                            const stats = new Stats(compilation);
                            this.hooks.done.callAsync(stats, err => {
                                    logger.timeEnd("done hook");
                                    if (err) return finalCallback(err);
                                    this.cache.storeBuildDependencies(
                                        compilation.buildDependencies,
                                        err => {
                                            if (err) return finalCallback(err);
                                            return finalCallback(null, stats);
                                        }
                                    );
                                });
                        });
                });
        });
    };

    const run = () => {
        // 串行执行beforeRun(AsyncSeriesHook)钩子注册的所有回调函数,传入complier对象作为参数
        this.hooks.beforeRun.callAsync(this, err => {
            // 出错执行错误回调钩子
            if (err) return finalCallback(err);
            // 串行执行run(AsyncSeriesHook)钩子注册的所有回调函数,传入complier对象作为参数
            this.hooks.run.callAsync(this, err => {
                if (err) return finalCallback(err);

                this.readRecords(err => {
                    if (err) return finalCallback(err);

                    this.compile(onCompiled);
                });
            });
        });
    };

    if (this.idle) {
        this.cache.endIdle(err => {
            if (err) return finalCallback(err);

            this.idle = false;
            run();
        });
    } else {
        run();
    }
}

addEntry入口逻辑

也就是在我上文说到的new WebpackOptionsApply().process(options, compiler);里面,有一个EntryOptionPlugin里有EntryPlugin,在compiler.hooks.make.tapAsync钩子(真正编译开始的钩子)里调用了compilation.addEntry(context, dep, options,...

除此之外,还有DllEntryPlugin,DynamicEntryPlugin,DynamicEntryPlugin,ContainerPlugin这四个插件都在compiler.hooks.make钩子调用了addEntry方法。

先上图,好像有点小,可能需要放大看,后面还会给出更详细的流程图,这个是看个大概。

截屏2021-08-14 下午10.18.27.png 此处再盗用一张图,可以结合我的中文描述一起看看,便于理解:

image.png 大致流程是:

  1. 入口文件调用了addModuleTree,根据要处理的依赖的dependency.constructor,从webpack的dependencyFactories中匹配相应的模块工厂函数,dependencyFactories在很多模块插件中set了值,基本上都是set的normalModuleFactory
  2. 接着调用了handleModuleCreation函数
  3. 之后在this.factorizeModule(options, (err, newModule) => {})中解析引入的模块。

下面来详细聊一下解析模块(factorizeModule)的步骤:

  1. 首先调用模块工厂函数的create方法:factory.create-->normalModuleFactory,创建模块实例NormalModule
  2. 添加模块
  3. 构建模块
  4. 处理模块依赖
  5. 循环调用:handleModuleCreation函数,直到解析完所有依赖为止。

构建逻辑中,用到了webpack的loader,大家可以看下面的loader处理逻辑,来进一步理解构建流程。

loader处理逻辑

1、初始化ruleSet

在创建normalModuleFactory时,通过webpack默认的rules,以及用户传入的rules配置,得到一个ruleSet

this.ruleSet = ruleSetCompiler.compile([
    {
        rules: options.defaultRules
    },
    {
        rules: options.rules
    }
]);

比如,css就是需要用户配置rule的,不然解析css文件,wenpack会报错。

ERROR in ./xxx.css 1:5
Module parse failed: Unexpected token (1:5)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. 

默认的webpack的rule配置有:

{mimetype"application/node"type"javascript/auto"}
{test/\.json$/itype"json"}
{mimetype"application/json"type"json"}
{test/\.mjs$/itype"javascript/esm"resolve: {…}}
{test/\.js$/idescriptionData: {…}, type"javascript/esm"resolve: {…}}
{test/\.cjs$/itype"javascript/dynamic"}
{test/\.js$/idescriptionData: {…}, type"javascript/dynamic"}
{mimetype: ["text/javascript","application/javascript"], type"javascript/esm"resolve: {…}}
{dependency"url", oneOf: [{scheme/^data$/type"asset/inline"},{type"asset/resource"}]
        

除此之外的,css,vue等文件都需要配置相应的loader。

module: {
    rules: [
        {
            test: /\.css$/,
            loader: "css-loader"
        }
    ]
}

2、执行ruleSet,收集模块信息

normalModuleFactoryresolve钩子下,会调用this.ruleSet.exec得到module的type,value等信息。如果是css文件会得到如下result信息:

1.  type"use"
1.  value:
1.  1.  identundefined
    1.  loader"css-loader"
    1.  optionsundefined

如果typeuse,则会在useLoaderspush相应的loader,后续runLoaders的时候就会用到

if (r.type === "use") {
    if (!noAutoLoaders && !noPrePostAutoLoaders) {
        useLoaders.push(r.value);
    }
}

3、调用runLoaders,处理模块资源

NormalModule中的module.build,调用了doBuild,里面调用了runLoaders。loader处理后的结果,最终所有的资源都会处理成webpack的parser能够parse的类型。

截屏2021-08-15 下午12.31.51.png

例如,如下是css文件的处理结果,也是处理成了JavascriptParser能够处理的类型:

// Imports
import ___CSS_LOADER_API_IMPORT___ from "../../node_modules/css-loader/dist/runtime/api.js";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(function(i){return i[1]});
// Module
___CSS_LOADER_EXPORT___.push([module.id, "html {
    height: 100%;
    }
    ", ""]);
// Exports
export default ___CSS_LOADER_EXPORT___;
    

4、模块资源解析

用相应的parser解析文件:this.parser.parse(this._ast || this._source.source() ,这里parser的类型与上面提到的type的值相联系,具体可看如下图:

截屏2021-08-15 下午12.36.01.png

JavascriptParser为例,会对资源进行AST解析。当检测到是import或者export的声明时,就会在parser.hooks.import钩子里,给dependency添加依赖:parser.state.module.addDependency(sideEffectDep);

5、处理解析依赖

handleParseResult函数中,会调用module.build传进来的callback函数,最终会走到compilationprocessModuleDependencies函数。然后该函数会针对之前AST解析时,添加在module中的dependency,循环调用processDependency,最终得到一个sortedDependencies。后续就是遍历依赖循环调用了,直到解析完所有依赖为止。

asyncLib.forEach(
    sortedDependencies,
    (item, callback) => {
        this.handleModuleCreation(item, err => {
        // In V8, the Error objects keep a reference to the functions on the stack. These warnings &
        // errors are created inside closures that keep a reference to the Compilation, so errors are
        // leaking the Compilation object.
        if (err && this.bail) {
            // eslint-disable-next-line no-self-assign
            err.stack = err.stack;
            return callback(err);
        }
        callback();
        });
    },
    err => {
        this.processDependenciesQueue.decreaseParallelism();
        return callback(err);
    }
);

文件生成逻辑

文件生成逻辑围绕chunks展开,从compilation.seal开始。注意的节点有:

  • hooks.optimizeChunkssplitChunksPlugin就是在这里处理chunks
  • createChunkAssets:里面调用了getRenderManifest,在compilation.hooks.renderManifest钩子中,针对js模块,图片等assets模块,生成一个assets对象,用于后来生成打包后的文件

截屏2021-08-17 下午10.32.13.png seal 原意密封、上锁,我个人理解在 webpack 语境下接近于  “将模块装进蜜罐”  。seal 函数主要完成从 module 到 chunks 的转化,核心流程由盗图一张:

image.png 至于打包出来的js代码是怎么生成的,一方面是__webpack_modules__const chunkModules = Template.renderChunkModules),一方面是webpack启动代码,都是通过webpack-sourcesrenderBootstrap)来拼接的。看如下逻辑: 截屏2021-08-19 下午9.27.06.png

webpack-sources

webpack的代码拼接以及生成sourceMap都是依赖的这个包。

Source(基类)

Source是所有webpack-sources实现类的基类,是一个抽象类。

名称类型参数备注
source() => source获得传入实例中的字符串代码
buffer() => BufferString获得 Buffer 字符串代码
size() => BufferString.length获得 source 转换为 BufferString 的长度
map(options) => (options) => SourceMapObj{columns: false}获取传入代码的 sourcemap 信息,参数为生成的 sourcemap 是否包含列信息
sourceAndMap(options) => ({source, map}){columns: false}此方法整合了 sourcemap 方法的返回值,将其一并返回
updateHash(hash) => void要更新替换的 hash 值更新当前 Source 的 hash 值

OriginalSource(可以生成代码的 sourcemap 对象)

const header = Template.asString(bootstrap.header) + "\n";
new PrefixSource(
    prefix,
    useSourceMap
    ? new OriginalSource(header, "webpack/bootstrap")
    : new RawSource(header)
)

ConcatSource(组合资源片段)

let source = new ConcatSource();
if (
    chunkModules ||
    runtimeRequirements.has(RuntimeGlobals.moduleFactories) ||
    runtimeRequirements.has(RuntimeGlobals.moduleFactoriesAddOnly)
) {
    source.add(prefix + "var __webpack_modules__ = (");
    source.add(chunkModules || "{}");
    source.add(");\n");
    source.add(
            "/************************************************************************/\n"
    );
}

最终生成的代码如下:


/******/ var __webpack_modules__ = {
/***/ "./example.js":
/*!********************************!*\
!*** ./example.js + 3 modules ***!
\********************************/
/***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
eval(
    '\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n  "add": () => (/* reexport */ add),\n  "addMore": () => (/* reexport */ addMore)\n});\n\n;// CONCATENATED MODULE: ./add.js\nfunction add(a, b) {\n\treturn a + b;\n}\n\n// EXTERNAL MODULE: ./images/file.png\nvar file = __webpack_require__("./images/file.png");\n;// CONCATENATED MODULE: ./addMore.js\n\n\nfunction addMore(a, b) {\n\treturn add(a, b) + 22;\n}\n\n;// CONCATENATED MODULE: ./index.json\nconst index_namespaceObject = {};\n;// CONCATENATED MODULE: ./example.js\n// ImportDeclaration\n// ./add --> statement.source.value\n//  parser.hooks.import.call() --> HarmonyImportDependencyParserPlugin\n\n\n// ImportDeclaration\n\nconsole.log(file);\naddMore(1, 2);\nadd(1, 2);\n// HarmonyImportSideEffectDependency\n\n// CommonJsRequireDependency\n__webpack_require__(/*! ./index.css */ "./index.css");\n// "ExpressionStatement" "ExpressionStatement" "ExportNamedDeclaration" --> HarmonyExportDependencyParserPlugin\n// harmonyNamedExports.add(\'add\') -- HarmonyExportSpecifierDependency\n\n\n\n//# sourceURL=webpack:///./example.js_+_3_modules?'
);

/***/
},
/***/ "./images/file.png":
/*!*************************!*\
!*** ./images/file.png ***!
\*************************/
/***/ (module, __unused_webpack_exports, __webpack_require__) => {
eval(
    'module.exports = __webpack_require__.p + "89a353e9c515885abd8e.png";\n\n//# sourceURL=webpack:///./images/file.png?'
);

/***/
},
/***/ "./index.css":
/*!*******************!*\
!*** ./index.css ***!
\*******************/
/***/ (module, __webpack_exports__, __webpack_require__) => {
eval(
    '__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ "../../node_modules/css-loader/dist/runtime/api.js");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__);\n// Imports\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default()(function(i){return i[1]});\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, "html {\\n\\theight: 100%;\\n}\\n", ""]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack:///./index.css?'
);
/***/
},
/***/ "../../node_modules/css-loader/dist/runtime/api.js":
/*!*********************************************************!*\
!*** ../../node_modules/css-loader/dist/runtime/api.js ***!
\*********************************************************/
/***/ module => {
eval(
    '\n\n/*\n  MIT License http://www.opensource.org/licenses/mit-license.php\n  Author Tobias Koppers @sokra\n*/\n// css base code, injected by the css-loader\n// eslint-disable-next-line func-names\nmodule.exports = function (cssWithMappingToString) {\n  var list = []; // return the list of modules as css string\n\n  list.toString = function toString() {\n    return this.map(function (item) {\n      var content = cssWithMappingToString(item);\n\n      if (item[2]) {\n        return "@media ".concat(item[2], " {").concat(content, "}");\n      }\n\n      return content;\n    }).join("");\n  }; // import a list of modules into the list\n  // eslint-disable-next-line func-names\n\n\n  list.i = function (modules, mediaQuery, dedupe) {\n    if (typeof modules === "string") {\n      // eslint-disable-next-line no-param-reassign\n      modules = [[null, modules, ""]];\n    }\n\n    var alreadyImportedModules = {};\n\n    if (dedupe) {\n      for (var i = 0; i < this.length; i++) {\n        // eslint-disable-next-line prefer-destructuring\n        var id = this[i][0];\n\n        if (id != null) {\n          alreadyImportedModules[id] = true;\n        }\n      }\n    }\n\n    for (var _i = 0; _i < modules.length; _i++) {\n      var item = [].concat(modules[_i]);\n\n      if (dedupe && alreadyImportedModules[item[0]]) {\n        // eslint-disable-next-line no-continue\n        continue;\n      }\n\n      if (mediaQuery) {\n        if (!item[2]) {\n          item[2] = mediaQuery;\n        } else {\n          item[2] = "".concat(mediaQuery, " and ").concat(item[2]);\n        }\n      }\n\n      list.push(item);\n    }\n  };\n\n  return list;\n};\n\n//# sourceURL=webpack:///../../node_modules/css-loader/dist/runtime/api.js?'
);
/***/
}
/******/
};

image.png

所有钩子执行顺序流程图

webpack.png

参考文献