webpack笔记——在html-webpack-plugin插件中提供给其它插件是使用的hooks
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 ifinject
isfalse
.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
的结果当然没有丢失。
- 分类:
- Web前端
相关文章
回顾下跨域解决方案http-proxy-middleware
我们在React(或Vue)项目本地开发过程中很容易由前端自己解决跨域问题,这里面就用到的是插件 http-proxy-middleware ,它并不是webpack独享的插件,而是一个通用插件,它 阅读更多…
webpack反向代理proxyTable设置
目前各大打包工具在本地开发时都是使用的http-proxy-middleware插件 具体以vue为例,反向代理配置的就是proxyTable proxyTable: { 'ht 阅读更多…
从vuecli3学习webpack记录(二)webpack分析
上一篇里面讲到运行 npm run serve 时运行的是 serveice.run(comand, args, rawArgv) 并且提到它提示返回的是一个promise,所以后面还接着 .cat 阅读更多…
从vuecli3学习webpack记录(零)整体流程
今天看了下自己之前写的从vuecli3学习webpack记录系列,感觉自己居然没有在一开始的时候把vuecli的 npm run serve 的整体流程在一篇文章里面完整的讲完,可能是因为打字打的手 阅读更多…
用webpack的require.context() 简化你的代码
随着我们的项目越来越大,平时的常见用操作就会觉得很‘麻烦’了,比如每次要添加新的路由, vuex里面添加新的module等 { name: 'moduleN', 阅读更多…
怎么调试Webpack+React项目,报错basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")怎么办
今天在WebStorm上Windows上准备调试一个React项目,就出现了这样的报错。 Node Parameters里面写的是webpack-dev-server的执行文件 .\node_mod 阅读更多…