Appearance
1、shell options 与 webpack config的获取与合并
命令行运行webpack的时候,首先会运行到 webpack/bin/webpack.js这个文件。这个文件内使用yargs这个第三方库对shell options以及webpack config进行了格式检查,确认无误后,对这两个参数进行合并。以下是关键代码:
js
// webpack/bin/webpack.js文件内
...
// 向yargs实例添加config检查规则
require("./config-yargs")(yargs);
...
// 向yargs实例添加shell options检查规则
yargs.options({
...
})
...
// NOTE: 重要方法 传入shell options,在这个函数内扫描webpack config文件,获取webpack config对象(函数返回的options对象就是)
// argv为shell options参数
var options = require("./convert-argv")(yargs, argv);
...
convert-argv.js文件内导出了一个函数,该函数内会根据shell options的--config options(没有则加载默认的webpack.config.js)来加载webpack config的配置文件得到webpack config对象。关键代码如下:
js
// webpack/bin/convert-argv.js文件内
...
if(argv.config) {
// argv是shell option对象
// NOTE: 如果shell option中指定了webpack config文件,则获取指定的config文件(webpack --config)
var getConfigExtension = function getConfigExtension(configPath) {
for(i = extensions.length - 1; i >= 0; i--) {
var tmpExt = extensions[i];
if(configPath.indexOf(tmpExt, configPath.length - tmpExt.length) > -1) {
return tmpExt;
}
}
return path.extname(configPath);
};
var mapConfigArg = function mapConfigArg(configArg) {
var resolvedPath = path.resolve(configArg);
var extension = getConfigExtension(resolvedPath);
return {
path: resolvedPath,
ext: extension
};
};
// NOTE: 将webpack --config选项保存成数组
var configArgList = Array.isArray(argv.config) ? argv.config : [argv.config];
// 将配置文件路径保存进configFiles数组
configFiles = configArgList.map(mapConfigArg);
} else {
// shell option中未指定webpack config文件,则获取获取默认的webpack.config.js文件作为配置文件
for(i = 0; i < defaultConfigFiles.length; i++) {
var webpackConfig = defaultConfigFiles[i].path;
if(fs.existsSync(webpackConfig)) {
// 将配置文件路径保存进configFiles数组
configFiles.push({
path: webpackConfig,
ext: defaultConfigFiles[i].ext
});
break;
}
}
}
// NOTE: 经过上面的处理,configFiles中保存了所有webpack config文件的绝对路径,如下所示:
// configFiles = [
// {
// path: '这里保存webpack config文件的绝对路径',
// ext: '这里保存webpack config文件的后缀',
// },
// ...
// ];
if(configFiles.length > 0) {
...
var requireConfig = function requireConfig(configPath) {
var options = require(configPath);
options = prepareOptions(options, argv);
return options;
};
configFiles.forEach(function(file) {
registerCompiler(interpret.extensions[file.ext]);
// 从config files中获取webpack config,并保存进options数组
options.push(requireConfig(file.path));
});
configFileLoaded = true;
}
if(!configFileLoaded) {
return processConfiguredOptions({});
} else if(options.length === 1) {
// processConfiguredOptions函数对webpack config对象进行进一步加工
// 然后返回最终的webpack config对象
return processConfiguredOptions(options[0]);
} else {
return processConfiguredOptions(options);
}
...
经过convert-argv.js文件的运行,最后得出了需要传递给webpack编译的webpack config对象,然后进入下一步。
2、创建compiler对象,在compiler对象上加载必要插件,调用compiler.run方法开始进入编译过程
js
// webpack/bin/webpack.js文件内
// 获取webpack函数,在外部require('webpack')得到就是该函数
var webpack = require("../lib/webpack.js");
...
// 关键方法 传入webpack config 得出一个compiler实例
compiler = webpack(options);
...
// 关键方法,开始编译过程
compiler.run(compilerCallback);
webpack是一个比较关键的函数,这个函数内创建了compiler对象,并且在compiler对象上注册了用户在webpack config上配置好的所有插件。
同时这个文件还在webpack上添加了所有webpack内置的插件,比如我们常用的webpack.optimize.UglifyJsPlugin插件。
js
// webpack/lib/webpack.js文件内
/**
* @description 用户require('webpack')导出的就是该函数,用于开启编译过程
* @param {*} options webpack config
* @param {*} callback 回调函数
* @returns compiler
*/
function webpack(options, callback) {
// 检查webpack config对象
const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
if(webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
let compiler;
if(Array.isArray(options)) {
// NOTE: 如果webpack config最外层是数组形式,则使用MultiCompiler
compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if(typeof options === "object") {
// NOTE: 如果传入的webpack config是一个object(通常传入的都是一个object,所以重点查看此处)
// TODO webpack 4: process returns options
new WebpackOptionsDefaulter().process(options);
// NOTE: 创建Compiler实例
compiler = new Compiler();
compiler.context = options.context;
compiler.options = options;
// 注册NodeEnvironmentPlugin插件,里面向compiler注册了 'before-run' hook
new NodeEnvironmentPlugin().apply(compiler);
if(options.plugins && Array.isArray(options.plugins)) {
// NOTE: 注册webpack config 中用户设置的插件
compiler.apply.apply(compiler, options.plugins);
}
// 触发environment和after-environment hook
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
// NOTE: 关键方法 这个方法将会针对我们传进去的webpack config 进行逐一编译,然后注册许多关键插件
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
if(callback) {
// 如果提供了回调函数,则立即调用compiler.run 进行编译过程
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;
}
exports = module.exports = webpack;
...
function exportPlugins(obj, mappings) {
Object.keys(mappings).forEach(name => {
Object.defineProperty(obj, name, {
configurable: false,
enumerable: true,
get: mappings[name]
});
});
}
// 向webpack函数对象添加内置插件的构造函数
exportPlugins(exports, {
"DefinePlugin": () => require("./DefinePlugin"),
...
});
// 向webpack.optimize 添加内置插件构造函数
exportPlugins(exports.optimize = {}, {
...
"UglifyJsPlugin": () => require("./optimize/UglifyJsPlugin")
});
上面源码中WebpackOptionsApply类的process方法非常重要。这个方法中对compiler注册了很多内置的插件。这些插件都比较关键,比如负责注册make事件钩子的EntryOptionPlugin等。这些插件在后面的构建生命周期中起到关键作用。
WebpackOptionsApply类的process如下:
js
// webpack/lib/WebpackOptionsApply.js文件内
/**
* @description 根据webpack config对象对compiler添加一些必要属性,以及加载必要的插件
* @param {*} options
* @param {*} compiler
* @returns
* @memberof WebpackOptionsApply
*/
process(options, compiler) {
let ExternalsPlugin;
// 给compiler添加必要属性
compiler.outputPath = options.output.path;
compiler.recordsInputPath = options.recordsInputPath || options.recordsPath;
compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath;
compiler.name = options.name;
compiler.dependencies = options.dependencies;
// 处理webpack config target选项
if(typeof options.target === "string") {
let JsonpTemplatePlugin;
let NodeSourcePlugin;
let NodeTargetPlugin;
let NodeTemplatePlugin;
// NOTE: 以下根据不同的target加载不同的插件
switch(options.target) {
case "web":
// 针对前端打包环境加载插件
JsonpTemplatePlugin = require("./JsonpTemplatePlugin");
NodeSourcePlugin = require("./node/NodeSourcePlugin");
compiler.apply(
new JsonpTemplatePlugin(options.output),
new FunctionModulePlugin(options.output),
new NodeSourcePlugin(options.node),
new LoaderTargetPlugin(options.target)
);
break;
case "webworker":
...
case "node":
case "async-node":
...
break;
case "node-webkit":
...
break;
case "atom":
case "electron":
case "electron-main":
...
break;
case "electron-renderer":
...
break;
default:
throw new Error("Unsupported target '" + options.target + "'.");
}
} else if(options.target !== false) {
options.target(compiler);
} else {
throw new Error("Unsupported target '" + options.target + "'.");
}
if(options.output.library || options.output.libraryTarget !== "var") {
// 针对output.library选项加载插件
...
}
if(options.externals) {
// 处针对externals选项加载插件
...
}
let noSources;
let legacy;
let modern;
let comment;
// 处理devtool选项
if(options.devtool && (options.devtool.indexOf("sourcemap") >= 0 || options.devtool.indexOf("source-map") >= 0)) {
...
} else if(options.devtool && options.devtool.indexOf("eval") >= 0) {
...
}
// NOTE: 重要插件:在EntryOptionPlugin插件内加载了 DynamicEntryPlugin 插件,进而注册了make事件回调钩子
// 在make事件回调钩子中,webpack会调用compiltion.addEntry()方法,开始加载和build模块
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 || {}),
// 为编译提供commonjs规范支持的插件
new CommonJsPlugin(options.module),
// loader插件,该插件在 compilation 上注册normal-module-loader事件,并在该回调钩子内调用loader
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)
);
// 加载chunk处理相关插件
compiler.apply(
new EnsureChunkConditionsPlugin(),
new RemoveParentModulesPlugin(),
new RemoveEmptyChunksPlugin(),
new MergeDuplicateChunksPlugin(),
new FlagIncludedChunksPlugin(),
new OccurrenceOrderPlugin(true),
new FlagDependencyExportsPlugin(),
new FlagDependencyUsagePlugin()
);
if(options.performance) {
compiler.apply(new SizeLimitsPlugin(options.performance));
}
compiler.apply(new TemplatedPathPlugin());
compiler.apply(new RecordIdsPlugin());
compiler.apply(new WarnCaseSensitiveModulesPlugin());
if(options.cache) {
...
// 处理cache选项
}
compiler.applyPlugins("after-plugins", compiler);
...
compiler.applyPlugins("after-resolvers", compiler);
return options;
}
上述process方法中加载很多关键的插件,这里关注其中的EntryOptionPlugin。
TIP
EntryOptionPlugin插件中又根据是否存在多个entry file,注册了MultiEntryPlugin或者SingleEntryPlugin,这两个插件会在compiler中注册make回调钩子,并在这个回调钩子中,调用compilation.addEntry方法开从入口文件解析并加载module。
以下是EntryOptionPlugin内的关键代码:
js
// webpack/lib/EntryOptionPlugin.js文件内
function itemToPlugin(context, item, name) {
if(Array.isArray(item)) {
// NOTE: entry为数组时,多入口文件
return new MultiEntryPlugin(context, item, name);
}
// NOTE: entry不为数组,单入口文件
return new SingleEntryPlugin(context, item, name);
}
module.exports = class EntryOptionPlugin {
apply(compiler) {
// 注册entry-option事件回调
compiler.plugin("entry-option", (context, entry) => {
// 在这里加载 SingleEntryPlugin 或者 MultiEntryPlugin 插件
if(typeof entry === "string" || Array.isArray(entry)) {
compiler.apply(itemToPlugin(context, entry, "main"));
} else if(typeof entry === "object") {
Object.keys(entry).forEach(name => compiler.apply(itemToPlugin(context, entry[name], name)));
} else if(typeof entry === "function") {
// 如果entry字段是一个函数,则加载 DynamicEntryPlugin 插件
compiler.apply(new DynamicEntryPlugin(context, entry));
}
return true;
});
}
};
EntryOptionPlugin内根据webpack config对象的entry选项是否为数组来加载MultiEntryPlugin或者SingleEntryPlugin
- 如果是entry是函数则加载DynamicEntryPlugin。
- 这三个插件都在compiler对象上注册了make事件钩子,并在该回调事件内调用了compilation.addEntry方法,这个方法是webpack加载入口文件并且递归解析加载依赖module的开始。
下面我们以SingleEntryPlugin为例:
js
// webpack/lib/SingleEntryPlugin.js文件内
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context;
this.entry = entry;
this.name = name;
}
apply(compiler) {
compiler.plugin("compilation", (compilation, params) => {
const normalModuleFactory = params.normalModuleFactory;
compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory);
});
// NOTE: 注册make事件钩子,这个事件会在 compiler.compile方法内触发。
compiler.plugin("make", (compilation, callback) => {
const dep = SingleEntryPlugin.createDependency(this.entry, this.name);
// NOTE: 关键方法:调用 compilation.addEntry 方法,从这个方法开始加载入口文件,并且递归解析加载依赖module
compilation.addEntry(this.context, dep, this.name, callback);
});
}
static createDependency(entry, name) {
const dep = new SingleEntryDependency(entry);
dep.loc = name;
return dep;
}
}
现在从深入WebpackOptionsApply类的process方法回到webpack方法中。
再来聊一下Compiler这个对象以及Compiler.run方法。
3、调用Compiler.run,创建Compilation对象,然后出发关键的make生命周期事件,将具体的编译事项交给Compilation对象。
Compiler对象是webpack编译过程中非常重要的一个对象。
它代表的是配置完备的Webpack环境。
compiler对象只在Webpack启动时构建一次,由Webpack组合所有的配置项构建生成。
Compiler 继承自Tapable类,借助继承的Tapable类,Compiler具备有被注册监听事件,以及发射事件触发hook的功能,webpack的插件机制也由此形成。
大多数面向用户的插件,都是首先在 Compiler 上注册的。
插件注册,可见的一些细节章节的插件注册。这里不再赘述。
Compiler对象的介绍,也可见的webpack重要对象的Compiler类。这里不再赘述。
Tapable类的介绍,可见下面的webpack重要对象的Tapable类。
webpack的编译构建流程,由compiler.run()开始进入
js
// webpack/lib/Complier.js文件内
/**
* @description 由该方法开始进入webpack编译流程
* @param {*} callback 编译完成后回调函数
* @memberof Compiler
* @file webpack/lin/Complier.js
*/
run(callback) {
const startTime = Date.now();
// 定义compile后的回调函数
const onCompiled = (err, compilation) => {
// NOTE: 此时已经完成了所有modules的构建,以及chunk和asset的生成
...
return callback();
};
// 触发before-run事件(compiler生命周期事件)
// 具体参考:https://webpack.js.org/api/compiler-hooks/
this.applyPluginsAsync("before-run", this, err => {
if(err) return callback(err);
// 触发run事件(compiler生命周期事件)
this.applyPluginsAsync("run", this, err => {
if(err) return callback(err);
this.readRecords(err => {
if(err) return callback(err);
// NOTE: 关键方法:在此键入具体的编译构建流程
this.compile(onCompiled);
});
});
});
}
Compiler.compile方法内会创建出compilation对象,并且将具体的编译事项交给compilation。
/**
* @description NOTE: 从这个方法开始具体的编译构建流程
* 在compiler触发run事件后调用本方法。watch模式下每次文件发生改变
* 都是通过调用本方法重启一次编译过程
* @param {*} callback
* @memberof Compiler
*/
compile(callback) {
const params = this.newCompilationParams();
// 触发 before-compile 生命周期
this.applyPluginsAsync("before-compile", params, err => {
if(err) return callback(err);
// NOTE: 触发compile 生命周期,Compiler进入编译构建阶段
this.applyPlugins("compile", params);
// NOTE: 创建关键的compilation实例
const compilation = this.newCompilation(params);
// NOTE: 触发make生命周期,然后将具体的编译构建流程交给compilation对象处理
this.applyPluginsParallel("make", compilation, err => {
if(err) return callback(err);
compilation.finish();
// NOTE: 关键方法:在seal方法内对编译完的chunk封装,并最后输出为bundle
compilation.seal(err => {
if(err) return callback(err);
// NOTE: compile生命周期 完成所有模块构建,结束编译过程
this.applyPluginsAsync("after-compile", compilation, err => {
if(err) return callback(err);
return callback(null, compilation);
});
});
});
});
}
4、在make生命周期事件中,调用Compilation.addEntry方法,从entry file开始,递归的解析和build module.
在上述第二步中已经说过,EntryOptionPlugin插件内会根据entry的类型注册不同的EntryPlugin。
在EntryPlugin中最重要的是在compiler上注册make事件处理函数。在该处理函数中会调用compilation.addEntry方法。 以下是SingleEntryPlugin插件的apply方法示例:
js
// webpack/lib/SingleEntryPlugin.js文件内
apply(compiler) {
compiler.plugin("compilation", (compilation, params) => {
const normalModuleFactory = params.normalModuleFactory;
compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory);
});
// NOTE: 注册make事件钩子,这个事件会在 compiler.compile方法内触发。
compiler.plugin("make", (compilation, callback) => {
const dep = SingleEntryPlugin.createDependency(this.entry, this.name);
// NOTE: 关键方法:调用 compilation.addEntry 方法,从这个方法开始加载入口文件,并且递归解析加载依赖module
compilation.addEntry(this.context, dep, this.name, callback);
});
}
从compilation.addEntry方法开始加载入口文件,并且递归解析加载依赖module
js
/**
* @description 从入口文件开始进行module的递归解析
* @param {*} context
* @param {string} entry
* @param {*} name
* @param {function} callback 该callback定义在Compiler.compile方法内,是make事件触发后的回调
* @memberof Compilation
*/
addEntry(context, entry, name, callback) {
const slot = {
name: name,
module: null
};
this.preparedChunks.push(slot);
// NOTE: _addModuleChain先调用buildModule()构建entry module,
// 然后调用 processModuleDependencies()从entry module开始对其依赖的module进行 递归的编译。
this._addModuleChain(context, entry, (module) => {
// 得到入口文件对应的module对象并保存到compilation对象
// 此时module对象还未进行构建
entry.module = module;
this.entries.push(module);
module.issuer = null;
}, (err, module) => {
// 参数 module 为entry module对象
// NOTE: 此时所有module已经构建处理完毕
if(err) {
return callback(err);
}
if(module) {
slot.module = module;
} else {
const idx = this.preparedChunks.indexOf(slot);
this.preparedChunks.splice(idx, 1);
}
// NOTE: 调用callback
// 该callback定义在Compiler.compile方法内,是make事件触发后的回调
return callback(null, module);
});
}
_addModuleChain先调用buildModule()构建entry module
然后调用 processModuleDependencies()从entry module开始对其依赖的module进行 递归的编译。
其中所有module的编译都是通过compilation.buildModule方法来进行的
而buildModule方法内,又是通过调用module.build方法,来具体的完成module对象的编译过程。
js
buildModule(module, optional, origin, dependencies, thisCallback) {
// NOTE: 触发build-module事件,将当前module对象作为参数 Before a module build has started.
this.applyPlugins1("build-module", module);
if(module.building) return module.building.push(thisCallback);
const building = module.building = [thisCallback];
function callback(err) {
module.building = undefined;
building.forEach(cb => cb(err));
}
// NOTE: 调用module.build方法开始编译module
module.build(this.options, this, this.resolvers.normal, this.inputFileSystem, (error) => {
// module编译后的回调
const errors = module.errors;
for(let indexError = 0; indexError < errors.length; indexError++) {
const err = errors[indexError];
err.origin = origin;
err.dependencies = dependencies;
if(optional)
this.warnings.push(err);
else
this.errors.push(err);
}
const warnings = module.warnings;
for(let indexWarning = 0; indexWarning < warnings.length; indexWarning++) {
const war = warnings[indexWarning];
war.origin = origin;
war.dependencies = dependencies;
this.warnings.push(war);
}
module.dependencies.sort(Dependency.compare);
if(error) {
this.applyPlugins2("failed-module", module, error);
return callback(error);
}
// NOTE: 成功构建当前module
this.applyPlugins1("succeed-module", module);
return callback();
});
}
module对象是webpack组织源码文件进行编译的核心对象
源码内符合webpack module规范的文件都会被封装成一个module对象。
Module类只是一个抽象类,他有很多具体的实现子类,每一个子类都定义了各自的build方法。
其中loader的调用就是在module.build中完成调用的。 module.build的运行大概过程如下图所示: @import "./build module.png";
5、封装module,生成chunk对象,然后根据chunk对象生成最终的assets对象,进而生成静态资源文件
经过第四步,所有module对象都被递归的调用了module.build方法
每一个module都经过了loader(如果配置了的话)的处理,此时的module一定是一个js module,能被浏览器运行。
此时调用compilation.seal方法,开始对module对象进行封装。
Compilation.seal方法是封装module的入口方法
js
// webpack/lib/Compilation.js 内
seal(callback) {
// NOTE: The sealing of the compilation has started.此时所有module已经经过loader处理,并且被解析为ast树
this.applyPlugins0("seal");
this.nextFreeModuleIndex = 0;
this.nextFreeModuleIndex2 = 0;
// preparedChunks 内存储着entry chunk; 处理entry chunk
this.preparedChunks.forEach(preparedChunk => {
const module = preparedChunk.module;
// NOTE: 生成一个chunk对象并且保存在compilation.chunks数组中;每个chunk对应一个输出文件。
const chunk = this.addChunk(preparedChunk.name, module);
const entrypoint = this.entrypoints[chunk.name] = new Entrypoint(chunk.name);
entrypoint.unshiftChunk(chunk);
// NOTE: 整理每个Module和chunk
chunk.addModule(module);
module.addChunk(chunk);
chunk.entryModule = module;
this.assignIndex(module);
this.assignDepth(module);
});
// NOTE: 为所有的chunk和module相互添加联系; 当前chunks数组只有entry chunk; 代码分割产生的chunk在这个方法内生成
this.processDependenciesBlocksForChunks(this.chunks.slice());
this.sortModules(this.modules);
// NOTE: 以下开始进入optimization阶段
// webpack is begining the optimization phase
this.applyPlugins0("optimize");
...
// NOTE: 根据chunk生成assets
this.createChunkAssets();
...
}
processDependenciesBlocksForChunks方法从entry chunk开始生成所有的 chunk(每个代码分割点生成一个chunk)。
所有的chunk生成之后,触发一系列optimize(优化)相关的声明周期事 件,很多插件就是在这里对module、chunk进行优化操作(比如著名的 commonsChunkPlugins)。
所有的chunk对象都生成后,由createChunkAssets方法对每一个chunk对象进行处理,借助mainTemplate或者chunkTemplat的render方法来生成最终需要输出到文件内的代码。
js
// webpack/lib/Compilation.js
// 根据chunk生成asset对象
createChunkAssets() {
const outputOptions = this.outputOptions;
const filename = outputOptions.filename;
const chunkFilename = outputOptions.chunkFilename;
// NOTE: 逐一处理所有chunk,每个chunk都会生成一个assets
for(let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
chunk.files = [];
const chunkHash = chunk.hash;
let source;
let file;
// chunk输出文件名模板
const filenameTemplate = chunk.filenameTemplate ? chunk.filenameTemplate :
chunk.isInitial() ? filename :
chunkFilename;
try {
const useChunkHash = !chunk.hasRuntime() || (this.mainTemplate.useChunkHash && this.mainTemplate.useChunkHash(chunk));
const usedHash = useChunkHash ? chunkHash : this.fullHash;
const cacheName = "c" + chunk.id;
if(this.cache && this.cache[cacheName] && this.cache[cacheName].hash === usedHash) {
source = this.cache[cacheName].source;
} else {
// NOTE: source.children 属性是个数组,数组内每一个元素都是即将输出到文件的一行代码文本字符串
if(chunk.hasRuntime()) {
// entry chunk处理
// NOTE: mainTemplate 生成的source包含了webpackBootstrap启动代码
source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);
} else {
// non entry chunk处理
// chunkTemplate打印的是普通的异步chunk
source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);
}
// 把当前source对象缓存进this.cache对象
if(this.cache) {
this.cache[cacheName] = {
hash: usedHash,
source: source = (source instanceof CachedSource ? source : new CachedSource(source))
};
}
}
// 根据filenameTemplate获取输出的文件名
file = this.getPath(filenameTemplate, {
noChunkHash: !useChunkHash,
chunk
});
if(this.assets[file])
throw new Error(`Conflict: Multiple assets emit to the same filename ${file}`);
// NOTE: 关键步骤,将source对象保存在compilation.assets,source.children属性保存了即将输出到assets中的每一行代码
this.assets[file] = source;
chunk.files.push(file);
//
this.applyPlugins2("chunk-asset", chunk, file);
} catch(err) {
this.errors.push(new ChunkRenderError(chunk, file || filenameTemplate, err));
}
}
}
上面生成source对像就是asset对象,器内部保存了最终需要输出到文件的js代码字符串。
最后在compiler.emitAssets方法中,会将这些asset对象输出到文件系统形成静态文件
js
// NOTE: 根据assets生成静态文件
emitAssets(compilation, callback) {
let outputPath;
// 生成静态文件函数
const emitFiles = (err) => {
if(err) return callback(err);
// NOTE: 遍历每一个assets对象,生成对应文件
require("async").forEach(Object.keys(compilation.assets), (file, callback) => {
let targetFile = file;
const queryStringIdx = targetFile.indexOf("?");
if(queryStringIdx >= 0) {
targetFile = targetFile.substr(0, queryStringIdx);
}
// 定义输出函数
const writeOut = (err) => {
if(err) return callback(err);
const targetPath = this.outputFileSystem.join(outputPath, targetFile);
const source = compilation.assets[file];
if(source.existsAt === targetPath) {
// 如果需要输出的文件已经存在,则不再生成
source.emitted = false;
return callback();
}
// NOTE: 从source对象获取到需要输出的代码
let content = source.source();
if(!Buffer.isBuffer(content)) {
content = new Buffer(content, "utf8"); // eslint-disable-line
}
source.existsAt = targetPath;
source.emitted = true;
// NOTE: 将buffer流输出到文件
this.outputFileSystem.writeFile(targetPath, content, callback);
};
if(targetFile.match(/\/|\\/)) {
const dir = path.dirname(targetFile);
this.outputFileSystem.mkdirp(this.outputFileSystem.join(outputPath, dir), writeOut);
} else writeOut();
}, err => {
if(err) return callback(err);
afterEmit.call(this);
});
};
// NOTE: 重要生命周期,即将开始根据assets生成静态文件,生成静态文件前修改assets的最后机会
this.applyPluginsAsync("emit", compilation, err => {
if(err) return callback(err);
// 获取静态文件输出路径
outputPath = compilation.getPath(this.outputPath);
// 创建静态文件输出路径文件夹,然后开始生成静态文件
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
function afterEmit() {
this.applyPluginsAsyncSeries1("after-emit", compilation, err => {
if(err) return callback(err);
return callback();
});
}
}
至此,webpack的编译过程基本完结,源码文件经过 module -> chunk -> asset 一系列的转换,最终输出到文件系统成为静态资源文件。