准备写个webpack的plugin,打开官网文档https://www.webpackjs.com/contribute/writing-a-plugin,发现有点蒙圈,看完文档好像知道怎么写,但又写不出来,所以看下html-webpack-plugin的实现,从而整体了解下插件的整体流程
这个插件都用过,它的作用可以概况为两类:
webpack文档的步骤:
webpack 插件由以下组成:
1. 一个 JavaScript 命名函数。
2.在插件函数的 prototype 上定义一个 apply 方法。
3.指定一个绑定到 webpack 自身的事件钩子。
4.处理 webpack 内部实例的特定数据。
5.功能完成后调用 webpack 提供的回调。
根据文档步骤可以写出一下代码:
class HtmlWebpackPlugin {
constructor(options) {
}
apply(compiler){
compiler.hooks.emit.tapAsync((compilation,callback) => {
...
callback()
})
}
}
module.exports = HtmlWebpackPlugin;
这就一个plugin插件的雏形,这个光看文档也能写出来,所以看源码的主要目的是看第4步,即如何处理webpack内部实例的特定数据
a.compiler
https://www.webpackjs.com/api/compiler-hooks/
https://github.com/webpack/webpack/blob/master/lib/Compiler.js
它是扩展自tapable类,用来注册和调用插件,tapable的用法有篇博客专业介绍,这里不再说了,简单来说就是compiler扩展与tapable,抛出一系列的生命周期钩子函数,以便我们在webpack的不同编译阶段进行不同的操作
本次主要关注 emit钩子
b.compilation
https://www.webpackjs.com/api/compilation-hooks/
https://github.com/webpack/webpack/blob/master/lib/Compilation.js
它可以理解为webpack编译一次生成的整个编译资源,一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。本次主要关注 assets
a. 如何生成html
1.插件调用:
new HtmlWebPackPlugin({
template: "./public/index.html",
filename: "./index.html"
}),
当插件调用的时候会传入html 的 template以及filename
apply(){
...
this.options.template = this.getFullTemplatePath(this.options.template, compiler.context);
...
}
getFullTemplatePath() 会判断模板是否配置了loader,如果没有配置会使用默认的loader
2.index.html生成过程:
index.js
....
compilationPromise = childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation)
.....
compiler.js
....
const childCompiler = compilation.createChildCompiler(compilerName, outputOptions);
... // 创建一个跟当前Compiler一样配置的Compiler(childCompiler)对象
childCompiler.runAsChild((err, entries, childCompilation) => { //开始执行childCompiler编译器的run操作(独立于主编译器外的编译器)
...
resolve({
hash: entries[0].hash, //当前模块(index.html)的hash值
outputName: outputName, //当前模块(index.html)的名称
content: childCompilation.assets[outputName].source() /当前模块(index.html)编译过后的源码
});
})
....
到这里其实index.html已经加载了,childCompilation.assets[outputName].source() 返回的就是index.html编译之后的源码,下一步就是把所有的打包文件添加到index.html中
3.将生成的资源文件(js/css)插入html
处理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);
compilation的assets是map对象,html-webpack-plugin会先将assets的chunks处理一下,便于后期插入
evaluateCompilationResult (compilation, source) {
if (!source) {
return Promise.reject('The child compilation didn\'t provide a result');
}
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});
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.');
}
这里使用node的vm(虚拟机)来构建index.html源码,source 其实就是上一步生成的compilationPromise
generateHtmlTags (assets) {
const scripts = assets.js.map(scriptPath => ({
tagName: 'script',
closeTag: true,
attributes: {
type: 'text/javascript',
src: scriptPath
}
}));
const selfClosingTag = !!this.options.xhtml;
const styles = assets.css.map(stylePath => ({
tagName: 'link',
selfClosingTag: selfClosingTag,
voidTag: true,
attributes: {
href: stylePath,
rel: 'stylesheet'
}
}));
let head = this.getMetaTags();
let body = [];
if (assets.favicon) {
head.push({
tagName: 'link',
selfClosingTag: selfClosingTag,
voidTag: true,
attributes: {
rel: 'shortcut icon',
href: assets.favicon
}
});
}
head = head.concat(styles)
if (this.options.inject === 'head') {
head = head.concat(scripts);
} else {
body = body.concat(scripts);
}
return {head: head, body: body};
}
generateHtmlTags()根据assets中js和css生成要插入html的head和body,assets是从compilation.getStats()获取并整理处理的 chunks对象
injectAssetsIntoHtml (html, assets, assetTags) {
const htmlRegExp = /(<html[^>]*>)/i;
const headRegExp = /(<\/head\s*>)/i;
const bodyRegExp = /(<\/body\s*>)/i;
const body = assetTags.body.map(this.createHtmlTag.bind(this));
const head = assetTags.head.map(this.createHtmlTag.bind(this));
if (body.length) {
if (bodyRegExp.test(html)) {
// Append assets to body element
html = html.replace(bodyRegExp, match => body.join('') + match);
} else {
// Append scripts to the end of the file if no <body> element exists:
html += body.join('');
}
}
if (head.length) {
// Create a head tag if none exists
if (!headRegExp.test(html)) {
if (!htmlRegExp.test(html)) {
html = '<head></head>' + html;
} else {
html = html.replace(htmlRegExp, match => match + '<head></head>');
}
}
// Append assets to head element
html = html.replace(headRegExp, match => head.join('') + match);
}
// Inject manifest into the opening html tag
if (assets.manifest) {
html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
// Append the manifest only if no manifest was specified
if (/\smanifest\s*=/.test(match)) {
return match;
}
return start + ' manifest="' + assets.manifest + '"' + end;
});
}
return html;
}
injectAssetsIntoHtml()函数就是将处理过的body和header插入index.html中
...
compilation.assets[self.childCompilationOutputName] = {
source: () => html,
size: () => html.length
};
....
最后一步就是将已经插入资源的index.html加入到compilation的assets中,便于webpack打包生成文件
然后再去看webpack官网plugin编写的案例:
function FileListPlugin(options) {} //第一步: 创建一个函数
FileListPlugin.prototype.apply = function(compiler) { //第二步:在函数的prototype上定义一个apply方法
compiler.plugin('emit', function(compilation, callback) { //第三步:指定一个webpack自身的钩子
//第四步: 处理webpack内部实例的特定数据
webpack的所有资源都会生成在assets对象中(html是html-webpack-plugin生成插入的),
assets 对象:
{
'./index.html': {
source: [Function:source], //文件源码
size:[Function:size] //文件大小
}
}
所以如果想生成新文件可以直接在assets中插入,webpack会将文件生成在dist文件(出口文件)下,如果想处理其他文件可以在assets中修改
var filelist = 'In this build:\n\n';
for (var filename in compilation.assets) {
filelist += ('- '+ filename +'\n');
}
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback(); //第五步: 功能完成之后调用webpack的回调
});
};
module.exports = FileListPlugin;
最后本地调试插件:
weebpack.config.js
const HtmlWebPackPlugin = require('./html-webpack-plugin') //引入本地文件
....
plugin:[
new HtmlWebPackPlugin({
template: "./public/index.html",
filename: "./index.html"
}),
]
本地开发完npm发布就行