从超简单demo入手了解webpack打包原理
本文从一个简单demo来探究文件到输出bundle 的过程;来了解webpack4.46.0打包主流程;
首先看我们的demo:
// 入口 index.js
import { add } from "./add.js";
function test() {
const tmp = "something";
return tmp + add;
}
const r = test();
// add.js
export const add = (a, b) => a + b;
打包配置:
const path = require("path");
module.exports = {
entry: {
main: "./src/index.js",
},
output: {
path: path.join(__dirname, "./dist"),
filename: "index.js",
},
mode: "none",
};
再来看我们的打包输出(简化了下,只保留了大概结构):
(function (modules) {
// webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
xxxxx...
}
__webpack_require__.xxxx = ...
__webpack_require__.xxxx = ...
....
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__((__webpack_require__.s = 0));
})(
[
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _add_js__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(1);
function test() {
const tmp = "something";
return tmp + _add_js__WEBPACK_IMPORTED_MODULE_0__["add"];
}
const r = test();
},
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(
__webpack_exports__,
"add",
function () {
return add;
}
);
const add = (a, b) => a + b;
},
]
);
打包输出的文件包含了webpack的运行时代码,执行的入口(webpack_require((webpack_require.s = 0)),打包后的module;
我们主要看一下这之间经历了什么,以及module中的代码是如何替换的,例如
import {add} from './add.js' => var add_js_WEBPACK_IMPORTED_MODULE_0_ = _webpack_require_(1);
add => add_js_WEBPACK_IMPORTED_MODULE_0_['add'];
webpack 启动阶段:
主要是将配置的options与默认的options合并;然后根据option中的字段注册相应的plugin;
启动代码在这里: node_modules/webpack/bin/webpack.js; node_modules/webpack-cli/bin/cli.js;
根据option注册插件在这里: node_modules/webpack/lib/WebpackOptionsApply.js;
这里注册了很多插件,在complier的不同阶段会触发它们订阅的回调;
最后通过webpack-cli中调用complier.run开始构建;
compiler.run((err, stats) => {
if (compiler.close) {
compiler.close(err2 => {
compilerCallback(err || err2, stats);
});
} else {
compilerCallback(err, stats);
}
});
先后触发complier的beforeRun,run,beforeCompile,compile,compilation,thisCompilation 生成comliation;进入make阶段:
make阶段:
complier的make事件被SingleEntryPlugin注册,它会将入口文件(我们配置中写的‘./src/index.js’),加入到构建中,开始模块解析和构建过程;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
addEntry 中我们会触发complilation的addEntry事件; addEntry 里继续调用complation的_addModuleChain方法,在这里我们获取moduleFactory ;开始module create; 因为dep是SingleEntryDependancy的实例,根据SingleEntry 在 compliation事件中 的注册;
compilation.dependencyFactories.set(SingleEntryDependency,normalModuleFactory);
所以我们获取的moduleFactory为normalModuleFactory, 在 normalModuleFactory.create中会依次触发beforeResolve,factory,resolver,afterResolve, 这个过程解析出文件的路径和loader的路径;
继续触发createModule和module事件,创建module:
createdModule = new NormalModule(result);
// result
context: context, // prcess.pwd();
request: loaders
.map(loaderToIdent)
.concat([resource])
.join("!"),
dependencies: data.dependencies, // SingleEntryDepedancy
userRequest, // /Users/userName/Documents/exercise/webpack-source-code/src/index.js
rawRequest: request, // ./src/index.js
loaders,
resource, // /Users/userName/Documents/exercise/webpack-source-code/src/index.js
matchResource,
resourceResolveData,
settings,
type, // 'javascript/auto'
parser: this.getParser(type, settings.parser), //node_modules/webpack/lib/JavascriptModulesPlugin.js 里注册了parser, generator
generator: this.getGenerator(type, settings.generator),
resolveOptions
回到compilation,将module加入到compilation 的 modules 里;然后module与singleEntryModule 建立起关系:
dependency.module = module;
module.addReason(null, dependency);
到这里只是解析了入口模块的路径并将它加到了compilation的modules中;下面进入module.build 过程,该过程触发buildModule, failedModule/succeedModule 事件
下面这段逻辑执行后,module的_source等字段才填充上;
buildModule(){
...
module.build();
}
// NormalModule.js
build () {
...
this._source = null;
this.buildMeta = {};
this.buildInfo = {};
...
return this.doBuild(...someParams..., () => {
...
this.parser.parse(this._ast || this._source.source(), {curent: this, module: this, ...}, () => {...})
...
});
}
doBuild () {
const loaderContext = this.createLoaderContext(...);
runLoaders({resource: this.resource, loaders: this.loaders, context: loaderContext,readSource: fs.readFile.bind(fs)}, (err, result) => {
if (result) {
this.buildInfo.xx = result.xx;
...
}
...
const source = result.result[0];
...
this._source = this.createSource(this.binary ? asBuffer(source) : asString(source),...);
...
this._ast = ...someCode..
})
}
在this.parser.parse 里 会对资源进行解析,为 module添加依赖; 具体逻辑在node_modules/webpack/lib/Parser.js 中;
最终对于index.js, 会给它添加五个依赖(这些依赖的originModule是a.js, module会在解析完添加);
- HarmonyCompatibilityDependency HarmonyDetectionParserPlugin 添加,因为顶层语句中有ImportDeclaration;
- HarmonyInitDependency 理由同上
const initDep = new HarmonyInitDependency(module);
initDep.loc = {
start: {
line: -1,
column: 0
},
end: {
line: -1,
column: 0
},
index: -2
};
module.addDependency(initDep);
- ConstDependency
- HarmonyImportSideEffectDependency 上面两个添加是遇到import 语句;
const sideEffectDep = new HarmonyImportSideEffectDependency(
source,
parser.state.module,
parser.state.lastHarmonyImportOrder,
parser.state.harmonyParserScope
);
sideEffectDep.loc = statement.loc; //{start: {line: 1, column: 0}, end: {line: 1, column: 31}
parser.state.module.addDependency(sideEffectDep);
- HarmonyImportSpecifierDependency
const dep = new HarmonyImportSpecifierDependency(
settings.source,
parser.state.module,
settings.sourceOrder,
parser.state.harmonyParserScope,
settings.id,
name,
expr.range,
this.strictExportPresence
);
dep.shorthand = parser.scope.inShorthand;
dep.directImport = true;
dep.loc = expr.loc; // {start: {line: 5, column: 15}, end: {line: 5, column: 18}}
parser.state.module.addDependency(dep);
这里的逻辑是这样的:
首先在遇到importSpecifier节点的时候,会把当前作用域(当前为顶层作用域)的变量引用删除掉,然后设置它的别名为‘import var';
parser.scope.definitions.delete(name);
parser.scope.renames.set(name, "imported var");
在遍历其它表达式时,遇到rename为import var 的时候,添加HarmonyImportSpecifierDependency依赖;
这里顺便说下parser的prewalkStatements;walkStatements;
一个是扫描当前作用域的变量,做变量收集和一些预处理,一个做最终处理;
这样就得到了index.js模块的依赖,webpack会根据它们在源码中应用的位置进行下排序,后调用callback进入到afterBuild 函数, 有依赖,执行compilation.processModuleDependencies中:
这里面主要干了两件事:
-
先对依赖根据factory分组,然后组内根据资源id进行分组;此处会过滤掉没有资源id的依赖;
-
对上一步继续处理,解构成<Factory, <Dependancy[]>>的结构;
提一下module的几个属性: dependencies: 同步方式引入的模块; blocks: import() 方式引入的模块: variables: webpack 内置的变量 加入的依赖;
进入到addModuleDependencies里,对上一步的数据结构进行遍历处理,开始新一轮的resolve,createModule, 本例中会创建 add.js 的module;然后加入到compilation中;
把add.js module跟 引入 它的module(index.js) 以及 填充index 中 该资源的依赖的module字段;
const iterationDependencies = depend => {
for (let index = 0; index < depend.length; index++) {
const dep = depend[index];
dep.module = dependentModule; // 填充依赖的module字段
dependentModule.addReason(module, dep); // 跟引入它的module建立关系
}
};
dependentModule = addModuleResult.module;
iterationDependencies(dependencies);
然后继续开始b.js的build,获取它的依赖,本次的依赖经过processModuleDependencies的处理为空数组,至此结束make阶段;
seal 阶段
具体代码在compilation.seal 中, 这里面触发了一堆事件,具体可以查阅代码,总之,这里面会构建chunk,将module分配到chunk中,为module设置id,为chunk设置id, 为module,chunk设置hash,生成文件内容;
我们主要看下文件的生成过程:
这是输出代码的各部分构成,方便后续理解:
先看createChunkAssets: 首先调用mainTemplate.getRenderList,触发renderManifest,触发JavascriptModulesPlugin的监听,返回如下:
[{
render: () =>
compilation.mainTemplate.render(
hash,
chunk,
moduleTemplates.javascript, // javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"),
dependencyTemplates
),
filenameTemplate, // 'index.js'
pathOptions: {
noChunkHash: !useChunkHash,
contentHashType: "javascript",
chunk
},
identifier: `chunk${chunk.id}`,// chunk.id = 0;
hash: useChunkHash ? chunk.hash : fullHash //13e9c3686eb70120279b158de175
}]
上面出现的三个模版: MainTemplate 主要用于生成webpack运行时的框架代码。 RunTimeTemplate 是 Webpack 编译器中用于生成运行时代码的模板。我们会用到importStatement的方法 ModuleTemplate 用于生成每个模块的代码。
获取到mainfest,webpack会遍历调用每一项的render方法,获取source;进入到mainTemplate.render 方法中
- 首先会renderBootstrap,生成webpack_require函数部分和它的静态属性;
- 将结果作为载荷触发 render钩子,执行mainTemplate的订阅函数,在这里面生成bootStrap 的外层IIFE, 参数部分的生成需要mainTemplate的modules事件订阅函数返回;
- 执行JavascriptModulesPlugin的订阅,调用Template.renderChunkModules, 里面循环调用ModuleTemplate的render方法
里面分为了四部分:
1.调用NormalModule.source
里面调用JavascriptGenerator.generate 为资源生成 ReplaceSource,这些replace是由模块的依赖来生成的;
// JavascriptGenerator
const source = new ReplaceSource(originalSource);
首先HarmonyCompatibilityDependency,为source加‘_webpack_require_.r(_webpack_exports_);\n’, 定义__esModule;
HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate {
apply(dep, source, runtime) {
const usedExports = dep.originModule.usedExports;
if (usedExports !== false && !Array.isArray(usedExports)) {
// 定义 module esmodule = true;
const content = runtime.defineEsModuleFlagStatement({
exportsArgument: dep.originModule.exportsArgument
});
source.insert(-10, content);
}
}
}
第二个依赖:HarmonyInitDependency,处理import语句,它会遍历该模块的依赖,找到HarmonyImportDepandancy的实例,也就是(HarmonyImportSideEffectDependency,HarmonyImportSpecifierDependency),然后依次执行它们的template.harmonyInit的方法:
harmonyInit(dep, source, runtime, dependencyTemplates) {
let sourceInfo = importEmittedMap.get(source); // importEmittedMap单例
if (!sourceInfo) { // HarmonyImportSideEffectDependency 执行时为空,后面HarmonyImportSpecifierDependency 执行时有值;
importEmittedMap.set(
source,
(sourceInfo = {
emittedImports: new Map()
})
);
}
const key = dep._module || dep.request;
if (key && sourceInfo.emittedImports.get(key)) return;
sourceInfo.emittedImports.set(key, true);
const content = dep.getImportStatement(false, runtime); // 调用HarmonyImportDepandancy的getImportStatement
source.insert(-1, content);
}
//HarmonyImportDepandancy.getImportStatement
getImportStatement(update, runtime) {
return runtime.importStatement({
update,
module: this._module,
importVar: this.getImportVar(),
request: this.request,
originModule: this.originModule
});
}
//HarmonyImportDepandancy.getImportVar
getImportVar() {
let importVarMap = this.parserScope.importVarMap; // 这个地方两个依赖共用,SlideEffer/Specifier
if (!importVarMap) this.parserScope.importVarMap = importVarMap = new Map();
let importVar = importVarMap.get(this._module);
if (importVar) return importVar;
importVar = `${Template.toIdentifier(
`${this.userRequest}`
)}__WEBPACK_IMPORTED_MODULE_${importVarMap.size}__`; // _add_js__WEBPACK_MODULE_0_
importVarMap.set(this._module, importVar);
return importVar;
}
// RunTimeTemplate.importStatement 处理导入语句
importStatement({ update, module, request, importVar, originModule }) {
if (!module) {
return this.missingModuleStatement({
request
});
}
// 获取moduleId
const moduleId = this.moduleId({
module,
request
});
const optDeclaration = update ? "" : "var ";
const exportsType = module.buildMeta && module.buildMeta.exportsType;
let content = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`;
if (!exportsType && !originModule.buildMeta.strictHarmonyModule) {
content += `/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/__webpack_require__.n(${importVar});\n`;
}
if (exportsType === "named") {
if (Array.isArray(module.buildMeta.providedExports)) {
content += `${optDeclaration}${importVar}_namespace = /*#__PURE__*/__webpack_require__.t(${moduleId}, 1);\n`;
} else {
content += `${optDeclaration}${importVar}_namespace = /*#__PURE__*/__webpack_require__.t(${moduleId});\n`;
}
}
return content;
}
经过一系列操作,生成'/* harmony import */ var add_js__WEBPACK_IMPORTED_MODULE_0_ = _webpack_require_(1);'处理了import语句;
第三个依赖:ConstDependency,加入了一个空的Replacement:
ConstDependency.Template = class ConstDependencyTemplate {
apply(dep, source) {
if (typeof dep.range === "number") {
source.insert(dep.range, dep.expression);
return;
}
source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
}
};
第四个依赖:HarmonyImportSideEffectDependency, 啥也没干,空函数
apply(dep, source, runtime) {
// no-op
}
第五个依赖:HarmonyImportSpecifierDependency 替换表达式中的引用
apply(dep, source, runtime) {
super.apply(dep, source, runtime); // super 啥也没干
const content = this.getContent(dep, runtime);
source.replace(dep.range[0], dep.range[1] - 1, content);
}
getContent(dep, runtime) {
const exportExpr = runtime.exportFromImport({
module: dep._module,
request: dep.request,
exportName: dep._id,
originModule: dep.originModule,
asiSafe: dep.shorthand,
isCall: dep.call,
callContext: !dep.directImport,
importVar: dep.getImportVar(), // 第二个依赖搞完设置了;
});
return dep.shorthand ? `${dep.name}: ${exportExpr}` : exportExpr;
}
// RuntimeTemplate
exportFromImport({module,request,exportName,originModule,asiSafe,isCall,callContext,importVar}) {
if (!module) {
...
}
const exportsType = module.buildMeta && module.buildMeta.exportsType;// ast 解析时相关节点触发的插件填充的,这里是HarmonyDetectionParserPlugin填充的
if (!exportsType) {
...
}
if (exportsType === "named") {
if (exportName === "default") {
return importVar;
} else if (!exportName) {
return `${importVar}_namespace`;
}
}
if (exportName) {
const used = module.isUsed(exportName);
if (!used) {
const comment = Template.toNormalComment(`unused export ${exportName}`);
return `${comment} undefined`;
}
const comment = used !== exportName ? Template.toNormalComment(exportName) + " " : "";
const access = `${importVar}[${comment}${JSON.stringify(used)}]`;
if (isCall) { // 是否时函数调用
if (callContext === false && asiSafe) {
return `(0,${access})`;
} else if (callContext === false) {
return `Object(${access})`;
}
}
return access; // _add_js__WEBPACK_IMPORTED_MODULE_0__["add"]
} else {
return importVar;
}
}
2.moduleTemplate.hooks.content
该事件在本例中没有相关订阅事件
3.moduleTemplate.hooks.module
该事件在本例中没有相关订阅事件,订阅该事件的plugin有EvalDevToolModuleTemplatePlugin.js,EvalSourceMapDevToolModuleTemplatePlugin.js
4.moduleTemplate.hooks.render
将模块代码用外层函数包裹
订阅函数:
// FunctionModuleTemplatePlugin
(moduleSource, module) => {
const source = new ConcatSource();
const args = [module.moduleArgument]; // ['module']
// TODO remove HACK checking type for javascript
if (module.type && module.type.startsWith("javascript")) { // type: 'javascript/auto'
args.push(module.exportsArgument); // __webpack_exports__
if (module.hasDependencies(d => d.requireWebpackRequire !== false)) { //true
args.push("__webpack_require__");
}
} else if (module.type && module.type.startsWith("json")) {
// no additional arguments needed
} else {
args.push(module.exportsArgument, "__webpack_require__");
}
// 将module,__webpack_exports__,__webpack_require__ 作为模块的参数
source.add("/***/ (function(" + args.join(", ") + ") {\n\n");
if (module.buildInfo.strict) source.add('"use strict";\n');
source.add(moduleSource);
source.add("\n\n/***/ })");
return source;
}
5.moduleTemplate.hooks.package
是否对打包的模块标注;我们没设置,所以不走;
至此,我们就得到了模块的source, 现在模块的replace操作还没有执行,只是保存在了source中,方便后续有流程改的话可以拿到信息;
最终替换的代码在最终输出文件的地方,也就是Complier的emitAssets中,source()调用会执行replace;
let content;
if (typeof source.buffer === "function") {
content = source.buffer();
} else {
const bufferOrString = source.source(); // 生成最终内容
if (Buffer.isBuffer(bufferOrString)) {
content = bufferOrString;
} else {
content = Buffer.from(bufferOrString, "utf8");
}
}
// Create a replacement resource which only allows to ask for size
// This allows to GC all memory allocated by the Source
// (expect when the Source is stored in any other cache)
cacheEntry.sizeOnlySource = new SizeOnlySource(content.length);
compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
size: content.length
});
// Write the file to output file system
this.outputFileSystem.writeFile(targetPath, content, errorCallback);
我们看一下index.js 最后的replacements(在source()调用中经过了排序, end大的在前,其次start大的靠前,最后看insertIndex):
{
start: 93,
end: 95,
content: '_add_js__WEBPACK_IMPORTED_MODULE_0__["add"]',
insertIndex: 3,
},
{ start: 0, end: 30, content: "", insertIndex: 2 },
{
start: -1,
end: -2,
content:
"/* harmony import */ var _add_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);\n",
insertIndex: 1,
},
{
start: -10,
end: -11,
content: "__webpack_require__.r(__webpack_exports__);\n",
insertIndex: 0,
},
每个replace执行如下操作,就是将替换部分切掉,然后填充content;
result=[str]; // 原来的文件内容
function(repl) {
var remSource = result.pop();
var splitted1 = this._splitString(remSource, Math.floor(repl.end + 1));
var splitted2 = this._splitString(splitted1[0], Math.floor(repl.start));
result.push(splitted1[1], repl.content, splitted2[0]);
}
_splitString(str, position) {
return position <= 0 ? ["", str] : [str.substr(0, position), str.substr(position)];
}
// 例如第一个replacement 切成
[
'd;\n}\nconst r = test();\n'
'_add_js__WEBPACK_IMPORTED_MODULE_0__["add"]'
'import { add } from "./add.js";\n\nfunction test() {\n const tmp = "something";\n return tmp + '
]
// 最后拼接起来得到最终结果;
let resultStr = "";
for(let i = result.length - 1; i >= 0; --i) {
resultStr += result[i];
}
这里有个细节,数组里第一项是内容的最后一个字符串,也就是倒过来了,可以想下这么做的原因🐶;(这也是为什么要对replacement排序的原因)
同理,add.js的代码跟index.js基本相同;至此我们就了解了webpack打包的整个流程,这里我省略了很多分支,只探讨了主干部分,希望对想探究webpack原理的朋友有所帮助。
转载自:https://juejin.cn/post/7202514143340232741