做者:崔靜、肖磊javascript
通過前幾篇文章咱們介紹了 webpack 如何從配置文件的入口開始,將每個文件轉變爲內部的 module,而後再由 module 整合成一個一個的 chunk。這篇文章咱們來看一下最後一步 —— chunk 如何轉變爲最終的 js 文件。html
上篇文章主要是梳理了在 seal 階段的開始, webpack 內部是如何將有依賴關係的 module 統一組織到一個 chunk 當中的。如今繼續來看 seal 階段,chunk 生成以後的部分,咱們從 optimizeTree.callAsync
看起java
seal(callback) {
// 優化 dependence 的 hook
// 生成 chunk
// 優化 modules 的 hook,提供給插件修改 modules 的能力
// 優化 chunk 的 hook,提供給插件修改 chunk 的能力
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
//... 優化 chunk 和 module
//... record 爲記錄相關的,不是主流程,這裏先忽略
//... 優化順序
// 生成 module id
this.hooks.beforeModuleIds.call(this.modules);
this.hooks.moduleIds.call(this.modules);
this.applyModuleIds();
//... optimize
// 排序
this.sortItemsWithModuleIds();
// 生成 chunk id
//...
this.hooks.optimizeChunkOrder.call(this.chunks);
this.hooks.beforeChunkIds.call(this.chunks);
this.applyChunkIds();
//... optimize
// 排序
this.sortItemsWithChunkIds();
//...省略 recode 相關代碼
// 生成 hash
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
//...
// 生成最終輸出靜態文件的內容
this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
}
this.hooks.additionalChunkAssets.call(this.chunks);
this.summarizeDependencies();
//...
// 增長 webpack 須要的額外代碼
this.hooks.additionalAssets.callAsync(err => {
//...
});
});
}
複製代碼
上面代碼中,按照從上往下的順序依次看,經歷的主流程以下:node
主要步驟爲:生成 moduleId,生成 chunkId,生成 hash,而後生成最終輸出文件的內容,同時每一步之間都會暴露 hook , 提供給插件修改的機會。接下來咱們一一看一下核心邏輯:id 生成,hash 生成,文件內容生成webpack
webpack 會對 module 和 chunk 分別生成id,這兩者在邏輯上基本相同。咱們先以 module id 爲例來看 id 生成的過程(在 webpack 爲 module 生成 id 的邏輯位於 applyModuleIds
方法中),代碼以下git
applyModuleIds() {
const unusedIds = [];
let nextFreeModuleId = 0;
const usedIds = new Set();
if (this.usedModuleIds) {
for (const id of this.usedModuleIds) {
usedIds.add(id);
}
}
const modules1 = this.modules;
for (let indexModule1 = 0; indexModule1 < modules1.length; indexModule1++) {
const module1 = modules1[indexModule1];
if (module1.id !== null) {
usedIds.add(module1.id);
}
}
if (usedIds.size > 0) {
let usedIdMax = -1;
for (const usedIdKey of usedIds) {
if (typeof usedIdKey !== "number") {
continue;
}
usedIdMax = Math.max(usedIdMax, usedIdKey);
}
let lengthFreeModules = (nextFreeModuleId = usedIdMax + 1);
while (lengthFreeModules--) {
if (!usedIds.has(lengthFreeModules)) {
unusedIds.push(lengthFreeModules);
}
}
}
// 爲 module 設置 id
const modules2 = this.modules;
for (let indexModule2 = 0; indexModule2 < modules2.length; indexModule2++) {
const module2 = modules2[indexModule2];
if (module2.id === null) {
if (unusedIds.length > 0) module2.id = unusedIds.pop();
else module2.id = nextFreeModuleId++;
}
}
}
複製代碼
能夠看到設置 id 的流程主要分兩步:github
找到當前未使用的 id 和 已經使用的最大的 id。舉個例子:若是已經使用的 id 是 [3, 6, 7 ,8]
,那麼通過第一步處理後,nextFreeModuleId = 9
, unusedIds = [0, 1, 2, 4, 5]
。web
給沒有 id 的 module 設置 id。設置 id 時,優先使用 unusedIds 中的值。json
在設置 id 的時候,有一個判斷 module2.id === null
,也就是說若在這一步以前,已經被設置過 id 值,那麼這裏便直接忽略。在設置 id 以前,會觸發兩個鉤子:bootstrap
this.hooks.beforeModuleIds.call(this.modules);
this.hooks.moduleIds.call(this.modules);
複製代碼
咱們可在這兩個鉤子中,操做 module,設置本身的 id。webpack 內部 NamedModulesPlugin 就是註冊在 beforeModuleIds
鉤子上,將 module 的相對路徑設置爲 id。在開發環境下,便於咱們調試和分析代碼,webpack 默認會使用這個插件。
設置完 id 以後,會對 this.modules 中的 module 和 chunks 中的 module 按照 id 來排序。同時還會對 module 中的 reason 和 usedExports 排序。
chunk id 的生成邏輯與 module id 相似,一樣的,在設置完 id 後,按照 id 進行排序。
在 webpack 生成最後文件的時候,咱們常常會設置文件名稱爲 [name].[hash].js
的模式,給文件名稱增長一個 hash 值。憑着直覺,這裏的 hash 值和文件內容相關,可是具體是怎麼來的呢?答案就位於 Compilation.js 的 createHash
方法中:
createHash() {
const outputOptions = this.outputOptions;
const hashFunction = outputOptions.hashFunction;
const hashDigest = outputOptions.hashDigest;
const hashDigestLength = outputOptions.hashDigestLength;
const hash = createHash(hashFunction);
//... update hash
// module hash
const modules = this.modules;
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
module.hash = moduleHash.digest(hashDigest);
module.renderedHash = module.hash.substr(0, hashDigestLength);
}
// clone needed as sort below is inplace mutation
const chunks = this.chunks.slice();
/** * sort here will bring all "falsy" values to the beginning * this is needed as the "hasRuntime()" chunks are dependent on the * hashes of the non-runtime chunks. */
chunks.sort((a, b) => {
const aEntry = a.hasRuntime();
const bEntry = b.hasRuntime();
if (aEntry && !bEntry) return 1;
if (!aEntry && bEntry) return -1;
return byId(a, b);
});
// chunck hash
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkHash = createHash(hashFunction);
if (outputOptions.hashSalt) chunkHash.update(outputOptions.hashSalt);
chunk.updateHash(chunkHash);
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
template.updateHashForChunk(chunkHash, chunk);
this.hooks.chunkHash.call(chunk, chunkHash);
chunk.hash = chunkHash.digest(hashDigest);
hash.update(chunk.hash);
chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
this.hooks.contentHash.call(chunk);
}
this.fullHash = hash.digest(hashDigest);
this.hash = this.fullHash.substr(0, hashDigestLength);
}
複製代碼
主結構其實就是兩部分:
webpack 中計算 hash 值底層所使用的是 Node.js 中的 crypto, 主要用到了兩個方法:
hash.update
能夠簡單認爲是增長用於生成 hash 的原始內容(如下統一簡稱爲 hash 源)digest
方法用來獲得最終 hash 值。下面咱們先看 module hash 生成過程。
module hash 生成的代碼邏輯以下:
createHash() {
//...省略其餘邏輯
const modules = this.modules;
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
module.hash = moduleHash.digest(hashDigest);
module.renderedHash = module.hash.substr(0, hashDigestLength);
}
//...省略其餘邏輯
}
複製代碼
其中關鍵的 updateHash
方法,封裝在每一個 module 類的實現中,調用關係以下:
上面圖能夠看到,module hash 內容包括:
每一個 module 中本身特有的須要寫入 hash 中的信息
而對於 NormalModule 來講,這個方法具體爲:
updateHash(hash) {
this.updateHashWithSource(hash);
this.updateHashWithMeta(hash);
super.updateHash(hash);
}
複製代碼
也就說會包含 souce 內容和生成文件相關的元信息 buildMeta。
module id 和 被使用到的 exports 信息
依賴的信息
各個依賴具體有哪些信息要寫入 hash 源,由 xxxDependency.js 中 updateHash
方法決定。例以下面的代碼
// 打包的入口 main.js
import { A } from './a.js'
import B from './b.js'
import 'test-module'
console.log(A)
B()
複製代碼
轉化爲 module 後(module 生成的流程請回憶webpack系列之五module生成2),三個 import
會獲得三個 HarmonyImportSideEffectDependency
。這裏就以該依賴爲例,看一下 hash 內容原始內容中寫入依賴信息的過程,以下圖
上面圖能夠看出,依賴的 module 會影響當前 module 的 hash,若是咱們修改順序或者其餘的操做形成依賴 module 的 id 改變了,那麼當前 module 獲得的 hash 也會改變。 因此 module 的 hash 內容不只包含了源碼,還包含了和其打包構建相關的內容。由於當咱們修改了 webpack 的相關配置時,最終獲得的代碼頗有可能會改變,將這些會影響最終代碼生成的配置寫入生成 hash 的 buffer 中能夠保證,當咱們僅修改 webpack 的打包配置,好比改變 module id 生成方式等,也能夠獲得一個 hash 值不一樣的文件名。
在生成 chunk hash 以前,會先對 chunck 進行排序(爲何要排序,這個問題先放一下,在咱們看完 chunk 生成以後再來解答)。 chunck hash 生成,第一步是 chunk.updateHash(chunkHash);
,具體代碼以下(位於 Chunck.js 中):
updateHash(hash) {
hash.update(`${this.id} `);
hash.update(this.ids ? this.ids.join(",") : "");
hash.update(`${this.name || ""} `);
for (const m of this._modules) {
hash.update(m.hash);
}
}
複製代碼
這部分邏輯很簡單,將 id,ids,name 和其包含的全部 module 的 hash 信息寫入。而後寫入生成 chunck 的模板信息: template.updateHashForChunk(chunkHash, chunk)
。webpack 將 template 分爲兩種:mainTemplate 最終會生成包含 runtime 的代碼 和 chunkTemplate,也就是咱們在第一篇文章裏看到的經過 webpackJsonp 加載的 chunck 代碼模板。
咱們主要看 mainTemplate 的 updateHashForChunk
方法
updateHashForChunk(hash, chunk) {
this.updateHash(hash);
this.hooks.hashForChunk.call(hash, chunk);
}
updateHash(hash) {
hash.update("maintemplate");
hash.update("3");
hash.update(this.outputOptions.publicPath + "");
this.hooks.hash.call(hash);
}
複製代碼
這裏會將 template 類型 "maintemplate" 和 咱們配置的 publicPath 寫入。而後觸發 的 hash 事件和 hashForChunk 事件會將一些文件的輸出信息寫入。例如:加載 chunck 所使用的 jsonp 方式,是經過 JsonpMainTemplatePlugin 實現的。在 hash hooks 中會觸發其回調,將 jsonp 的相關信息寫入 hash,例如:jsonp 回調函數的名稱等。將相關信息都存入 hash 的 buffer 以後,調用 digest 方法生成最終的 hash,而後從中截取出須要的長度,chunk 的 hash 就獲得了。
總的來看,chunk hash 依賴於其內部全部 module 的 hash,而且還依賴於咱們配置的各類輸出 chunk 相關的信息。和 module hash 相似,這樣才能保證當咱們修改了 webpack 的相關配置致使代碼改變後會獲得不一樣的 hash 值。
到此還遺留了一個問題,爲何在生成 chunk hash 時候要排序?
在 updateHashForChunk
過程當中,插件 TemplatePathPlugin 會在 hashForChunk hook 時被觸發並執行一段下面的邏輯
// TemplatePathPlugin.js
mainTemplate.hooks.hashForChunk.tap(
"TemplatedPathPlugin",
(hash, chunk) => {
const outputOptions = mainTemplate.outputOptions;
const chunkFilename =
outputOptions.chunkFilename || outputOptions.filename;
// 文件名帶 chunkhash
if (REGEXP_CHUNKHASH_FOR_TEST.test(chunkFilename))
hash.update(JSON.stringify(chunk.getChunkMaps(true).hash));
// 文件名帶 contenthash
if (REGEXP_CONTENTHASH_FOR_TEST.test(chunkFilename)) {
hash.update(
JSON.stringify(
chunk.getChunkMaps(true).contentHash.javascript || {}
)
);
}
// 文件名帶 name
if (REGEXP_NAME_FOR_TEST.test(chunkFilename))
hash.update(JSON.stringify(chunk.getChunkMaps(true).name));
}
);
複製代碼
若是咱們在 webpack.config.js 中設置輸出文件名稱帶有 chunkhash 的時候,好比: filename: [name].[chunkhash].js
,會查找當前全部 chunk 的 hash,獲得一個下面的結構:
{
hash: { // chunkHashMap
0: 'chunk 0 的 hash',
...
},
name: nameHashMap,
contentHash: { // chunkContentHashMap
javascript: {
0: 'chunk 0 的 contentHash',
...
}
}
}
複製代碼
而後將上面結果中 hash 內容轉爲字符串寫入 hash buffer 中。因此說對於有 runtime 的 chunk 這一步依賴於全部不含 runtime 的 chunk 的 hash 值。所以在計算 chunk hash 以前會有一段排序的邏輯。再深刻思考一步,爲何要依賴不含 runtime 的 chunk 的 hash 值呢?對於須要被異步加載的 chunk (即不含 runtime 的 chunk)在用到時會經過 script 標籤加載,這時 src 中即是其文件名稱,所以這個文件的名稱須要被保存在含有 runtime 的 chunk 中。當文件名稱包含 hash 值時,含 runtime 的 chunk 文件的內容會由於其餘 chunk 的 hash 值的不一樣而不一樣,從而生成的 hash 值也應該隨之改變。
hash 值生成以後,會調用 createChunkAssets 方法來決定最終輸出到每一個 chunk 當中對應的文本內容是什麼。
// Compilation.js
class Compilation extends Tapable {
...
createChunkAssets() {
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i]
try {
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
const manifest = template.getRenderManifest({
chunk,
hash: this.hash, // 此次 compilation 的 hash 值
fullHash: this.fullHash, // 此次 compilation 未被截斷的 hash 值
outputOptions,
moduleTemplates: this.moduleTemplates,
dependencyTemplates: this.dependencyTemplates
}); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
for (const fileManifest of manifest) {
...
source = fileManifeset.render() // 渲染生成每一個 chunk 最終輸出的代碼
...
this.assets[file] = source;
...
}
}
....
}
}
...
}
複製代碼
主要步驟:
在 createChunkAssets 方法內部會對最終須要輸出的 chunk 進行遍歷,根據這個 chunk 是否包含有 webpack runtime 代碼來決定使用的渲染模板(mainTemplate/chunkTemplate)。其中 mainTemplate 主要用於包含 webpack runtime bootstrap 的 chunk 代碼渲染生成工做,chunkTemplate 主要用於普通 chunk 的代碼渲染工做。
mainTemplate 和 chunkTemplate 分別有本身的 getRenderManifest 方法,在這個方法中會生成 render 代碼須要的全部信息,包括文件名稱格式、對應的 render 函數,哈希值等。
咱們首先來看下包含有 webpack runtime 代碼的 chunk 是如何輸出最終的 chunk 文本內容的。
這種狀況下使用的 mainTemplate,調用實例上的 getRenderManifest 方法獲取 manifest 配置數組,其中每項包含的字段內容爲:
// MainTemplate.js
class MainTemplate extends Tapable {
...
getRenderManifest(options) {
const result = [];
this.hooks.renderManifest.call(result, options);
return result;
}
...
}
複製代碼
接下來會判斷這個 chunk 是否有被以前已經輸出過(輸出過的 chunk 是會被緩存起來的)。若是沒有的話,那麼就會調用 render 方法去完成這個 chunk 的文本輸出工做,即:compilation.mainTemplate.render
方法。
// MainTemplate.js
module.exports = class MainTemplate extends Tapable {
...
constructor() {
// 註冊 render 鉤子函數
this.hooks.render.tap(
"MainTemplate",
(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
const source = new ConcatSource();
source.add("/******/ (function(modules) { // webpackBootstrap\n");
source.add(new PrefixSource("/******/", bootstrapSource));
source.add("/******/ })\n");
source.add(
"/************************************************************************/\n"
);
source.add("/******/ (");
source.add(
// 調用 modules 鉤子函數,用以渲染 runtime chunk 當中所須要被渲染的 module
this.hooks.modules.call(
new RawSource(""),
chunk,
hash,
moduleTemplate,
dependencyTemplates
)
);
source.add(")");
return source;
}
);
}
...
/** * @param {string} hash hash to be used for render call * @param {Chunk} chunk Chunk instance * @param {ModuleTemplate} moduleTemplate ModuleTemplate instance for render * @param {Map<Function, DependencyTemplate>} dependencyTemplates dependency templates * @returns {ConcatSource} the newly generated source from rendering */
render(hash, chunk, moduleTemplate, dependencyTemplates) {
// 生成 webpack runtime bootstrap 代碼
const buf = this.renderBootstrap(
hash,
chunk,
moduleTemplate,
dependencyTemplates
);
// 調用 render 鉤子函數
let source = this.hooks.render.call(
new OriginalSource(
Template.prefix(buf, " \t") + "\n",
"webpack/bootstrap"
),
chunk,
hash,
moduleTemplate,
dependencyTemplates
);
if (chunk.hasEntryModule()) {
source = this.hooks.renderWithEntry.call(source, chunk, hash);
}
if (!source) {
throw new Error(
"Compiler error: MainTemplate plugin 'render' should return something"
);
}
chunk.rendered = true;
return new ConcatSource(source, ";");
}
...
}
複製代碼
這個方法內部首先調用 renderBootstrap 方法完成 webpack runtime bootstrap 代碼的拼接工做,接下來調用 render hook,這個 render hook 是在 MainTemplate 的構造函數裏面就完成了註冊。 咱們能夠看到這個 hook 內部,主要是在 runtime bootstrap 代碼外面完成了一層包裝,而後調用 modules hook 開始進行這個 runtime chunk 當中須要渲染的 module 的生成工做(具體每一個 module 如何去完成代碼的拼接渲染工做後文會講)。 render hook 調用完後,即獲得了包含 webpack runtime bootstrap 代碼的 chunk 代碼,最終返回一個 ConcatSource 類型實例。 簡化一下,大概以下圖:
最終的代碼會被保存在一個 ConcatSource 類的 children 中,而每一個 module 的最終代碼在一個 ReplaceSource 的類中,這個類包含一個 replacements 的數組,裏面存放了對源碼轉化的操做,數組中每一個元素結構以下:
[替換源碼的起始位置,替換源碼的終止位置,替換的最終內容,優先級]
複製代碼
也就是說在 render 的過程當中,其實不會真的去改變源碼字符串,而是將要更改的內容保存在了一個數組中,在最後輸出靜態文件的時候根據這個數組和源碼來生成最終代碼。這樣保證了整個過程當中,咱們能夠追溯對源碼作了那些改變,而且在一些 hook 中,咱們能夠靈活的修改這些操做。
runtime chunk
webpack config 提供了一個代碼優化配置選項:是否將 runtime chunk 單獨抽離成一個 chunk 並輸出到最終的文件當中。這也決定了最終在 render hook 生成 runtime chunk 代碼時最終所包含的內容。首先咱們來看下相關配置信息:
// webpack.config.js
module.exports = {
...
optimization: {
runtimeChunk: {
name: 'bundle'
}
}
...
}
複製代碼
經過進行 optimization 字段的配置,能夠出發 RuntimeChunkPlugin 插件的註冊相關的事件。
module.exports = class RuntimeChunkPlugin {
constructor(options) {
this.options = Object.assign(
{
name: entrypoint => `runtime~${entrypoint.name}`
},
options
);
}
apply(compiler) {
compiler.hooks.thisCompilation.tap("RuntimeChunkPlugin", compilation => {
// 在 seal 階段,生成最終的 chunk graph 後觸發這個鉤子函數,用以生成新的 runtime chunk
compilation.hooks.optimizeChunksAdvanced.tap("RuntimeChunkPlugin", () => {
// 遍歷全部的 entrypoints(chunkGroup)
for (const entrypoint of compilation.entrypoints.values()) {
// 獲取每一個 entrypoints 的 runtimeChunk(chunk)
const chunk = entrypoint.getRuntimeChunk();
// 最終須要生成的 runtimeChunk 的文件名
let name = this.options.name;
if (typeof name === "function") {
name = name(entrypoint);
}
if (
chunk.getNumberOfModules() > 0 ||
!chunk.preventIntegration ||
chunk.name !== name
) {
// 新建一個 runtime 的 chunk,在 compilation.chunks 中也會新增這一個 chunk。
// 這樣在最終生成的 chunk 當中會包含一個 runtime chunk
const newChunk = compilation.addChunk(name);
newChunk.preventIntegration = true;
// 將這個新的 chunk 添加至 entrypoint(chunk) 當中,那麼 entrypoint 也就多了一個新的 chunk
entrypoint.unshiftChunk(newChunk);
newChunk.addGroup(entrypoint);
// 將這個新生成的 chunk 設置爲這個 entrypoint 的 runtimeChunk
entrypoint.setRuntimeChunk(newChunk);
}
}
});
});
}
};
複製代碼
這樣便經過 RuntimeChunkPlugin 這個插件將 webpack runtime bootstrap 單獨抽離至一個 chunk 當中輸出。最終這個 runtime chunk 僅僅只包含了 webpack bootstrap 相關的代碼,不會包含其餘須要輸出的 module 代碼。固然,若是你不想將 runtime chunk 單獨抽離出來,那麼這部分 runtime 代碼最終會被打包進入到包含 runtime chunk 的 chunk 當中,這個 chunk 最終輸出文件內容就不只僅須要包含這個 chunk 當中依賴的不一樣 module 的最終代碼,同時也須要包含 webpack bootstrap 代碼。
var window = window || {}
// webpackBootstrap
(function(modules) {
// 包含了 webpack bootstrap 的代碼
})([
/* 0 */ // module 0 的最終代碼
(function(module, __webpack_exports__, __webpack_require__) {
}),
/* 1 */ // module 1 的最終代碼
(function(module, __webpack_exports__, __webpack_require__) {
})
])
module.exports = window['webpackJsonp']
複製代碼
以上就是有關使用 MainTemplate 去渲染完成 runtime chunk 的有關內容。
接下來咱們看下不包含 webpack runtime 代碼的 chunk (使用 chunkTemplate 渲染模板)是如何輸出獲得最終的內容的。
首先調用 ChunkTemplate 類上提供的 getRenderManifest 方法來獲取 chunk manifest 相關的內容。
// ChunkTemplate.js
class ChunkTemplate {
...
getRenderManifest(options) {
const result = []
// 觸發 ChunkTemplate renderManifest 鉤子函數
this.hooks.renderManifest.call(result, options)
return result
}
...
}
// JavascriptModulesPlugin.js
class JavascriptModulesPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('JavascriptModulesPlugin', (compilation, { normalModuleFactory }) => {
...
// ChunkTemplate hooks.manifest 鉤子函數
compilation.chunkTemplate.hooks.renderManifest.tap('JavascriptModulesPlugin', (result, options) => {
...
result.push({
render: () =>
// 每一個 chunk 代碼的生成即調用 JavascriptModulesPlugin 提供的 renderJavascript 方法來進行生成
this.renderJavascript(
compilation.chunkTemplate, // chunk模板
chunk, // 須要生成的 chunk 實例
moduleTemplates.javascript, // 模塊類型
dependencyTemplates // 不一樣依賴所對應的渲染模板
),
filenameTemplate,
pathOptions: {
chunk,
contentHashType: 'javascript'
},
identifier: `chunk${chunk.id}`,
hash: chunk.hash
})
...
})
...
})
}
renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
const moduleSources = Template.renderChunkModules(
chunk,
m => typeof m.source === "function",
moduleTemplate,
dependencyTemplates
)
const core = chunkTemplate.hooks.modules.call(
moduleSources,
chunk,
moduleTemplate,
dependencyTemplates
)
let source = chunkTemplate.hooks.render.call(
core,
chunk,
moduleTemplate,
dependencyTemplates
)
if (chunk.hasEntryModule()) {
source = chunkTemplate.hooks.renderWithEntry.call(source, chunk)
}
chunk.rendered = true
return new ConcatSource(source, ";")
}
}
複製代碼
這樣經過觸發 renderManifest hook 獲取到了渲染這個 chunk manifest 配置項。和 MainTemplate 獲取到的 manifest 數組不一樣的主要地方就在於其中的 render 函數,這裏能夠看到的就是渲染每一個 chunk 是調用的 JavascriptModulesPlugin 這個插件上提供的 render 函數。
獲取到了 chunk 渲染所需的 manifest 配置項後,即開始調用 render 函數開始渲染這個 chunk 最終的輸出內容了,即對應於 JavascriptModulesPlugin 上的 renderJavascript 方法。
renderChunkModules——生成每一個module的代碼
在 webpack 總覽中,咱們介紹過 webpack 打包以後的常見代碼結構:
(function(modules){
...(webpack的函數)
return __webpack_require__(__webpack_require__.s = "./demo01/main.js");
})(
{
"./a.js": (function(){...}),
"./b.js": (function(){...}),
"./main.js": (function(){...}),
}
)
複製代碼
一個當即執行函數,函數的參數是各個 module 組成的對象(某些時候是數組)。這裏函數的參數就是 renderChunkModules 這個函數獲得的:經過 moduleTemplate.render 方法獲得每一個 module 的代碼,而後將其封裝爲數組的形式: [/*module a.js*/, /*module b.js*/]
或者對象的形式: {'a.js':function, 'b.js': function}
的形式,做爲參數添加到當即執行函數中。 renderChunkModules 方法代碼以下:
class Template {
static renderChunkModules(
chunk,
filterFn,
moduleTemplate,
dependencyTemplates,
prefix = ""
) {
const source = new ConcatSource();
const modules = chunk.getModules().filter(filterFn); // 獲取這個 chunk 所依賴的模塊
let removedModules;
if (chunk instanceof HotUpdateChunk) {
removedModules = chunk.removedModules;
}
// 若是這個 chunk 沒有依賴的模塊,且 removedModules 不存在,那麼當即返回,代碼再也不繼續向下執行
if (
modules.length === 0 &&
(!removedModules || removedModules.length === 0)
) {
source.add("[]");
return source;
}
// 遍歷全部依賴的 module,每一個 module 經過使用 moduleTemplate.render 方法進行渲染獲得最終這個 module 須要輸出的內容
/** @type {{id: string|number, source: Source|string}[]} */
const allModules = modules.map(module => {
return {
id: module.id, // 每一個 module 的 id
source: moduleTemplate.render(module, dependencyTemplates, { // 渲染每一個 module
chunk
})
};
});
// 判斷這個 chunk 所依賴的 module 的 id 是否存在邊界值,若是存在邊界值,那麼這些 modules 將會放置於一個以邊界數組最大最小值做爲索引的數組當中;
// 若是沒有邊界值,那麼 modules 將會被放置於一個以 module.id 做爲 key,module 實際渲染內容做爲 value 的對象當中
const bounds = Template.getModulesArrayBounds(allModules);
if (bounds) {
// Render a spare array
const minId = bounds[0];
const maxId = bounds[1];
if (minId !== 0) {
source.add(`Array(${minId}).concat(`);
}
source.add("[\n");
/** @type {Map<string|number, {id: string|number, source: Source|string}>} */
const modules = new Map();
for (const module of allModules) {
modules.set(module.id, module);
}
for (let idx = minId; idx <= maxId; idx++) {
const module = modules.get(idx);
if (idx !== minId) {
source.add(",\n");
}
source.add(`/* ${idx} */`);
if (module) {
source.add("\n");
source.add(module.source); // 添加每一個 module 最終輸出的代碼
}
}
source.add("\n" + prefix + "]");
if (minId !== 0) {
source.add(")");
}
} else {
// Render an object
source.add("{\n");
allModules.sort(stringifyIdSortPredicate).forEach((module, idx) => {
if (idx !== 0) {
source.add(",\n");
}
source.add(`\n/***/ ${JSON.stringify(module.id)}:\n`);
source.add(module.source);
});
source.add(`\n\n${prefix}}`);
}
return source
}
}
複製代碼
咱們來看下在 chunk 渲染過程當中,如何對每一個所依賴的 module 進行渲染拼接代碼的,即在 Template 類當中提供的 renderChunkModules 方法中,遍歷這個 chunk 當中全部依賴的 module 過程當中,調用 moduleTemplate.render 完成每一個 module 的代碼渲染拼接工做。
首先咱們來了解下3個和輸出 module 代碼相關的模板:
RuntimeTemplate
顧名思義,這個模板類主要是提供了和 module 運行時相關的代碼輸出方法,例如你的 module 使用的是 esModule 類型,那麼導出的代碼模塊會帶有__esModule
標識,而經過 import 語法引入的外部模塊都會經過/* harmony import */
註釋來進行標識。
dependencyTemplates
dependencyTemplates 模板數組主要是保存了每一個 module 不一樣依賴的模板,在輸出最終代碼的時候會經過 dependencyTemplates 來完成模板代碼的替換工做。
ModuleTemplate
ModuleTemplate 模板類主要是對外暴露了 render 方法,經過調用 moduleTemplate 實例上的 render 方法,即完成每一個 module 的代碼渲染工做,這也是每一個 module 輸出最終代碼的入口方法。
如今咱們從 ModuleTemplate 模板開始:
// ModuleTemplate.js
module.exports = class ModuleTemplate extends Tapable {
constructor(runtimeTemplate, type) {
this.runtimeTemplate = runtimeTemplate
this.type = type
this.hooks = {
content: new SyncWaterfallHook([]),
module: new SyncWaterfallHook([]),
render: new SyncWaterfallHook([]),
package: new SyncWaterfallHook([]),
hash: new SyncHook([])
}
}
render(module, dependencyTemplates, options) {
try {
// replaceSource
const moduleSource = module.source(
dependencyTemplates,
this.runtimeTemplate,
this.type
);
const moduleSourcePostContent = this.hooks.content.call(
moduleSource,
module,
options,
dependencyTemplates
);
const moduleSourcePostModule = this.hooks.module.call(
moduleSourcePostContent,
module,
options,
dependencyTemplates
);
// 添加編譯 module 外層包裹的函數
const moduleSourcePostRender = this.hooks.render.call(
moduleSourcePostModule,
module,
options,
dependencyTemplates
);
return this.hooks.package.call(
moduleSourcePostRender,
module,
options,
dependencyTemplates
);
} catch (e) {
e.message = `${module.identifier()}\n${e.message}`;
throw e;
}
}
}
複製代碼
首先調用 module.source 方法,傳入 dependencyTemplates, runtimeTemplate,以及渲染類型 type(默認爲 javascript)。 module.source 方法執行完成後會返回一個 ReplaceSource 類,其中包含源碼和一個 replacement 數組。其中 replacement 數組中保存了對源碼處理操做。
FunctionModuleTemplatePlugin 會在 render hook 階段被調用,將咱們寫在文件中的代碼封裝爲一個函數
children:[
'/***/ (function(module, __webpack_exports__, __webpack_require__) {↵↵'
'"use strict";↵'
CachedSource // 1,2 步驟中獲得的結果
'↵↵/***/ })'
]
複製代碼
最終打包,觸發 package hook。FunctionModuleTemplatePlugin 會在這個階段爲咱們的最終代碼增長一些註釋,方便咱們查看代碼。
source——代碼裝換 如今咱們要深刻 module.source,即在每一個 module 上定義的 source 方法:
// NormalModule.js
class NormalModule extends Module {
...
source(dependencyTemplates, runtimeTemplate, type = "javascript") {
const hashDigest = this.getHashDigest(dependencyTemplates);
const cacheEntry = this._cachedSources.get(type);
if (cacheEntry !== undefined && cacheEntry.hash === hashDigest) {
// We can reuse the cached source
return cacheEntry.source;
}
// JavascriptGenerator
const source = this.generator.generate(
this,
dependencyTemplates, // 依賴的模板
runtimeTemplate,
type
);
const cachedSource = new CachedSource(source);
this._cachedSources.set(type, {
source: cachedSource,
hash: hashDigest
});
return cachedSource;
}
...
}
複製代碼
咱們看到在 module.source 方法內部調用了 generator.generate 方法,那麼這個 generator 又是從哪裏來的呢?事實上在經過 NormalModuleFactory 建立 NormalModule 的過程即完成了 generator 的建立,以用來生成每一個 module 最終渲染的 javascript 代碼。
因此 module.source 中 generator.generate 的執行代碼在 JavascriptGenerator.js 中
// JavascriptGenerator.js
class JavascriptGenerator {
generate(module, dependencyTemplates, runtimeTemplate) {
const originalSource = module.originalSource(); // 獲取這個 module 的 originSource
if (!originalSource) {
return new RawSource("throw new Error('No source available');");
}
// 建立一個 ReplaceSource 類型的 source 實例
const source = new ReplaceSource(originalSource);
this.sourceBlock(
module,
module,
[],
dependencyTemplates,
source,
runtimeTemplate
);
return source;
}
sourceBlock(
module,
block,
availableVars,
dependencyTemplates,
source,
runtimeTemplate
) {
// 處理這個 module 的 dependency 的渲染模板內容
for (const dependency of block.dependencies) {
this.sourceDependency(
dependency,
dependencyTemplates,
source,
runtimeTemplate
);
}
...
for (const childBlock of block.blocks) {
this.sourceBlock(
module,
childBlock,
availableVars.concat(vars),
dependencyTemplates,
source,
runtimeTemplate
);
}
}
// 獲取對應的 template 方法並執行,完成依賴的渲染工做
sourceDependency(dependency, dependencyTemplates, source, runtimeTemplate) {
const template = dependencyTemplates.get(dependency.constructor);
if (!template) {
throw new Error(
"No template for dependency: " + dependency.constructor.name
);
}
template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
}
}
複製代碼
在 JavascriptGenerator 提供的 generate 方法主要流程以下:
完成對源碼轉換的大部分操做在上面第二步中,這個過程就是調用每一個依賴對應的 dependencyTemplate 的 apply 方法。webpack 中全部的 xxDependency 類中會有一個靜態的 Template 方法,這個方法即是該 dependency 對應的生成最終代碼的方法(相關的可參考dependencyTemplates)。
咱們用下面一個簡單的例子,詳細看一下源碼轉換的過程。demo 以下
// 打包的入口 main.js
import { A } from './a.js'
console.log(A)
// a.ja
export const A = 'a'
export const B = 'B'
複製代碼
前面幾篇文章咱們介紹過,通過 webpack 中文件 make 的過程以後會獲得全部文件的 module,同時每一個文件中 import/export 會轉化爲一個 dependency。以下
因此 main.js 模塊,在執行 generate 方法中 for (const dependency of block.dependencies)
這一步時,會遇到有 5 類 dependency,一個一個來看
HarmonyCompatibilityDependency 它的 Template 代碼以下:
HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate {
apply(dep, source, runtime) {
const usedExports = dep.originModule.usedExports;
if (usedExports !== false && !Array.isArray(usedExports)) {
const content = runtime.defineEsModuleFlagStatement({
exportsArgument: dep.originModule.exportsArgument
});
source.insert(-10, content);
}
}
};
複製代碼
這裏 usedExport
變量中保存了 module 中被其餘 module 使用過的 export。對於每一個 chunk 的入口模塊來講比較特殊,這個值會被直接賦爲 true,而對於有 export default
語句的模塊來講,這個值是 default
,這兩種狀況在這裏都生成一句以下的代碼:
__webpack_require__.r(__webpack_exports__);
複製代碼
__webpack_require__.r
方法會爲 __webpack_exports__
對象增長一個 __esModule
屬性,將其標識爲一個 es module。webpack 會將咱們代碼中暴露出的 export 都轉化爲 module.exports 的屬性,對於有 __esModule
標識的模塊,當咱們經過 import x from 'xx'
引入時,x 是 module.exports.default
的內容,不然的話會被當成爲 CommonJs module 的規範,引入的是整個 module.exports。
HarmonyInitDependency 和 HarmonyCompatibilityDependency 依賴一塊兒出現的還有 HarmonyInitDependency。這個 Template 方法中會遍歷 module 下的全部依賴,若是依賴有 harmonyInit,則會執行。
for (const dependency of module.dependencies) {
const template = dependencyTemplates.get(dependency.constructor);
if (
template &&
typeof template.harmonyInit === "function" &&
typeof template.getHarmonyInitOrder === "function"
) {
//...
}
}
複製代碼
harmonyInit 方法這裏須要解釋一下:當咱們在源碼中使用到 import 和 export 時 , webpack 爲處理這些引用的邏輯,須要在咱們源碼的最開始有針對性的插入一些代碼,能夠認爲是初始化的代碼。例如咱們在 webpack 生成的代碼中常見到的
__webpack_exports__["default"] = /*...*/
或者
__webpack_require__.d(__webpack_exports__, "A", function() { return A; });
複製代碼
這些代碼的生成邏輯就保存在對應的 dependency 的 harmonyInit 方法中,而在處理 HarmonyInitDependency 階段會被執行。到這裏,你也會更加明白咱們曾在講 module 生成中 parse 階段的時候提到過,若是檢測到源碼中有 import 或者 export 的時候,會增長一個 HarmonyInitDependency 依賴的緣由。
main.js 的 HarmonyImportSideEffectDependency 和 HarmonyImportSpecifierDependency 中的 harmonyInit
方法,都會在這裏被調用,分別生成下面的代碼
"/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);↵" // 對應:import { A } from './a.js'
複製代碼
ConstDepedency 和 HarmonyImportSideEffectDependency import { A } from './a.js'
爲例:
/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
HarmonyImportSpecifierDependency 在處理 console.log(A)
中的 A 的時候被加入這個依賴,在這裏會生成下面一個變量名稱:
_a_js__WEBPACK_IMPORTED_MODULE_0__[/* A */ "a"]
複製代碼
並用這個名稱替換源碼中的 A,最終將 A 對應到 a.js 中暴露出來的 A 變量上。
當 main.js 全部依賴處理完以後,會獲得下面的數據
//ReplaceSource
replacements:[
[-10, -11, "__webpack_require__.r(__webpack_exports__);↵", 0],
[-1, -2, "/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);↵", 1],
[0, 25, "", 2], // 0-25 對應源碼:import { A } from './a.js'
[39, 39, "_a_js__WEBPACK_IMPORTED_MODULE_0__[/* A */ "a"]", 7], // 84-84 對應源碼:console.log(A) 中的 A
]
複製代碼
對照一下源碼,把源碼中對應位置的代碼替換成 ReplaceSource 中的內容:
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ "./demo01/a.js");
console.log(_a_js__WEBPACK_IMPORTED_MODULE_0__["A"])
複製代碼
進行 webpack 處理後,咱們的 main.js 就會變成上面這樣的代碼。
下面咱們再看一下 a.js:
a.js 中有 4 類 dependency:HarmonyCompatibilityDependency、HarmonyInitDependency、HarmonyExportHeaderDependency、HarmonyExportSpecifierDependency。
HarmonyInitDependency 前面在 main.js 中已經介紹過這個 dependency,它會遍歷全部的 dependency。在這個過程當中 a.js 代碼 export const A
和 export const B
所對應的 HarmonyExportSpecifierDependency 中的 template.harmonyInit
方法將會在這時執行,而後獲得下面兩句
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return A; });
/* unused harmony export B */
複製代碼
這樣在最終的代碼中 const A
就被註冊到了 a.js 對應的 module 的 exports 上。而 const B
因爲沒被其餘代碼所引用,因此會被 webpack 的 tree-shaking 邏輯探測到,在這裏只是轉化爲一句註釋
HarmonyExportHeaderDependency
它對源碼的處理很簡單,代碼以下:
HarmonyExportHeaderDependency.Template = class HarmonyExportDependencyTemplate {
apply(dep, source) {
const content = "";
const replaceUntil = dep.range
? dep.range[0] - 1
: dep.rangeStatement[1] - 1;
source.replace(dep.rangeStatement[0], replaceUntil, content);
}
};
複製代碼
因爲前面在 HarmonyInitDependency 的邏輯中已經完成了對 export 變量的處理,因此這裏將 export const A = 'a'
和 export const B = 'b'
語句中的 export
替換爲空字符串。
HarmonyExportSpecifierDependency 自己的 Template.apply 是空函數,因此這個依賴主要在 HarmonyInitDependency 時發揮做用。
完成對 a.js 中全部 dependency 的處理後,會獲得下面的一個結果:
// ReplaceSource
children:[
[-1, -2, "/* harmony export (binding) */ __webpack_require__…", "a", function() { return A; });↵", 0],
[-1, -2, "/* unused harmony export B */↵", 1],
[0, 6, "", 2], // 0-6 對應源碼:'export '
[21, 27, "", 3], // 21-27 對應源碼:'export '
]
複製代碼
一樣的,若是把 a.js 源碼中對應位置的代碼替換一下,a.js 的源碼就變成了下面這樣:
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "A", function() { return A; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "B", function() { return B; });
const A = 'a'
const B = 'B'
複製代碼
generate.render 這一步完成了對源碼內容的轉換,以後回到 ModuleTemplate.js 的 render 方法中,繼續將獨立的 module 整合成最終的可執行函數。
content、module、render、package——代碼包裹 generate.render 完成以後,接下來觸發 hooks.content 、 hooks.module 這2個鉤子函數,主要是用來對於 module 完成依賴代碼替換後的代碼處理工做,開發者能夠經過註冊相關的鉤子完成對於 module 代碼的改造,由於這個時候獲得代碼尚未在外層包裹 webpack runtime 的代碼,所以在這2個鉤子函數對於 module 代碼作改造最合適。
當上面2個 hooks 都執行完後,開始觸發 hooks.render 鉤子:
// FunctionModuleTemplatePlugin.js
class FunctionModuleTemplatePlugin {
apply(moduleTemplate) {
moduleTemplate.hooks.render.tap(
"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")) {
args.push(module.exportsArgument); // __webpack_exports__
if (module.hasDependencies(d => d.requireWebpackRequire !== false)) {
// 判斷這個模塊內部是否使用了被引入的其餘模塊,若是有的話,那麼就須要加入 __webpack_require__
args.push("__webpack_require__"); // __webpack_require__
}
} else if (module.type && module.type.startsWith("json")) {
// no additional arguments needed
} else {
args.push(module.exportsArgument, "__webpack_require__");
}
source.add("/***/ (function(" + args.join(", ") + ") {\n\n");
if (module.buildInfo.strict) source.add('"use strict";\n'); // harmony module 會使用 use strict; 嚴格模式
// 將 moduleSource 代碼包裹至這個函數當中
source.add(moduleSource);
source.add("\n\n/***/ })");
return source;
}
)
}
}
複製代碼
這個鉤子函數主要的工做就是完成對上面已經完成的 module 代碼進行一層包裹,包裹的內容主要是 webpack 自身的一套模塊加載系統,包括模塊導入,導出等,每一個 module 代碼最終生成的形式爲:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// module 最終生成的代碼被包裹在這個函數內部
// __webpack_exports__ / __webpack_require__ 相關的功能能夠閱讀 webpack runtime bootstrap 代碼去了解
/***/ })
複製代碼
當 hooks.render 鉤子觸發後完成 module 代碼的包裹後,觸發 hooks.package 鉤子,這個主要是用於在 module 代碼中添加註釋的功能,就不展開說了,具體查閱FunctionModuleTemplatePlugin.js
。
到這裏就完成了對於一個 module 的代碼的渲染工做,最終在每一個 chunk 當中的每個 module 代碼也就是在今生成。
module 代碼生成以後便返回到上文JavascriptModulePlugin.renderJavascript
方法當中,繼續後面生成每一個 chunk 最終代碼的過程當中了。
整合成可執行函數 接下來觸發 chunkTemplate.hooks.modules 鉤子函數,若是你須要對於 chunk 代碼有所修改,那麼在這裏能夠經過 plugin 註冊 hooks.modules 鉤子函數來完成相關的工做。這個鉤子觸發後,繼續觸發 chunkTemplate.hooks.render 鉤子函數,在JsonpChunkTemplatePlugin
這個插件當中註冊了對應的鉤子函數:
class JsonpChunkTemplatePlugin {
/** * @param {ChunkTemplate} chunkTemplate the chunk template * @returns {void} */
apply(chunkTemplate) {
chunkTemplate.hooks.render.tap(
"JsonpChunkTemplatePlugin",
(modules, chunk) => {
const jsonpFunction = chunkTemplate.outputOptions.jsonpFunction;
const globalObject = chunkTemplate.outputOptions.globalObject;
const source = new ConcatSource();
const prefetchChunks = chunk.getChildIdsByOrders().prefetch;
source.add(
`(${globalObject}[${JSON.stringify( jsonpFunction )}] = ${globalObject}[${JSON.stringify( jsonpFunction )}] || []).push([${JSON.stringify(chunk.ids)},`
);
source.add(modules);
const entries = getEntryInfo(chunk);
if (entries.length > 0) {
source.add(`,${JSON.stringify(entries)}`);
} else if (prefetchChunks && prefetchChunks.length) {
source.add(`,0`);
}
if (prefetchChunks && prefetchChunks.length) {
source.add(`,${JSON.stringify(prefetchChunks)}`);
}
source.add("])");
return source;
}
)
}
}
複製代碼
這個鉤子函數主要完成的工做就是將這個 chunk 當中全部已經渲染好的 module 的代碼再一次進行包裹組裝,生成這個 chunk 最終的代碼,也就是最終會被寫入到文件當中的代碼。與此相關的是 JsonpTemplatePlugin,這個插件內部註冊了 chunkTemplate.hooks.render 的鉤子函數,在這個函數裏面完成了 chunk 代碼外層的包裹工做。咱們來看個經過這個鉤子函數處理後生成的 chunk 代碼的例子:
// a.js
import { add } from './add.js'
add(1, 2)
-------
// 在 webpack config 配置環節將 webpack runtime bootstrap 代碼單獨打包成一個 chunk,那麼最終 a.js 所在的 chunk輸出的代碼是:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// module id 爲0的 module 輸出代碼,即 a.js 最終輸出的代碼
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// module id 爲1的 module 輸出代碼,即 add.js 最終輸出的代碼
/***/ })
],[[0,0]]]);
複製代碼
到此爲止,有關 renderJavascript 方法的流程已經梳理完畢了,這也是非 runtime bootstrap chunk 代碼最終的輸出時的處理流程。
以上就是有關 chunk 代碼生成的流程分析即 createChunkAssets,當這個流程進行完後,全部須要生成到文件的 chunk 最終會保存至 compilation 的一個 key/value 結構當中:
compilation.assets = {
[輸出文件路徑名]: ConcatSource(最終 chunk 輸出的代碼)
}
複製代碼
接下來針對保存在內容當中的這些 assets 資源作相關的優化工做,同時會暴露出一些鉤子供開發者對於這些資源作相關的操做,例如可使用 compilation.optimizeChunkAssets 鉤子函數去往 chunk 內添加代碼等等,有關這些鉤子的說明具體能夠查閱webpack文檔上有關assets優化的內容。
經歷了上面全部的階段以後,全部的最終代碼信息已經保存在了 Compilation 的 assets 中。而後代碼片斷會被拼合起來,而且上一步 generator.generate 獲得的 ReplaceSource 結果中,會遍歷 replacement 中的操做,按照要替換的源碼的前後位置(同一位置的話,按照 replacement 中的最後一個參數優先級前後)來一一對源碼進行替換,而後代碼最終代碼。 webpack 配置中能夠配置一些優化,例如壓縮,因此在獲得代碼後會進行一些優化。 當 assets 資源相關的優化工做結束後,seal 階段也就結束了。這時候執行 seal 函數接受到 callback,進入到 webpack 後續的流程。具體內容可查閱 compiler 編譯器對象提供的 run 方法。這個 callback 方法內容會執行到 compiler.emitAssets 方法:
// Compiler.js
class Compiler extends Tapable {
...
emitAssets(compilation, callback) {
let outputPath;
const emitFiles = err => {
if (err) return callback(err);
asyncLib.forEach(
compilation.assets,
(source, 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
);
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, 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);
this.hooks.afterEmit.callAsync(compilation, err => {
if (err) return callback(err);
return callback();
});
}
);
};
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
}
...
}
複製代碼
在這個方法當中首先觸發 hooks.emit 鉤子函數,即將進行寫文件的流程。接下來開始建立目標輸出文件夾,並執行 emitFiles 方法,將內存當中保存的 assets 資源輸出到目標文件夾當中,這樣就完成了內存中保存的 chunk 代碼寫入至最終的文件。最終有關 emit assets 輸出最終 chunk 文件的流程圖見下: