原文地址:webpack筆記——在html-webpack-plugin外掛中提供給其它外掛是使用的hooks
最近在這段時間剛好在溫故下webpack原始碼,webpack5都出來了,4還不再學習下? 這次順便學習下webpack的常用外掛html-webpack-plugin。 發現這個外掛裡面還額外加入了自己的hooks,方便其它外掛來實現自己的功能,不得不說作者真是個好人。
部分程式碼如下
// node_modules/html-webpack-plugin/index.js
app(compiler) {
// setup hooks for webpack 4
if (compiler.hooks) {
compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', compilation => {
const SyncWaterfallHook = require('tapable').SyncWaterfallHook;
const AsyncSeriesWaterfallHook = require('tapable').AsyncSeriesWaterfallHook;
compilation.hooks.htmlWebpackPluginAlterChunks = new SyncWaterfallHook(['chunks', 'objectWithPluginRef']);
compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration = new AsyncSeriesWaterfallHook(['pluginArgs']);
compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
compilation.hooks.htmlWebpackPluginAlterAssetTags = new AsyncSeriesWaterfallHook(['pluginArgs']);
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
compilation.hooks.htmlWebpackPluginAfterEmit = new AsyncSeriesWaterfallHook(['pluginArgs']);
});
}
...
// Backwards compatible version of: compiler.plugin.emit.tapAsync()
(compiler.hooks ? compiler.hooks.emit.tapAsync.bind(compiler.hooks.emit, 'HtmlWebpackPlugin') : compiler.plugin.bind(compiler, 'emit'))((compilation, callback) => {
const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
// Get chunks info as json
// Note: we're excluding stuff that we don't need to improve toJson serialization speed.
const chunkOnlyConfig = {
assets: false,
cached: false,
children: false,
chunks: true,
chunkModules: false,
chunkOrigins: false,
errorDetails: false,
hash: false,
modules: false,
reasons: false,
source: false,
timings: false,
version: false
};
const allChunks = compilation.getStats().toJson(chunkOnlyConfig).chunks;
// Filter chunks (options.chunks and options.excludeCHunks)
let chunks = self.filterChunks(allChunks, self.options.chunks, self.options.excludeChunks);
// Sort chunks
chunks = self.sortChunks(chunks, self.options.chunksSortMode, compilation);
// Let plugins alter the chunks and the chunk sorting
if (compilation.hooks) {
chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });
} else {
// Before Webpack 4
chunks = compilation.applyPluginsWaterfall('html-webpack-plugin-alter-chunks', chunks, { plugin: self });
}
// Get assets
const assets = self.htmlWebpackPluginAssets(compilation, chunks);
// If this is a hot update compilation, move on!
// This solves a problem where an `index.html` file is generated for hot-update js files
// It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
if (self.isHotUpdateCompilation(assets)) {
return callback();
}
// If the template and the assets did not change we don't have to emit the html
const assetJson = JSON.stringify(self.getAssetFiles(assets));
if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
return callback();
} else {
self.assetJson = assetJson;
}
Promise.resolve()
// Favicon
.then(() => {
if (self.options.favicon) {
return self.addFileToAssets(self.options.favicon, compilation)
.then(faviconBasename => {
let publicPath = compilation.mainTemplate.getPublicPath({hash: compilation.hash}) || '';
if (publicPath && publicPath.substr(-1) !== '/') {
publicPath += '/';
}
assets.favicon = publicPath + faviconBasename;
});
}
})
// Wait for the compilation to finish
.then(() => compilationPromise)
.then(compiledTemplate => {
// Allow to use a custom function / string instead
if (self.options.templateContent !== undefined) {
return self.options.templateContent;
}
// Once everything is compiled evaluate the html factory
// and replace it with its content
return self.evaluateCompilationResult(compilation, compiledTemplate);
})
// Allow plugins to make changes to the assets before invoking the template
// This only makes sense to use if `inject` is `false`
.then(compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
assets: assets,
outputName: self.childCompilationOutputName,
plugin: self
})
.then(() => compilationResult))
// Execute the template
.then(compilationResult => typeof compilationResult !== 'function'
? compilationResult
: self.executeTemplate(compilationResult, chunks, assets, compilation))
// Allow plugins to change the html before assets are injected
.then(html => {
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-processing', true, pluginArgs);
})
.then(result => {
const html = result.html;
const assets = result.assets;
// Prepare script and link tags
const assetTags = self.generateHtmlTags(assets);
const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, chunks: chunks, outputName: self.childCompilationOutputName};
// Allow plugins to change the assetTag definitions
return applyPluginsAsyncWaterfall('html-webpack-plugin-alter-asset-tags', true, pluginArgs)
.then(result => self.postProcessHtml(html, assets, { body: result.body, head: result.head })
.then(html => _.extend(result, {html: html, assets: assets})));
})
// Allow plugins to change the html after assets are injected
.then(result => {
const html = result.html;
const assets = result.assets;
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
return applyPluginsAsyncWaterfall('html-webpack-plugin-after-html-processing', true, pluginArgs)
.then(result => result.html);
})
.catch(err => {
// In case anything went wrong the promise is resolved
// with the error message and an error is logged
compilation.errors.push(prettyError(err, compiler.context).toString());
// Prevent caching
self.hash = null;
return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
})
.then(html => {
// Replace the compilation result with the evaluated html code
compilation.assets[self.childCompilationOutputName] = {
source: () => html,
size: () => html.length
};
})
.then(() => applyPluginsAsyncWaterfall('html-webpack-plugin-after-emit', false, {
html: compilation.assets[self.childCompilationOutputName],
outputName: self.childCompilationOutputName,
plugin: self
}).catch(err => {
console.error(err);
return null;
}).then(() => null))
// Let webpack continue with it
.then(() => {
callback();
});
});
}
複製程式碼
我在node_modules裡面搜了下,還真有一些外掛使用這些hooks呢
在百度上搜了下,還有朋友提過這樣的問題html-webpack-plugin中定義的鉤子在什麼時候被call那我就帶著這個目的看下html-webpack-plugin的原始碼裡面是怎麼call的。
首先我們看到在compiler的compilation的hooks裡面加入了html-webpack-plugin自己的6個hooks,所以我們在使用這些hooks需要注意時機,得等加入後才能使用。 這6個hooks在compiler的emit時期呼叫,這一點怎麼看出來的呢? 我們往下看還真能看到這個
chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });
複製程式碼
這個比較明顯,直接呼叫的,但是其它5個hooks呢?它們就沒有這麼容易看出來了。
我們繼續往下面看,發現有個html-webpack-plugin-before-html-generation,這個是不是跟compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration
很像,沒錯,它只是htmlWebpackPluginBeforeHtmlGeneration
的另一種命名書寫方式而已。
在html-webpack-plugin是利用trainCaseToCamelCase
將html-webpack-plugin-before-html-generation
轉為htmlWebpackPluginBeforeHtmlGeneration
的,先忽略這些細枝末節,我們繼續在emit這個hooks裡面看看它的自定義外掛的呼叫流程。
apply(compiler) {
...
const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
...
.then(compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
assets: assets,
outputName: self.childCompilationOutputName,
plugin: self
})
...
}
applyPluginsAsyncWaterfall (compilation) {
if (compilation.hooks) {
return (eventName, requiresResult, pluginArgs) => {
const ccEventName = trainCaseToCamelCase(eventName);
if (!compilation.hooks[ccEventName]) {
compilation.errors.push(
new Error('No hook found for ' + eventName)
);
}
return compilation.hooks[ccEventName].promise(pluginArgs);
};
}
複製程式碼
上面的applyPluginsAsyncWaterfall
常量就是支援三個引數的函式,利用閉包,保留了compilation
的引用,執行applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {})
的時候,compilation.hooks[ccEventName].promise(pluginArgs)
就執行了,我們上面的自定義的hooks的回撥就得到了呼叫。通過前面的webpack分析文章中我們知道,這些回撥是放在this._taps陣列裡面,執行這些回撥的方式有三種,call
、promise
、callAsync
,我們不能老是侷限於最常用的call
方法,另外的5個hooks本身就是AsyncSeriesWaterfallHook
型別的,所以用promise
呼叫合情合理。
前面網友提的問題html-webpack-plugin中定義的鉤子在什麼時候被call也就有了答案。
html-webpack-plugin的核心功能就是通過compilation.getStats()
獲取到chunks。
const allChunks = compilation.getStats().toJson(chunkOnlyConfig).chunks;
// Filter chunks (options.chunks and options.excludeCHunks)
let chunks = self.filterChunks(allChunks, self.options.chunks, self.options.excludeChunks);
// Sort chunks
chunks = self.sortChunks(chunks, self.options.chunksSortMode, compilation);
// Let plugins alter the chunks and the chunk sorting
if (compilation.hooks) {
chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });
} else {
// Before Webpack 4
chunks = compilation.applyPluginsWaterfall('html-webpack-plugin-alter-chunks', chunks, { plugin: self });
}
// Get assets
const assets = self.htmlWebpackPluginAssets(compilation, chunks);
複製程式碼
在一切準備就緒後,再執行自己的自定義hooks。那需要準備就緒的是什麼呢?
- 上面的chunks
- 確保外掛傳入的template內容已經編譯就緒
其中用一個變數儲存了compiler的make裡面的一個promise
compilationPromise = childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation)
.catch(err => {
compilation.errors.push(prettyError(err, compiler.context).toString());
return {
content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR',
outputName: self.options.filename
};
})
.then(compilationResult => {
// If the compilation change didnt change the cache is valid
isCompilationCached = compilationResult.hash && self.childCompilerHash === compilationResult.hash;
self.childCompilerHash = compilationResult.hash;
self.childCompilationOutputName = compilationResult.outputName;
callback();
return compilationResult.content;
});
複製程式碼
在childCompiler.compileTemplate
裡面建立了子compiler,用它來編譯我們的傳入的template
(也就是準備當成模板的那個html檔案)內容
// node_modules/html-webpack-plugin/lib/compiler.js
module.exports.compileTemplate = function compileTemplate (template, context, outputFilename, compilation) {
...
const childCompiler = compilation.createChildCompiler(compilerName, outputOptions);
....
return new Promise((resolve, reject) => {
childCompiler.runAsChild((err, entries, childCompilation) => {})
...
resolve({
// Hash of the template entry point
hash: entries[0].hash,
// Output name
outputName: outputName,
// Compiled code
content: childCompilation.assets[outputName].source()
});
})
}
複製程式碼
獲取完template的編譯內容,也就是返回的compilationResult.content,後面它被賦值給compiledTemplate,它的內容大致如下
還有個重要步驟。apply(compiler) {
...
.then(compiledTemplate => {
// Allow to use a custom function / string instead
if (self.options.templateContent !== undefined) {
return self.options.templateContent;
}
// Once everything is compiled evaluate the html factory
// and replace it with its content
return self.evaluateCompilationResult(compilation, compiledTemplate);
})
.then(compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
assets: assets,
outputName: self.childCompilationOutputName,
plugin: self
})
.then(() => compilationResult))
// Execute the template
.then(compilationResult => typeof compilationResult !== 'function'
? compilationResult
: self.executeTemplate(compilationResult, chunks, assets, compilation))
// Allow plugins to change the html before assets are injected
.then(html => {
const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-processing', true, pluginArgs);
})
...
}
evaluateCompilationResult (compilation, source) {
if (!source) {
return Promise.reject('The child compilation didn\'t provide a result');
}
// The LibraryTemplatePlugin stores the template result in a local variable.
// To extract the result during the evaluation this part has to be removed.
source = source.replace('var HTML_WEBPACK_PLUGIN_RESULT =', '');
const template = this.options.template.replace(/^.+!/, '').replace(/\?.+$/, '');
const vmContext = vm.createContext(_.extend({HTML_WEBPACK_PLUGIN: true, require: require}, global));
const vmScript = new vm.Script(source, {filename: template});
// Evaluate code and cast to string
let newSource;
try {
newSource = vmScript.runInContext(vmContext);
} catch (e) {
return Promise.reject(e);
}
if (typeof newSource === 'object' && newSource.__esModule && newSource.default) {
newSource = newSource.default;
}
return typeof newSource === 'string' || typeof newSource === 'function'
? Promise.resolve(newSource)
: Promise.reject('The loader "' + this.options.template + '" didn\'t return html.');
}
複製程式碼
經過vm
的一頓操作之後返回了newSource
,這是一個函式,在後續的Promise裡面叫compilationResult
,它可以生成出模板內容的字串。
仔細觀察可以看到compilationResult
並沒有傳遞給自定義鉤子html-webpack-plugin-before-html-generation來使用,在html-webpack-plugin-before-html-processing鉤子之前執行self.executeTemplate(compilationResult, chunks, assets, compilation))
生成了對應的html內容。
小插曲
在看上面的幾個自定義鉤子執行時,我發現在html-webpack-plugin-before-html-generation之前compilationResult
(下面1號then的入參)是self.evaluateCompilationResult(compilation, compiledTemplate)
返回的函式,但是怎麼在經過html-webpack-plugin-before-html-generation之後,後面的準備使用html-webpack-plugin-before-html-processing的then方法(下面的3號the)裡面入參compilationResult
依然還是那個函式呢?
我在自己測試使用html-webpack-plugin-before-html-processing鉤子時是這麼使用的
compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tap('test', (data) => {
console.log(' data-> ', data);
})
複製程式碼
對,啥也沒幹,就一個console而已。 在呼叫對應的回撥函式時,是這麼進行的
(function anonymous(pluginArgs
) {
"use strict";
return new Promise((_resolve, _reject) => {
var _sync = true;
function _error(_err) {
if(_sync)
_resolve(Promise.resolve().then(() => { throw _err; }));
else
_reject(_err);
};
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _hasError0 = false;
try {
var _result0 = _fn0(pluginArgs);
} catch(_err) {
_hasError0 = true;
_error(_err);
}
if(!_hasError0) {
if(_result0 !== undefined) {
pluginArgs = _result0;
}
_resolve(pluginArgs);
}
_sync = false;
});
})
複製程式碼
傳入給我的回撥函式裡面的就是這個pluginArgs
,由於我的回撥函式裡面,未對入參進行過任何修改,並且還返回的undefined
,所有compilation.hooks[ccEventName].promise(pluginArgs)
返回的這個Promise的值還是pluginArgs,而並非之前的
compilationResult`那個函式啊
經過認真排查發現,原來是這一部分Promise回撥太多,容易眼花。原來1號then裡面的2號then是這樣寫的,並非直接鏈式寫的1號--applyPluginsAsyncWaterfall--2號--3號
,而是1號--(applyPluginsAsyncWaterfall--2號)--3號
.then(// 1
compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
assets: assets,
outputName: self.childCompilationOutputName,
plugin: self
})
.then( // 2
() => compilationResult
)
)
// Execute the template
.then(compilationResult => typeof compilationResult !== 'function' //3
? compilationResult
: self.executeTemplate(compilationResult, chunks, assets, compilation)
)
複製程式碼
我將排版調整下,這樣看的更清楚了,這樣的話compilationResult
的結果當然沒有丟失。