本文基于webpack5.0.0-beta.7, 参考一些优秀的文章,通过debug源码的方式,了解webpack的编译主流程,仅供参考。
准备工作
1
| git clone git@github.com:webpack/webpack.git
|
为了不污染源码,在根目录新建debug文件夹📃,并创建如下文件:
1 2 3 4
|
const name = "webpack"; console.log("很高兴认识你,", name);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
const path = require("path"); module.exports = { context: __dirname, mode: "development", devtool: "source-map", entry: "./src/index.js", output: { path: path.join(__dirname, "./dist") }, module: { rules: [ { test: /\.js$/, use: ["babel-loader"], exclude: /node_modules/ } ] } };
|
1 2 3 4 5 6 7 8 9 10 11 12 13
|
const webpack = require("../lib/index.js"); const config = require("./webpack.config"); const compiler = webpack(config); compiler.run((err, stats) => { if (err) { console.error(err); } else { console.log(stats); } });
|
1 2 3 4 5 6 7 8 9 10
| { "configurations": [ { "type": "node", "request": "launch", "name": "启动webpack调试程序", "program": "${workspaceFolder}/debug/start.js" } ] }
|
至此,点击▶️,看看调试程序是否正常运行(若正常运行,会在debug/dist自动生成打包后的js文件)
接下来,就可以在源码进行断点调试啦~~ 🤔
源码解读
compiler.run()
cd webpack, 打开package.json, 找到main字段,可以看到webpack的主入口在lib/index.js
当调用了const compiler = webpack(config),其实就是创建一个Compiler实例,在这里我们发现,Compiler在webpack运行过程中,仅此创建一次,贯穿整个打包过程,下面为主要逻辑代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const createCompiler = options => { const compiler = new Compiler(options.context); compiler.options = options; if (Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); compiler.options = new WebpackOptionsApply().process(options, compiler); return compiler; };
|
当准备工作做好,内部钩子相关联的事件处于蓄势待发状态,接下来把我们进入Compiler, 看里面的run方法是什么回事🤔?
最终定位到this.compile(), 通过callAsync触发钩子,逻辑代码如下:
1 2 3 4 5 6 7 8 9 10 11
| this.hooks.make.callAsync(compilation, err => { compilation.finish(err => { compilation.seal(err => { this.hooks.afterCompile.callAsync(compilation, err => { return callback(null, compilation); }); }); }); });
|
这里需要提下Tapable,Tapable的核心功能是根据不同钩子将注册的事件在触发时按序进行,是典型的发布者/订阅模式,在Tapable的帮助下,原本杂乱无章的事件,能够得到有效的控制。
简单实现一个SyncHook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Hooks { constructor(args) { this.taps = []; this.interceptors = []; this._args = args; }
tab(name, fn) { this.taps.push({ name, fn }) }
}
class SyncHooks extends Hooks { call(name, fn) { try { this.taps.forEach(tap => tap.fn(name)); fn(null, name); }catch(error) { fn(error) } } }
|
process(options, compiler)
当调用this.hooks.make.callAsync其实触发了什么?为了找到答案,我们在vscode全局搜索hooks.make.tapAsync, 发现lib/EntryPlugin.js下有如下逻辑:
成熟的开源库,往往作者大佬进行了非常优雅的封装,代码的模块依赖也非常错中复杂,依靠搜索关键词可以快速找到答案。
1 2 3 4 5 6 7 8
| compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => { const { entry, name, context } = this;
const dep = EntryPlugin.createDependency(entry, name); compilation.addEntry(context, dep, name, err => { callback(err); }); });
|
先不管这里的逻辑是怎样的,思考下EntryPlugin是什么时候调用的?我们继续通过关键词new EntryPlugin,
在lib/EntryOptionPlugin.js找到EntryPlugin在此进行了实例化,继续回溯搜索new EntryOptionPlugin, 最终发现在lib/WebpackOptionsApply.js发现它的身影。
回到最初createCompiler方法,如下,compiler.options = new WebpackOptionsApply().process(options, compiler)这行代码,make钩子在process函数中就已经注册好了, 把一切准备工作都关联了起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const createCompiler = options => { const compiler = new Compiler(options.context); compiler.options = options; if (Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); compiler.options = new WebpackOptionsApply().process(options, compiler); return compiler; };
|
回到lib/EntryPlugin.js看看compiler.hooks.make.tapAsync都干了啥。其实就是运行compiliation.addEntry方法,继续探索compiliation.addEntry。
1 2 3 4 5 6 7 8 9 10
| addEntry(context, entry, name, callback) { this.hooks.addEntry.call(entry, name); let entriesArray = this.entryDependencies.get(name) entriesArray.push(entry) this.addModuleChain(context, entry, (err, module) => { this.hooks.succeedEntry.call(entry, name, module); return callback(null, module); }) }
|
通过debug找出函数的调用顺序
this.addEntry –> this.addModuleChain –> this.handleModuleCreation –> this.addModule –> this.buildModule –> this._buildModule –> module.build(this指代compiliation)
在/lib/NormalModule.js下找到build方法,返回doBuild方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| build(options, compilation, resolver, fs, callback) {
return this.doBuild(options, compilation, resolver, fs, err => { try { const result = this.parser.parse( this._ast || this._source.source(), { current: this, module: this, compilation: compilation, options: options }, (err, result) => { if (err) {
} else { handleParseResult(result); } } ); if (result !== undefined) { handleParseResult(result); } } catch (e) { } }); }
|
doBuild方法其实就是用适合的loader去加载模块资源,webpack只能识别js文件,通过doBuild后,
所有文件都转换了js文件。
接着通过传入的callback,执行this.parser.parse,this.parser是JavascriptParser的实例,
最终调用parse调用第三方库acorn对js源代码进行词法解析, 这个过程就是收集模块之间的依赖。
1 2 3 4 5 6 7 8 9 10 11 12
| parse(code, options){ let ast = acorn.parse(code) if (this.hooks.program.call(ast, comments) === undefined) { this.detectStrictMode(ast.body) this.prewalkStatements(ast.body) this.blockPrewalkStatements(ast.body) this.walkStatements(ast.body) } }
|
compilation.seal()
至此从入口文件,webpack已经收集了所有的模块依赖,就等着打包输入啦~~(真累🥺)
compilation.seal最终将资源保存在compilation.assets, compilation.chunks中,所以在编写
webpack插件时候,经常会看到这两个的字段的身影。
compiler.hooks.emit.callAsync()
执行seal后,所以资源都保存到内存中了,最后调用emit钩子根据webpack.config的配置将文件输出到指定路径。
总结
写得比较模糊仓促,其实对于webpack loader/tapable这一块还可以继续深入探究,可能只有自己才能看得懂哈哈哈,anyway~~ 有空再回头补充下,感谢开源~~