版本为webpack@4.44.1
一个简单的示例代码
// webpack.config配置文件
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: './src/webpack/index.js',
output: {
path: path.resolve(__dirname +'/src/webpack/', 'dist'),
filename: 'webpack-bundle.js'
},
mode: 'development',
plugins: [
new HtmlWebpackPlugin({template: './src/webpack/index.html'})
]
}
// 入口文件index
import {getA} from './a.js';
let name = 'haha';
function init() {
getA();
}
init();
// a.js
import B from './b.js';
let a = 'haha'
export function getA() {
B();
console.log(a);
return a;
}
// b.js
function B () {
console.log('i am a B')
}
export default B;
webpack下debug断点调试命令node --inspect --inspect-brk ./node_modules/webpack-cli/bin/cli.js --config webpack.config1.js
在webpack-cli下bin/cli.js 文件中找到const webpack = require("webpack");
webpack编译的流程图大体如下
查看入口文件可知, 在进行webpack调用时会new一个 Complier作为返回的对象; 调用run方法时,实现编译打包的过程。
在run的过程中创建Compilation对象进行文件的分析、依赖的收集、chunks以及优化等工作。
Complier
Complier的主要方法
- run方法, 执行一些hooks方法,然后进入complie编译阶段, 设置编译完成后的回调函数
onCompiled
; - this.compile方法, 执行一些hooks方法,
a. 创建Compilation对象,执行hooks.make.callAsync方法进入到buildModule过程.
b. 执行compilation.seal方法进入的模块的封装过程。
c. 执行compile完成后的回调, 执行onCompiled方法。 - onCompiled方法中主要是执行emitAssets, emitRecords以及hooks方法。
- 获取source内容输出最终的打包文件。
new webpack
- new一个compiler对象
- 通过NodeEnvironmentPlugin.apply方法在compiler对象上绑定用于文件输入输出的对象。
- 循环执行绑定的plugn的初始化方法。
const webpack = (options, callback) => {
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
let compiler;
if (Array.isArray(options)) {
compiler = new MultiCompiler(
Array.from(options).map(options => webpack(options))
);
} else if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (options.plugins && 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);
} else {
throw new Error("Invalid argument: options");
}
if (callback) {
...
}
return compiler;
};
NodeEnvironmentPlugin主要是用于创建文件输入输出对象, 其结构流程如下
- 创建一个cacheInputFiles的对象, 可以对文件进行缓存的处理。
- 创建一个outputFiles对象, 将原生的fs上的 方法进行绑定
run
通过此方法执行hooks钩子函数, 调用compile方法进入编译阶段。 同时该方法上设置onCompiled 编译完成后的回调函数。
编译完成后调用this.emitAssets实现文件的输出。
run(callback) {
if (this.running) return callback(new ConcurrentCompilationError());
const finalCallback = (err, stats) => {
...
};
const startTime = Date.now();
this.running = true;
const onCompiled = (err, compilation) => {
if (err) return finalCallback(err);
if (this.hooks.shouldEmit.call(compilation) === false) {
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
this.emitAssets(compilation, err => {
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
return;
}
this.emitRecords(err => {
if (err) return finalCallback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
});
});
};
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
}
this.emitAssets
由代码可知,该方法的主要功能是将source内容写入到输出文件中, 最终实现打包输出。
emitAssets(compilation, callback) {
let outputPath;
const emitFiles = err => {
if (err) return callback(err);
asyncLib.forEachLimit(
compilation.getAssets(),
15,
({ name: file, source }, 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
);
// TODO webpack 5 remove futureEmitAssets option and make it on by default
if (this.options.output.futureEmitAssets) {
...
} else {
if (source.existsAt === targetPath) {
source.emitted = false;
return callback();
}
let content = source.source();
if (!Buffer.isBuffer(content)) {
content = Buffer.from(content, "utf8");
}
source.existsAt = targetPath;
source.emitted = true;
this.outputFileSystem.writeFile(targetPath, content, err => {
if (err) return callback(err);
this.hooks.assetEmitted.callAsync(file, content, callback);
});
}
};
if (targetFile.match(/\/|\\/)) {
const dir = path.dirname(targetFile);
this.outputFileSystem.mkdirp(
this.outputFileSystem.join(outputPath, dir),
writeOut
);
} else {
writeOut();
}
},
err => {
...
}
);
};
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
}
Compilation
Compilation主要有两个过程, 一是创建module进行依赖收集,另一个是seal过程。
- 首先是从入口文件调用addEntry、_addModuleChain递归的创建module。
- 通过processModuleDependencies和addModuleDependencies的相互调用, 从而实现循环buildModule和依赖收集的工作。
- module.build类似工厂模式, 不同的module分别调用其自身的build方法。在build的过程中会执行相关的loader插件, 进行parse解析生成AST树。
- seal过程中进行asset的创建收集, chunks的创建分组、创建hash以及代码的优化工作。
_addModuleChain
该方法主要功能点如下
- 创建一个模块工厂函数, 调用create方法生成module。
- 对当前的模块进行build的过程, build完成后会有其依赖的文件的相关信息。
- 对于有依赖文件的执行
this.processModuleDependencies
的调用。
_addModuleChain(context, dependency, onModule, callback) {
...
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
if (!moduleFactory) {
throw new Error(
`No dependency factory available for this dependency type: ${dependency.constructor.name}`
);
}
this.semaphore.acquire(() => {
moduleFactory.create(
{
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
},
(err, module) => {
...
const addModuleResult = this.addModule(module);
module = addModuleResult.module;
onModule(module);
dependency.module = module;
module.addReason(null, dependency);
const afterBuild = () => {
if (addModuleResult.dependencies) {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
} else {
return callback(null, module);
}
};
...
if (addModuleResult.build) {
this.buildModule(module, false, null, null, err => {
if (err) {
this.semaphore.release();
return errorAndCallback(err);
}
if (currentProfile) {
const afterBuilding = Date.now();
currentProfile.building = afterBuilding - afterFactory;
}
this.semaphore.release();
afterBuild();
});
} else {
this.semaphore.release();
this.waitForBuildingFinished(module, afterBuild);
}
}
);
});
}
processModuleDependencies
processModuleDependencies 主要是处理依赖信息以及接下来调用this.addModuleDependencies
processModuleDependencies(module, callback) {
const dependencies = new Map();
const addDependency = dep => {
const resourceIdent = dep.getResourceIdentifier();
if (resourceIdent) {
const factory = this.dependencyFactories.get(dep.constructor);
if (factory === undefined) {
throw new Error(
`No module factory available for dependency type: ${dep.constructor.name}`
);
}
let innerMap = dependencies.get(factory);
if (innerMap === undefined) {
dependencies.set(factory, (innerMap = new Map()));
}
let list = innerMap.get(resourceIdent);
if (list === undefined) innerMap.set(resourceIdent, (list = []));
list.push(dep);
}
};
const addDependenciesBlock = block => {
if (block.dependencies) {
iterationOfArrayCallback(block.dependencies, addDependency);
}
if (block.blocks) {
iterationOfArrayCallback(block.blocks, addDependenciesBlock);
}
if (block.variables) {
iterationBlockVariable(block.variables, addDependency);
}
};
try {
addDependenciesBlock(module);
} catch (e) {
callback(e);
}
const sortedDependencies = [];
for (const pair1 of dependencies) {
for (const pair2 of pair1[1]) {
sortedDependencies.push({
factory: pair1[0],
dependencies: pair2[1]
});
}
}
this.addModuleDependencies(
module,
sortedDependencies,
this.bail,
null,
true,
callback
);
}
addModuleDependencies
addModuleDependencies方法循环遍历依赖模块, 对每个模块进行build。 如果build后的模块含有dependencies会继续进入processModuleDependencies
方法(注意, 这也是实现依赖模块递归build和收集的关键。)
addModuleDependencies(
module,
dependencies,
bail,
cacheGroup,
recursive,
callback
) {
const start = this.profile && Date.now();
const currentProfile = this.profile && {};
asyncLib.forEach(
dependencies,
(item, callback) => {
const dependencies = item.dependencies;
...
const semaphore = this.semaphore;
semaphore.acquire(() => {
const factory = item.factory;
factory.create(
{
...
},
(err, dependentModule) => {
let afterFactory;
...
const iterationDependencies = depend => {
for (let index = 0; index < depend.length; index++) {
const dep = depend[index];
dep.module = dependentModule;
dependentModule.addReason(module, dep);
}
};
const addModuleResult = this.addModule(
dependentModule,
cacheGroup
);
dependentModule = addModuleResult.module;
iterationDependencies(dependencies);
const afterBuild = () => {
if (recursive && addModuleResult.dependencies) {
this.processModuleDependencies(dependentModule, callback);
} else {
return callback();
}
};
...
if (addModuleResult.build) {
this.buildModule(
dependentModule,
isOptional(),
module,
dependencies,
err => {
if (err) {
semaphore.release();
return errorOrWarningAndCallback(err);
}
if (currentProfile) {
const afterBuilding = Date.now();
currentProfile.building = afterBuilding - afterFactory;
}
semaphore.release();
afterBuild();
}
);
} else {
semaphore.release();
this.waitForBuildingFinished(dependentModule, afterBuild);
}
}
);
});
},
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) {
// eslint-disable-next-line no-self-assign
err.stack = err.stack;
return callback(err);
}
return process.nextTick(callback);
}
);
}
buildModule
以NormalModule为例, 我们可以先看一下其流程大体如下图所示
- 由图可知, NormalModule继承至module, 而module又继承至DependenciesBlock。 故而NormalModule存在addDependency方法,用来实现依赖的收集。
*build方法首先会对文件进行loaders插件处理, 执行loader插件实现source的变更。 然后进入parser阶段 - parser用来进行解析source将其生成ast树, 遍历ast的节点实现依赖收集的工作。
注意
多个parsePlugin都会涉及到module.addDependency的调用。
createChunkAssets
createChunkAssets阶段对module中代码进行变更替换,进行chunk分组。
其大致流程如下
- 首先会通过template.getRenderMainfest生成render方法, 然后调用fileManifest.render实现代码的生成。
- ModuleTemplate中的render的方法,生成module的代码片段组,然后进行拼装生成代码块。 如开始的示例中,a.js文件会生成
import B from './b.js';
let a = 'haha'
export function getA() {
B();
console.log(a);
return a;
}
// 编译后生成的代码结构,为了查看方便注释部分有删减
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getA", function() { return getA; });
/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b.js */ "./src/webpack/b.js");
let a = 'haha'
function getA() {
Object(_b_js__WEBPACK_IMPORTED_MODULE_0__["default"])();
console.log(a);
return a;
}
- JavascriptGenerator是一个js的代码生成器, 在语法树解析的阶段会对文件的依赖进行收集, 文件中需要插入或者替换的部分也进行收集。然后再generate阶段生成各种代码片段。
- createChunkAssets 是一个进行代码片段收集以及封装的过程。