继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

探寻 webpack 插件机制

千万里不及你
关注TA
已关注
手记 362
粉丝 53
获赞 237

webpack 可谓是让人欣喜又让人忧,功能强大但需要一定的学习成本。在探寻 webpack 插件机制前,首先需要了解一件有意思的事情,webpack 插件机制是整个 webpack 工具的骨架,而 webpack 本身也是利用这套插件机制构建出来的。因此在深入认识 webpack 插件机制后,再来进行项目的相关优化,想必会大有裨益。

webpack 插件

先来瞅瞅 webpack 插件在项目中的运用

const MyPlugin = require('myplugin')const webpack = require('webpack')

webpack({
  ...,  plugins: [new MyPlugin()]
  ...,
})

那么符合什么样的条件能作为 webpack 插件呢?一般来说,webpack 插件有以下特点:

  1. 独立的 JS 模块,暴露相应的函数

  2. 函数原型上的 apply 方法会注入 compiler 对象

  3. compiler 对象上挂载了相应的 webpack 事件钩子

  4. 事件钩子的回调函数里能拿到编译后的 compilation 对象,如果是异步钩子还能拿到相应的 callback

下面结合代码来看看:

function MyPlugin(options) {}// 2.函数原型上的 apply 方法会注入 compiler 对象MyPlugin.prototype.apply = function(compiler) {  // 3.compiler 对象上挂载了相应的 webpack 事件钩子 4.事件钩子的回调函数里能拿到编译后的 compilation 对象
  compiler.plugin('emit', (compilation, callback) => {
    ...
  })
}// 1.独立的 JS 模块,暴露相应的函数module.exports = MyPlugin

这样子,webpack 插件的基本轮廓就勾勒出来了,此时疑问点有几点,

  1. 疑问 1:函数的原型上为什么要定义 apply 方法?阅读源码后发现源码中是通过 plugin.apply() 调用插件的。

const webpack = (options, callback) => {
  ...
  for (const plugin of options.plugins) {
    plugin.apply(compiler);
  }
  ...
}
  1. 疑问 2:compiler 对象是什么呢?

  2. 疑问 3:compiler 对象上的事件钩子是怎样的?

  3. 疑问 4:事件钩子的回调函数里能拿到的 compilation 对象又是什么呢?

这些疑问也是本文的线索,让我们一个个探索。

compiler 对象

compiler 即 webpack 的编辑器对象,在调用 webpack 时,会自动初始化 compiler 对象,源码如下:

// webpack/lib/webpack.jsconst Compiler = require("./Compiler")const webpack = (options, callback) => {
  ...
  options = new WebpackOptionsDefaulter().process(options) // 初始化 webpack 各配置参数
  let compiler = new Compiler(options.context)             // 初始化 compiler 对象,这里 options.context 为 process.cwd()
  compiler.options = options                               // 往 compiler 添加初始化参数
  new NodeEnvironmentPlugin().apply(compiler)              // 往 compiler 添加 Node 环境相关方法
  for (const plugin of options.plugins) {
    plugin.apply(compiler);
  }
  ...
}

终上,compiler 对象中包含了所有 webpack 可配置的内容,开发插件时,我们可以从 compiler 对象中拿到所有和 webpack 主环境相关的内容。

compilation 对象

compilation 对象代表了一次单一的版本构建和生成资源。当运行 webpack 时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。

结合源码来理解下上面这段话,首先 webpack 在每次执行时会调用 compiler.run() (源码位置),接着追踪 onCompiled 函数传入的 compilation 参数,可以发现 compilation 来自构造函数 Compilation。

// webpack/lib/Compiler.jsconst Compilation = require("./Compilation");

newCompilation(params) {  const compilation = new Compilation(this);
  ...  return compilation;
}

不得不提的 tapable 库

再介绍完 compiler 对象和 compilation 对象后,不得不提的是 tapable 这个库,这个库暴露了所有和事件相关的 pub/sub 的方法。而且函数 Compiler 以及函数 Compilation 都继承自 Tapable。

事件钩子

事件钩子其实就是类似 MVVM 框架的生命周期函数,在特定阶段能做特殊的逻辑处理。了解一些常见的事件钩子是写 webpack 插件的前置条件,下面列举些常见的事件钩子以及作用:

钩子作用参数类型
after-plugins设置完一组初始化插件之后compilersync
after-resolvers设置完 resolvers 之后compilersync
run在读取记录之前compilerasync
compile在创建新 compilation 之前compilationParamssync
compilationcompilation 创建完成compilationsync
emit在生成资源并输出到目录之前compilationasync
after-emit在生成资源并输出到目录之后compilationasync
done完成编译statssync

完整地请参阅官方文档手册,同时浏览相关源码 也能比较清晰地看到各个事件钩子的定义。

插件流程浅析

拿 emit 钩子为例,下面分析下插件调用源码:

compiler.plugin('emit', (compilation, callback) => {  // 在生成资源并输出到目录之前完成某些逻辑})

此处调用的 plugin 函数源自上文提到的 tapable 库,其最终调用栈指向了 hook.tapAsync(),其作用类似于 EventEmitter 的 on,源码如下:

// Tapable.jsoptions => {
  ...  if(hook !== undefined) {    const tapOpt = {
      name: options.fn.name || "unnamed compat plugin",
      stage: options.stage || 0
    };    if(options.async)
      hook.tapAsync(tapOpt, options.fn); // 将插件中异步钩子的回调函数注入
    else
      hook.tap(tapOpt, options.fn);    return true;
  }
};

有注入必有触发的地方,源码中通过 callAsync 方法触发之前注入的异步事件,callAsync 类似 EventEmitter 的 emit,相关源码如下:

this.hooks.emit.callAsync(compilation, err => {    if (err) return callback(err);
    outputPath = compilation.getPath(this.outputPath);    this.outputFileSystem.mkdirp(outputPath, emitFiles);
});

一些深入细节这里就不展开了,说下关于阅读比较大型项目的源码的两点体会,

  • 要抓住一条主线索去读,忽视细节。否则会浪费很多时间而且会有挫败感;

  • 结合调试工具来分析,很多点不用调试工具的话很容易顾此失彼;

动手实现个 webpack 插件

结合上述知识点的分析,不难写出自己的 webpack 插件,关键在于想法。为了统计项目中 webpack 各包的有效使用情况,在 fork webpack-visualizer 的基础上对代码升级了一番,项目地址。效果如下:

https://img2.mukewang.com/5d576d2500014b0a04690462.png

image

插件核心代码正是基于上文提到的 emit 钩子,以及 compiler 和 compilation 对象。代码如下:

class AnalyzeWebpackPlugin {  constructor(opts = { filename: 'analyze.html' }) {    this.opts = opts
  }

  apply(compiler) {    const self = this
    compiler.plugin("emit", function (compilation, callback) {      let stats = compilation.getStats().toJson({ chunkModules: true }) // 获取各个模块的状态
      let stringifiedStats = JSON.stringify(stats)      // 服务端渲染
      let html = `<!doctype html>
          <meta charset="UTF-8">
          <title>AnalyzeWebpackPlugin</title>
          <style>${cssString}</style>
          <div id="App"></div>
          <script>window.stats = ${stringifiedStats};</script>
          <script>${jsString}</script>
      `
      compilation.assets[`${self.opts.filename}`] = { // 生成文件路径
        source: () => html,        size: () => html.length
      }
      callback()
    })
  }
}

参考资料

看清楚真正的 Webpack 插件

webpack 官网



作者:牧云云
链接:https://www.jianshu.com/p/316c9f6c1880


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP