webpack之plugin编写 -- 从html-webpack-plugin源码的角度来看如何编写plugin

宇文曦
2023-12-01

webpack之plugin编写 – 从html-webpack-plugin源码的角度来看如何编写plugin

​ 准备写个webpack的plugin,打开官网文档https://www.webpackjs.com/contribute/writing-a-plugin,发现有点蒙圈,看完文档好像知道怎么写,但又写不出来,所以看下html-webpack-plugin的实现,从而整体了解下插件的整体流程

一、html-webpack-plugin源码分析

1.插件的作用

这个插件都用过,它的作用可以概况为两类:

  1. 生成html页面
  2. 处理bundle.js

2.结合plugin 的编写步骤分析html-webpack-plugin的实现

2.1流程步骤

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内部实例的特定数据

2.2需要了解的钩子

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

2.3html-webpack-plugin如何处理数据

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发布就行

 类似资料: