webpack筆記——在html-webpack-plugin插件中提供給其它插件是使用的hooks
最近在這段時間剛好在溫故下webpack源碼,webpack5都出來了,4還不再學習下?
這次順便學習下webpack的常用插件html-webpack-plugin。
發現這個插件裏面還額外加入了自己的hooks,方便其它插件來實現自己的功能,不得不說作者真是個好人。
部分代碼如下
<code>// 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 </code><code>index.html</code> 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 <code>inject</code> is <code>false</code> .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 也就有了答案。
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script> <script> (adsbygoogle = window.adsbygoogle || []).push({ google_ad_client: "ca-pub-3013839362871866", enable_page_level_ads: true }); </script>
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
的結果當然沒有丟失。