webpack2源码make后对模块封装打包输出

68 阅读8分钟

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,如下图所示:

335549422-ec7eebcf-e8c6-430c-a7b2-edc9b83e2488.png

将 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整个打包流程如下:

339248386-244eb233-0ec9-4e51-af5f-9aefce08f1b4.png