html-webpack-plugin插件可以将现有的资源添加进html文件,同时也可以监听该插件的一些hooks进行自定义操作,接下来我们看看具体实现原理。
module.exports = {
entry: {
testxx: './src/index.js',
},
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
filename: 'index.html',
})
]
}
先看看该插件的apply方法:
apply(compiler) {
// Wait for configuration preset plugions to apply all configure webpack defaults
compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
const userOptions = this.userOptions;
// Default options
const defaultOptions = {
template: 'auto',
templateContent: false,
templateParameters: templateParametersGenerator,
filename: 'index.html',
publicPath: userOptions.publicPath === undefined ? 'auto' : userOptions.publicPath,
hash: false,
inject: userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
scriptLoading: 'defer',
compile: true,
favicon: false,
minify: 'auto',
cache: true,
showErrors: true,
chunks: 'all',
excludeChunks: [],
chunksSortMode: 'auto',
meta: {},
base: false,
title: 'Webpack App',
xhtml: false
};
/** 合并默认options */
const options = Object.assign(defaultOptions, userOptions);
this.options = options;
// entryName to fileName conversion function
const userOptionFilename = userOptions.filename || defaultOptions.filename;
const filenameFunction = typeof userOptionFilename === 'function'
? userOptionFilename
// Replace '[name]' with entry name
: (entryName) => userOptionFilename.replace(/\[name\]/g, entryName);
/** output filenames for the given entry names */
const entryNames = Object.keys(compiler.options.entry);
const outputFileNames = new Set((entryNames.length ? entryNames : ['main']).map(filenameFunction));
/** Option for every entry point */
const entryOptions = Array.from(outputFileNames).map((filename) => ({
...options,
filename
}));
// Hook all options into the webpack compiler
entryOptions.forEach((instanceOptions) => {
hookIntoCompiler(compiler, instanceOptions, this);
});
});
}
apply方法合并了用户传入的options,主要执行hookIntoCompiler方法。
...
// 初始化childCompiler并添加入口
const childCompilerPlugin = new CompileFilePlugins(compiler);
if (!options.templateContent) {
childCompilerPlugin.addEntry(options.template);
}
再来看看怎么获取到资源的:
compilation.hooks.processAssets.tapAsync(
{
name: 'HtmlWebpackPlugin',
stage:
/**
* Generate the html after minification and dev tooling is done
*/
webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
},
/**
* Hook into the process assets hook
* @param {WebpackCompilation} compilationAssets
* @param {(err?: Error) => void} callback
*/
(compilationAssets, callback) => {
// Get all entry point names for this html file
const entryNames = Array.from(compilation.entrypoints.keys());
const filteredEntryNames = filterChunks(entryNames, options.chunks, options.excludeChunks);
const sortedEntryNames = sortEntryChunks(filteredEntryNames, options.chunksSortMode, compilation);
// 获取编译结果
const templateResult = options.templateContent
? { mainCompilationHash: compilation.hash }
: childCompilerPlugin.getCompilationEntryResult(options.template);
// 如果在上一次主编译运行期间未执行子编译
// it is a cached result
const isCompilationCached = templateResult.mainCompilationHash !== compilation.hash;
/** The public path used inside the html file */
const htmlPublicPath = getPublicPath(compilation, options.filename, options.publicPath);
/** 获取输出的js和css列表 */
const assets = htmlWebpackPluginAssets(compilation, sortedEntryNames, htmlPublicPath);
// If the template and the assets did not change we don't have to emit the html
const newAssetJson = JSON.stringify(getAssetFiles(assets));
if (isCompilationCached && options.cache && assetJson === newAssetJson) {
previousEmittedAssets.forEach(({ name, html }) => {
compilation.emitAsset(name, new webpack.sources.RawSource(html, false));
});
return callback();
} else {
previousEmittedAssets = [];
assetJson = newAssetJson;
}
// html webpack插件使用html标签的对象表示,这些标签将被注入以允许更容易地更改。就在它们被转换之前,第三方插件作者可能会更改顺序和内容
// The html-webpack plugin uses a object representation for the html-tags which will be injected
// to allow altering them more easily
// 执行beforeAssetTagGeneration的hook
const assetsPromise = getFaviconPublicPath(options.favicon, compilation, assets.publicPath)
.then((faviconPath) => {
assets.favicon = faviconPath;
return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
assets: assets,
outputName: options.filename,
plugin: plugin
});
});
// 将css和js生成node模式
const assetTagGroupsPromise = assetsPromise // 插入html的脚本进行分组后触发的钩子
// And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
.then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
assetTags: {
scripts: generatedScriptTags(assets.js),
styles: generateStyleTags(assets.css),
meta: [
...generateBaseTag(options.base),
...generatedMetaTags(options.meta),
...generateFaviconTags(assets.favicon)
]
},
outputName: options.filename,
publicPath: htmlPublicPath,
plugin: plugin
}))
.then(({ assetTags }) => {
// Inject scripts to body unless it set explicitly to head
const scriptTarget = options.inject === 'head' ||
(options.inject !== 'body' && options.scriptLoading !== 'blocking') ? 'head' : 'body';
// 生成 `head` and `body` 数组
const assetGroups = generateAssetGroups(assetTags, scriptTarget);
// Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
headTags: assetGroups.headTags,
bodyTags: assetGroups.bodyTags,
outputName: options.filename,
publicPath: htmlPublicPath,
plugin: plugin
});
});
// 在沙盒中执行打包后的html模块
const templateEvaluationPromise = Promise.resolve()
.then(() => {
if ('error' in templateResult) {
return options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR';
}
// Allow to use a custom function / string instead
if (options.templateContent !== false) {
return options.templateContent;
}
// Once everything is compiled evaluate the html factory
// 在沙盒中执行html打包后的文件并输出资源
return ('compiledEntry' in templateResult)
? plugin.evaluateCompilationResult(templateResult.compiledEntry.content, htmlPublicPath, options.template)
: Promise.reject(new Error('Child compilation contained no compiledEntry'));
});
// 1.钩子 2.将css和js生成node模式 3.在沙盒中执行打包后的html模块
const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
// Execute the template
.then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
? compilationResult
: executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
// 2.将css和js生成node模式 3.在沙盒中执行打包后的html模块
const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
// 添加afterTemplateExecution钩子,此时已生成html,执行postProcessHtml
.then(([assetTags, html]) => {
const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: plugin, outputName: options.filename };
return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
})
.then(({ html, headTags, bodyTags }) => {
// 将script和css插入html
return postProcessHtml(html, assets, { headTags, bodyTags });
});
// 触发钩子并且将script和css插入html后
const emitHtmlPromise = injectedHtmlPromise
// 添加beforeEmit钩子
.then((html) => {
const pluginArgs = { html, plugin: plugin, outputName: options.filename };
return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(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());
return options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
})
.then(html => {
// filename警告
const filename = options.filename.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
(match, options) => `[contenthash${options}]`,
'[templatehash] is now [contenthash]')
);
// 替换contenthash
const replacedFilename = replacePlaceholdersInFilename(filename, html, compilation);
// 将html code添加到webpack assets
compilation.emitAsset(replacedFilename.path, new webpack.sources.RawSource(html, false), replacedFilename.info);
previousEmittedAssets.push({ name: replacedFilename.path, html });
return replacedFilename.path;
})// afterEmit的钩子
.then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
outputName: finalOutputName,
plugin: plugin
}).catch(err => {
console.error(err);
return null;
}).then(() => null));
// Once all files are added to the webpack compilation
// let the webpack compiler continue
emitHtmlPromise.then(() => {
callback();
});
});
获取资源主要调用htmlWebpackPluginAssets函数:
/** 获取打包的js和css列表
* The htmlWebpackPluginAssets extracts the asset information of a webpack compilation
* for all given entry names
* @param {WebpackCompilation} compilation
* @param {string[]} entryNames
* @param {string | 'auto'} publicPath
* @returns {{
publicPath: string,
js: Array,
css: Array,
manifest?: string,
favicon?: string
}}
*/
function htmlWebpackPluginAssets(compilation, entryNames, publicPath) {
const compilationHash = compilation.hash;
/**
* @type {{
publicPath: string,
js: Array,
css: Array,
manifest?: string,
favicon?: string
}}
*/
const assets = {
// The public path
publicPath,
// Will contain all js and mjs files
js: [],
// Will contain all css files
css: [],
// Will contain the html5 appcache manifest files if it exists
manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
// Favicon
favicon: undefined
};
// Append a hash for cache busting
if (options.hash && assets.manifest) {
assets.manifest = appendHash(assets.manifest, compilationHash);
}
// Extract paths to .js, .mjs and .css files from the current compilation
const entryPointPublicPathMap = {};
const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
for (let i = 0; i < entryNames.length; i++) {
const entryName = entryNames[i];
/** 获取所有的输出资源js和css文件等 */
const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles();
const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
// compilation.getAsset was introduced in webpack 4.4.0
// once the support pre webpack 4.4.0 is dropped please
// remove the following guard:
const asset = compilation.getAsset && compilation.getAsset(chunkFile);
if (!asset) {
return true;
}
// Prevent hot-module files from being included:
const assetMetaInformation = asset.info || {};
return !(assetMetaInformation.hotModuleReplacement || assetMetaInformation.development);
});
// Prepend the publicPath and append the hash depending on the
// webpack.output.publicPath and hashOptions
// E.g. bundle.js -> /bundle.js?hash
const entryPointPublicPaths = entryPointFiles
.map(chunkFile => {
const entryPointPublicPath = publicPath + urlencodePath(chunkFile);
return options.hash
? appendHash(entryPointPublicPath, compilationHash)
: entryPointPublicPath;
});
entryPointPublicPaths.forEach((entryPointPublicPath) => {
const extMatch = extensionRegexp.exec(entryPointPublicPath);
// Skip if the public path is not a .css, .mjs or .js file
if (!extMatch) {
return;
}
// Skip if this file is already known
// (e.g. because of common chunk optimizations)
if (entryPointPublicPathMap[entryPointPublicPath]) {
return;
}
entryPointPublicPathMap[entryPointPublicPath] = true;
// ext will contain .js or .css, because .mjs recognizes as .js
const ext = extMatch[1] === 'mjs' ? 'js' : extMatch[1];
assets[ext].push(entryPointPublicPath);
});
}
return assets;
}
然后开始设置当前插件的钩子:beforeAssetTagGeneration、将css和js生成node模式-alterAssetTags、生成 head
and body
数组-alterAssetTagGroups、在沙盒中执行后获取html-afterTemplateExecution、将script和css插入html、beforeEmit、替换contenthash并将html code添加到webpack assets、afterEmit。到此整个插件就执行完毕。