Logo
Published on

写一个webpack插件到底有多复杂?

Authors

在前端工程化的今天,如果说一个前端工程师说自己没用过甚至没听说webpack,我想一定会让人吓掉下巴。毕竟无论是三大框架的工程化脚手架解决方案,还是各个团队基于自身特点构建的前端打包工具,我们都可以在这里面看到webpack或者其它构建工具的身影。

在我们直接或者间接地接触这些构建工具时,已经被各种各样的插件所包围了,但是我们是否有考虑过,一个构建工具,或者说webpack,的插件到底是怎么样实现的呢?webpack又是怎样做到可以让插件处理各种各样的文件的呢?

以下的内容,我会用一个很简单的已经写好了的webpack插件作为demo来介绍如何写一个webpack的插件(插件暂时不支持webpack5,插件的源代码请移步阅读原文)

背景

至于为什么要写这个插件,原因其实很简单。webpack虽然本身支持使用dll将第三方依赖包不打到最后的vender.js中,但是所有的依赖包仍然会被打到同一个js文件中。那么在依赖包的数量过多的情况下,该文件依然会很大。因此这个插件的目的就是通过配置,将这些依赖抽取出来,使用script标签的形式将这些插件有序地插入到HTML模板文件中去。因此,这个插件是基于html-webpack-plugin的。

思路

在确定了插件的作用之后,我们来思考一下插件实现的大致思路。这里我们需要参考webpack的开发文档(https://www.webpackjs.com/concepts/plugins/)。

通过文档我们可以看到,webpack的插件最重要的两个钩子 -- compiler和compilation,前者是webpack的支柱,在webpack4中,可以通过它的hooks属性,访问各个生命周期的钩子函数,而在webpack3中,是通过compiler.plugin给不同的生命周期注册回调函数来等待调用的;

而后者能访问所有的模块和它们的依赖,在编译阶段,模块会被加载(loaded)、封存(sealed)、优化(optimized)、分块(chunked)、哈希(hashed)和重新创建(restored),在webpack3/4中,compilation的调用方式和compiler都是相同的。

另外,webpack其实是一个具有apply属性的JavaScript对象,这个apply属性会被compiler调用。

同时,对于我们的插件来说,重点就是把传入的配置处理后回填到webpack的配置中。为了实现这一点,在我们的插件中,对于每一个依赖包,需要收集如下信息:

1.依赖包/库的名称:libName

2.依赖包/库在全局暴露的名称:globalName

3.依赖包/库需要请求的地址,这个地址可以是自己服务器上的地址,也可以是CDN的地址:url

通过这三个配置就能看出来,前两个字段是为了往webpack的配置信息中回填内容的,而url字段是为了往HTML模板中插入script标签用的。

在有了这样的思路之后,我们可以先构建项目目录了。

项目结构

对于webpack插件来说,其实只需要一个index.js文件,然后在package.json中配置好入口文件即可。

代码

在这个index.js文件中,首先需要对外导出一个函数,用于给开发者调用并传入参数。

function main(options) {
  this.options = options
}

module.exports = main

然后给这个函数的原型添加apply属性,并且针对不同的webpack版本注册生命周期函数

main.prototype.apply = function (complier) {
  // 处理参数

  if (compiler.hooks) {
    // webpack 4
    compiler.hooks.compilation.tap('xxx', (compilation) => {
      compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tapAsync(
        'htmlWebpackScriptPlugin',
        (htmlPluginData, callback) => {
          // 核心代码
        }
      )
    })
  } else {
    compiler.plugin('compilation', function (compilation) {
      compilation.plugin(
        'html-webpack-plugin-before-html-processing',
        function (htmlPluginData, callback) {
          // 核心代码
        }
      )
    })
  }
}

因为我们的插件需要在html-webpack-plugin之后再处理,所以需要用compilation找到html-webpack-plugin的钩子再注册一个回调函数。具体的核心代码可以在阅读原文的链接中找到。

总结

至此,一个极简的支持webpack3/4的插件就完成了。

我们知道了webpack插件的本质就是一个有apply属性的JavaScript对象,同时最重要的两个钩子是compiler和compilation,它们可以访问webpack整个生命周期并且可以访问所有模块及其依赖等。

当然,要完全搞懂webpack的插件机制也并不是写一两个简单的插件就够的,但是,至少现在已经入门了不是吗?