index.js
import { A } from './a.js'
function test() {
const tmp = 'something'
return tmp + A
}
const r = test()
a.js
export const A = "a"
如上所示,index.js和a.js文件,上篇我们分析了webpack的make阶段,现在我们根据make生成的module以及dependencies树来分析如何封装打包输出的。
class Compilation extends Tapable {
seal(callback) {
const self = this;
self.applyPlugins0("seal");
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.processDependenciesBlockForChunk(module, chunk);
});
self.applyPlugins0("optimize");
//执行各种优化 module...
//优化module树
self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) {
self.createModuleAssets();
if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) {
self.applyPlugins0("before-chunk-assets");
self.createChunkAssets();
}
self.applyPluginsAsync("additional-assets", err => {
if(err) {
return callback(err);
}
//优化chunk assets
self.applyPluginsAsync("optimize-chunk-assets", self.chunks, err => {
if(err) {
return callback(err);
}
self.applyPluginsAsync("optimize-assets", self.assets, err => {
if(err) {
return callback(err);
}
self.applyPlugins1("after-optimize-assets", self.assets);
//回调
return self.applyPluginsAsync("after-seal", callback);
});
});
});
});
}
}
遍历preparedChunks,preparedChunks只有一个元素,这个元素存储是之前生成module时存储的index.js mudule和module名,这个元素即是entry module相关内容。addChunk方法创建 entry module的chunk。然后创建entrypoint对象存储这个chunk。chun.addModule把module存储到chunk.modules数组。同时,module.addChunk把chunk也存储到module。self.processDependenciesBlockForChunk遍历模块依赖树,收集子module。
class Compilation extends Tapable {
processDependenciesBlockForChunk(block, chunk) {
const queue = [
[block, chunk]
];
while(queue.length) {
const queueItem = queue.pop();
block = queueItem[0];
chunk = queueItem[1];
if(block.variables) {
block.variables.forEach(v => v.dependencies.forEach(iteratorDependency, this));
}
if(block.dependencies) {
block.dependencies.forEach(iteratorDependency, this);
}
if(block.blocks) {
block.blocks.forEach(iteratorBlock, this);
}
}
function iteratorBlock(b) {
let c;
if(!b.chunks) {
c = this.addChunk(b.chunkName, b.module, b.loc);
b.chunks = [c];
c.addBlock(b);
} else {
c = b.chunks[0];
}
chunk.addChunk(c);
c.addParent(chunk);
queue.push([b, c]);
}
function iteratorDependency(d) {
if(!d.module) {
return;
}
if(d.weak) {
return;
}
if(chunk.addModule(d.module)) {
d.module.addChunk(chunk);
queue.push([d.module, chunk]);
}
}
}
}
processDependenciesBlockForChunk接受参数entry module和entry module 对应的chunk,参数组成一个数组queue。循环遍历queue,module.dependencies为true,iteratorDependency函数遍历module.dependencies。下面看看函数iteratorDependency,如果dep没有module则跳出函数,chunk.addModule判断chunk中是否含有此module,如果没有module,向chunk.modules数组中放入此module,同时此chunk也会记录到module。然后把[d.module, chunk]放到queue中,处理子模块依赖。最终entry chunk包含index.js module和a.js module,如下图所示:
将 module 按规则组织成 chunks ,webpack 内置的 chunk 封装规则:
- entry 及 entry 触达到的模块,组合成一个 chunk
- 使用动态引入语句引入的模块,各自组合成一个 chunk( 例如:import('./xxx/xx.js').then(res => {}) )
class Compilation extends Tapable {
createChunkAssets() {
const outputOptions = this.outputOptions;
const filename = outputOptions.filename;
const chunkFilename = outputOptions.chunkFilename;
for(let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
chunk.files = [];
let source;
let file;
const filenameTemplate = chunk.filenameTemplate ? chunk.filenameTemplate :
chunk.isInitial() ? filename :
chunkFilename;
try {
const useChunkHash = !chunk.hasRuntime() || (this.mainTemplate.useChunkHash && this.mainTemplate.useChunkHash(chunk));
if(chunk.hasRuntime()) {
source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);
} else {
source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);
}
file = this.getPath(filenameTemplate, {
noChunkHash: !useChunkHash,
chunk
});
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));
}
}
}
}
createChunkAssets函数创建要打包输出的资源,主要包含两部分,(1)打包输出文件名(2)打包输出的内容。遍历this.chunks数组,在上面的demo实例中,只有一个以index.js入口的chunk。chunk.hasRuntime为true,执行source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);这个用于渲染生成要输出的资源source。file是要输出的文件名。把要输出的资源source作为键值,file作为键名存储到compilation.assets对象中。assets就是最终要生成的文件列表。下面分析render如何工作的。
MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) {
var buf = [];
buf.push(this.applyPluginsWaterfall("bootstrap", "", chunk, hash, moduleTemplate, dependencyTemplates));
buf.push(this.applyPluginsWaterfall("local-vars", "", chunk, hash));
buf.push("");
buf.push("// The require function");
buf.push("function " + this.requireFn + "(moduleId) {");
buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash)));
buf.push("}");
buf.push("");
buf.push(this.asString(this.applyPluginsWaterfall("require-extensions", "", chunk, hash)));
buf.push("");
buf.push(this.asString(this.applyPluginsWaterfall("startup", "", chunk, hash)));
var source = this.applyPluginsWaterfall("render", new OriginalSource(this.prefix(buf, " \t") + "\n", "webpack/bootstrap " + hash), chunk, hash, moduleTemplate, dependencyTemplates);
if(chunk.hasEntryModule()) {
source = this.applyPluginsWaterfall("render-with-entry", source, chunk, hash);
}
chunk.rendered = true;
return new ConcatSource(source, ";");
};
MainTemplate.prototype.render方法把要输出_webpack_require_函数相关的分别处理的子字符串放到buf数组中,然后通过this.prefix(buf, " \t") + "\n"把这些子字符串拼接在一起,最后生成的字符串如下所示。然后触发render事件,渲染chunk相关的modules的输出内容,render事件返回ConcatSource对象。然后把render事件返回的source对象进一步封装到ConcatSource对象,并作为render函数的返回值。
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId])
return installedModules[moduleId].exports;
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// identity function for calling harmony imports with the correct context
__webpack_require__.i = function(value) { return value; };
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = 1);
this.plugin("render", function(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) {
var source = new ConcatSource();
source.add("/******/ (function(modules) { // webpackBootstrap\n");
source.add(new PrefixSource("/******/", bootstrapSource));
source.add("/******/ })\n");
source.add("/************************************************************************/\n");
source.add("/******/ (");
var modules = this.renderChunkModules(chunk, moduleTemplate, dependencyTemplates, "/******/ ");
source.add(this.applyPluginsWaterfall("modules", modules, chunk, hash, moduleTemplate, dependencyTemplates));
return source;
});
监听render事件,渲染生成要输出内容。new ConcatSource对象source,通过 add方法把一些要输出的内容依次push到source对象的children数组中。 source.add(new PrefixSource("/******/", bootstrapSource));把上面刚介绍的打包输出的_wepack_require函数及其相关代码,封装到PrefixSource对象存储到children数组中。 source.add(this.applyPluginsWaterfall("modules", modules, chunk, hash, moduleTemplate, dependencyTemplates));把chunk相关的modules的source封装成ConcatSource对象存储到children数组中。 下面是source对象:
{
children: [
"/******/ (function(modules) { // webpackBootstrap\n",
{
_source: {
_value: " \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 1);\n",
_name: "webpack/bootstrap 3ad9d4d12d99c6e3531a",
},
_prefix: "/******/",
},
"/******/ })\n",
"/************************************************************************/\n",
"/******/ (",
{
children: [
"[\n",
"/* 0 */",
"\n",
{
children: [
"/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n",
"\"use strict\";\n",
{
_source: {
_source: {
_value: "export var A = \"a\";",
_name: "/Users/lispringli/Desktop/webpack-2.2.0/node_modules/babel-loader/lib/index.js?{\"presets\":[\"@babel/preset-env\"]}!/Users/lispringli/Desktop/webpack-2.2.0/bin/test/src/a.js",
},
_name: undefined,
replacements: [
[
0,
6,
"",
0,
],
[
-0.5,
-1.5,
"/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return A; });\n",
1,
],
],
},
_cachedSource: undefined,
_cachedSize: undefined,
_cachedMaps: {
},
node: function(options) {
return this._source.node(options);
},
listMap: function(options) {
return this._source.listMap(options);
},
},
"\n\n/***/ })",
],
},
",\n",
"/* 1 */",
"\n",
{
children: [
"/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n",
"\"use strict\";\n",
{
_source: {
_source: {
_value: "import { A } from './a.js';\nfunction test() {\n var tmp = 'something';\n return tmp + A;\n}\nvar r = test();",
_name: "/Users/lispringli/Desktop/webpack-2.2.0/node_modules/babel-loader/lib/index.js?{\"presets\":[\"@babel/preset-env\"]}!/Users/lispringli/Desktop/webpack-2.2.0/bin/test/src/index.js",
},
_name: undefined,
replacements: [
[
-1,
-2,
"Object.defineProperty(__webpack_exports__, \"__esModule\", { value: true });\n",
0,
],
[
0,
26,
"",
1,
],
[
-1,
-2,
"/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__a_js__ = __webpack_require__(0);\n",
2,
],
[
86,
86,
"__WEBPACK_IMPORTED_MODULE_0__a_js__[\"a\" /* A */]",
3,
],
],
},
_cachedSource: undefined,
_cachedSize: undefined,
_cachedMaps: {
},
node: function(options) {
return this._source.node(options);
},
listMap: function(options) {
return this._source.listMap(options);
},
},
"\n\n/***/ })",
],
},
"\n/******/ ]",
],
},
")",
],
}
触发并执行render事监听后,获得source对象,MainTemplate render方法进一步把source包装到一个ConcatSource对象。并且返回new ConcatSource(source, ";")对象。对象如下所示:
{
children: [
{
children: [
"/******/ (function(modules) { // webpackBootstrap\n",
{
_source: {
_value: " \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 1);\n",
_name: "webpack/bootstrap 3ad9d4d12d99c6e3531a",
},
_prefix: "/******/",
},
"/******/ })\n",
"/************************************************************************/\n",
"/******/ (",
{
children: [
"[\n",
"/* 0 */",
"\n",
{
children: [
"/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n",
"\"use strict\";\n",
{
_source: {
_source: {
_value: "export var A = \"a\";",
_name: "/Users/lispringli/Desktop/webpack-2.2.0/node_modules/babel-loader/lib/index.js?{\"presets\":[\"@babel/preset-env\"]}!/Users/lispringli/Desktop/webpack-2.2.0/bin/test/src/a.js",
},
_name: undefined,
replacements: [
[
0,
6,
"",
0,
],
[
-0.5,
-1.5,
"/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return A; });\n",
1,
],
],
},
_cachedSource: undefined,
_cachedSize: undefined,
_cachedMaps: {
},
node: function(options) {
return this._source.node(options);
},
listMap: function(options) {
return this._source.listMap(options);
},
},
"\n\n/***/ })",
],
},
",\n",
"/* 1 */",
"\n",
{
children: [
"/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n",
"\"use strict\";\n",
{
_source: {
_source: {
_value: "import { A } from './a.js';\nfunction test() {\n var tmp = 'something';\n return tmp + A;\n}\nvar r = test();",
_name: "/Users/lispringli/Desktop/webpack-2.2.0/node_modules/babel-loader/lib/index.js?{\"presets\":[\"@babel/preset-env\"]}!/Users/lispringli/Desktop/webpack-2.2.0/bin/test/src/index.js",
},
_name: undefined,
replacements: [
[
-1,
-2,
"Object.defineProperty(__webpack_exports__, \"__esModule\", { value: true });\n",
0,
],
[
0,
26,
"",
1,
],
[
-1,
-2,
"/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__a_js__ = __webpack_require__(0);\n",
2,
],
[
86,
86,
"__WEBPACK_IMPORTED_MODULE_0__a_js__[\"a\" /* A */]",
3,
],
],
},
_cachedSource: undefined,
_cachedSize: undefined,
_cachedMaps: {
},
node: function(options) {
return this._source.node(options);
},
listMap: function(options) {
return this._source.listMap(options);
},
},
"\n\n/***/ })",
],
},
"\n/******/ ]",
],
},
")",
],
},
";",
],
}
执行完 seal后,执行compiler.compile回调onCompiled,回调onCompiled是在compiler.run方法中执行的,如下。onCompiled 中调用 compiler.emitAssets 和 compiler.emitRecords 对资源做输出。
Compiler.prototype.run = function(callback) {
var self = this;
self.compile(function onCompiled(err, compilation) {
if(err) return callback(err);
self.emitAssets(compilation, function(err) {
if(err) return callback(err);
self.emitRecords(function(err) {
if(err) return callback(err);
var stats = compilation.getStats();
stats.startTime = startTime;
stats.endTime = new Date().getTime();
self.applyPlugins("done", stats);
return callback(null, stats);
});
});
});
};
seal把各个代码块 chunk 转换成一个一个文件加入到输出列表。确定好输出内容之后,根据配置的输出路径和文件名,将文件内容写入到文件系统。 webpack整个打包流程如下: