作者:牟金涛
webpack可以说是目前最流行的前端脚手架,至少在Parceljs这类脚手架工具成熟之前,webpack依旧是前端架构首选,虽然其存在配置庞大复杂等问题,但其层次不穷功能强大甚至奇葩的loader与plugin满足了各种打包需求。 不过webpack不仅仅是配置复杂,实际上写一个webpack的插件也远比gulp复杂,相比gulp webpack的打包流程要复杂很多,因为一些项目需要,我简单的研究了一下webpack的源码,在此记录分享一下。
个人觉webpack源码中与流程相关最重要的几个部分其实包括了complier,compilation与plugin,前两者控制着流程后者则控制流程中具体操作,实际上webpack中的plugin并不单纯的是指我们配置的plugin,其内置的plugin更多,控制这入口文件的读取,parse过程中require的处理,模板路径处理等等......。可以先了解Tapable,个人理解Tapable就是一个事件对象,存储各种事件队列回调函数(这些回调就是plugin apply注册的函数)tapable可以通过applyPlugins,applyPluginsAsync来同步或者异步的回调这些函数。
我们一般调用webpack是通过命令行读取config文件来实现的,但是实际上我们也可以手动在js文件中起用webpack
const webpack = require('webpack');
const config = require('./webpack.config.js');
const path = require('path');
const fs = require('fs');
const compiler = webpack(config);
// compiler.watch({}, err => {
compiler.run(err => {
if (err) {
console.error(err)
throw err;
}
});
如果没有回调函数webpack只会生成comiler而不会运行compiler。而compiler是webpack中最重要的对象,它是整个webpack的核心,compiler控制着整个webpack的流程,从options处理一直到所有的资源处理完毕emit。 具体的事件请阅读webpack文档中的 comiler event hook 具体的实现在/lib/webpack.js
let compiler;
if(Array.isArray(options)) {
compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if(typeof options === "object") {
new WebpackOptionsDefaulter().process(options);
compiler = new Compiler();
compiler.context = options.context;
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
if(options.plugins && Array.isArray(options.plugins)) {
compiler.apply.apply(compiler, options.plugins);
}
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
if(callback) {
if(typeof callback !== "function") throw new Error("Invalid argument: callback");
if(options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {
const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : (options.watchOptions || {});
return compiler.watch(watchOptions, callback);
}
compiler.run(callback);
}
return compiler;
这一段中涉及了webpack中很重要的两个流程步骤,一个是compiler的建立,另一个则是传入参数。
compiler.options = new WebpackOptionsApply().process(options, compiler);
WebpackOptionsApply是处理参数的对象定义在/lib/WebpackOptionsApply是处理参数的对象定义在.js中,为什么说其重要,因为这个过程中的webpack并不是单纯的处理用户shell传参与config.js的传参,更多的其在处理过程中会apply大部分webpack需要的plugin,不说了直接上部分源码吧,有兴趣的可以打开webpack源码看一下
compiler.apply(new EntryOptionPlugin());
compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
compiler.apply(
new CompatibilityPlugin(),
new HarmonyModulesPlugin(options.module),
new AMDPlugin(options.module, options.amd || {}),
new CommonJsPlugin(options.module),
new LoaderPlugin(),
new NodeStuffPlugin(options.node),
new RequireJsStuffPlugin(),
new APIPlugin(),
new ConstPlugin(),
new UseStrictPlugin(),
new RequireIncludePlugin(),
new RequireEnsurePlugin(),
new RequireContextPlugin(options.resolve.modules, options.resolve.extensions, options.resolve.mainFiles),
new ImportPlugin(options.module),
new SystemPlugin(options.module)
);
这里只是部分compiler添加plugin的内容,实际上WebpackOptionsApply还包括了其他的Plugin插入,包括了用户的plugin在此不再罗列。
compiler.apply(new EntryOptionPlugin());
compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
其中最重要的是这一段插入的plugin,这是webpack的options处理plugin,其还有很重要的功能插件SingleEntryPlugin。在EntryOptionPlugin中会为每一个chunk入口增加一个SingleEntryPlugin用于调用compilation的添加入口的函数
之前有说过每一个webpack运行会创建一个compiler对象,compiler实际上一个Tapable的子类,它贯穿了整个webpack打包,控制着webpack的编译开始与资源生产结束 lib/compiler.js
class Compiler extends Tapable {
run(){
//启动打包
},
compile() {
//开始编译
}
......
}
其中比较重要的是compile函数的调用,其实例化了一个Compilation对象。
compile(callback) {
const params = this.newCompilationParams();
this.applyPluginsAsync("before-compile", params, err => {
if(err) return callback(err);
this.applyPlugins("compile", params);
//这里newCompilation实例化了一个Compilation,并抛出了Compilation事件
const compilation = this.newCompilation(params);
this.applyPluginsParallel("make", compilation, err => {
if(err) return callback(err);
compilation.finish();
compilation.seal(err => {
if(err) return callback(err);
this.applyPluginsAsync("after-compile", compilation, err => {
if(err) return callback(err);
return callback(null, compilation);
});
});
});
});
}
newCompilation(params) {
const compilation = this.createCompilation();
compilation.fileTimestamps = this.fileTimestamps;
compilation.contextTimestamps = this.contextTimestamps;
compilation.name = this.name;
compilation.records = this.records;
compilation.compilationDependencies = params.compilationDependencies;
this.applyPlugins("this-compilation", compilation, params);
this.applyPlugins("compilation", compilation, params);
return compilation;
}
Compilation创建是一个很重要的时间节点,大部分与模块处理相关的逻辑都会在这个时候添加到Compilation对象中,很多plugin会这样写
compiler.plugin('this-compilation', (compilation, params) => {
compilation.plugin('additional-assets', (callback) => {
});
compilation.plugin('optimize-chunk-assets', (chunks, callback) => {
});
});
Compilation事件 Compilation也是一个Tapable的子类,这应该是webpack中最核心的一个对象,它控制着webpack处理依赖生成模块,承载了生成的模块与静态资源列表。 在创建 module 之前,Compiler 会触发 make,并调用 Compilation.addEntry 方法(SingleEntryPlugin调用),通过 options 对象的 entry 字段找到我们的入口js文件。之后,在 addEntry 中调用私有方法 _addModuleChain ,这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。
addEntry(context, entry, name, callback) {
const slot = {
name: name,
module: null
};
this.preparedChunks.push(slot);
this._addModuleChain(context, entry, (module) => {
entry.module = module;
this.entries.push(module);
module.issuer = null;
}, (err, module) => {
if(err) {
return callback(err);
}
if(module) {
slot.module = module;
} else {
const idx = this.preparedChunks.indexOf(slot);
this.preparedChunks.splice(idx, 1);
}
return callback(null, module);
});
}
_addModuleChain(context, dependency, onModule, callback) {
const start = this.profile && Date.now();
const moduleFactory = this.dependencyFactories.get(dependency.constructor);
......
this.semaphore.acquire(() => {
moduleFactory.create({
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
}, (err, module) => {
.......
const result = this.addModule(module);
if(!result) {
module = this.getModule(module);
onModule(module);
if(this.profile) {
const afterBuilding = Date.now();
module.profile.building = afterBuilding - afterFactory;
}
this.semaphore.release();
return callback(null, module);
}
.......
onModule(module);
.......
this.buildModule(module, false, null, null, (err) => {
if(err) {
this.semaphore.release();
return errorAndCallback(err);
}
if(this.profile) {
const afterBuilding = Date.now();
module.profile.building = afterBuilding - afterFactory;
}
moduleReady.call(this);
});
function moduleReady() {
this.semaphore.release();
this.processModuleDependencies(module, err => {
if(err) {
return callback(err);
}
return callback(null, module);
});
}
});
});
}
在这里做了两件事
创建模块根据模块的内容调用模块工程创建出该模块moduleFactory.create()。然后进行第二步buildModule,buildModule实际上就是调用了module的build,那这个build做了什么?
遍历ast如果ast中有新的模块引入怎么办,上面moduleReady回调中调用processModuleDependencies开始递归处理依赖的 module,重复之前的操作。更多内容可以查阅webpack中NormalModule.js这个文件,其中可以着重关注与build与doBuild两个函数的内容。这个是所有 module 的 父类,它有几种不同子类:NormalModule , MultiModule , ContextModule等。
而当所有的模块编译处理完毕之后,运行重新交回给compiler,compiler会调用Compilation中的seal方法
seal(callback) {
const self = this;
self.applyPlugins0("seal");
self.nextFreeModuleIndex = 0;
self.nextFreeModuleIndex2 = 0;
self.preparedChunks.forEach(preparedChunk => {
const module = preparedChunk.module;
const chunk = self.addChunk(preparedChunk.name, module);
const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name);
entrypoint.unshiftChunk(chunk);
chunk.addModule(module);
module.addChunk(chunk);
chunk.entryModule = module;
self.assignIndex(module);
self.assignDepth(module);
self.processDependenciesBlockForChunk(module, chunk);
});
self.sortModules(self.modules);
self.applyPlugins0("optimize");
while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) ||
self.applyPluginsBailResult1("optimize-modules", self.modules) ||
self.applyPluginsBailResult1("optimize-modules-advanced", self.modules)) { /* empty */ }
self.applyPlugins1("after-optimize-modules", self.modules);
.....
}
seal阶段会抛出各种事件(可能是抛出最多的一个阶段了),这一阶段主要的功能其实是plugin做的,compilation主要负责流程的控制,而plugin会在流程中对传入的模块做一个优化处理,比如生成额外的资源,比如在生成内容前面加一段标记的js诸如此类的功能,起到整理chunk与module的作用,生成编译后的源码,合并,拆分,生成 hash 。我们自己自定义的plugin很多会在这一阶段处理代码,对plugin开发来说这是一个很关键的节点。
终于该处理完的module和chunk的代码都已经优化完了,该干啥?生成assets,这个过程也是在seal的过程中。
seal(callback) {
......
self.applyPlugins0("before-module-assets");
self.createModuleAssets();
if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) {
self.applyPlugins0("before-chunk-assets");
self.createChunkAssets();
}
.....
}
compilation.createChunkAssets是生产模块静态资源的方法,实际上就是延着chunks树调用module与chunk对应的template方法生成最终的静态js代码(Template 中的 render 方法)。 生成完毕之后运行会重新交还给complier,complier做的事就简单了,触发emmit将assets对象生成真正的文件,当然也可能是webpack-dev-server中的虚拟文件,反正到此的话整个打包就真正的结束。
const onCompiled = (err, compilation) => {
if(err) return callback(err);
if(this.applyPluginsBailResult("should-emit", compilation) === false) {
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.applyPlugins("done", stats);
return callback(null, stats);
}
this.emitAssets(compilation, err => {
if(err) return callback(err);
if(compilation.applyPluginsBailResult("need-additional-pass")) {
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.applyPlugins("done", stats);
this.applyPluginsAsync("additional-pass", err => {
if(err) return callback(err);
this.compile(onCompiled);
});
return;
}
this.emitRecords(err => {
if(err) return callback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.applyPlugins("done", stats);
return callback(null, stats);
});
});
};
complier的打包具体可以看onCompiled回调函数与emitAssets实际资源生产函数。
来个总结吧,webpack本身最主要的核心是Compilation与Module两个,Compilation完成编译过程中的入口添加,依赖处理,打包chunk的静态资源等,Module则是完成具体单个module的处理。主要思想就是tapable的流程控制。
参考:细说 webpack 之流程篇 webpack API