zoukankan      html  css  js  c++  java
  • 探寻 webpack 插件机制

    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.js
    const 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.js
    const 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 设置完一组初始化插件之后 compiler sync
    after-resolvers 设置完 resolvers 之后 compiler sync
    run 在读取记录之前 compiler async
    compile 在创建新 compilation 之前 compilationParams sync
    compilation compilation 创建完成 compilation sync
    emit 在生成资源并输出到目录之前 compilation async
    after-emit 在生成资源并输出到目录之后 compilation async
    done 完成编译 stats sync

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

    插件流程浅析

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

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

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

    // Tapable.js
    options => {
      ...
      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 的基础上对代码升级了一番,项目地址。效果如下:

    插件核心代码正是基于上文提到的 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://cloud.tencent.com/developer/support-plan?invite_code=u9das0thv85y

  • 相关阅读:
    http与websocket(基于SignalR)两种协议下的跨域基于ASP.NET MVC--竹子整理
    让Visual Studio 2015 支持ASP.NET MVC4.0.0.1
    自定义滚动条CSS样式
    使用NuGet发布自己的类库包(Library Package)
    基于EF的数据外键关联查询
    基于EF创建数据库迁移
    用SQL命令查看Mysql数据库大小
    Python之MySQL数据操作
    Python之MySQL基础
    Python网络编程之黏包问题
  • 原文地址:https://www.cnblogs.com/MuYunyun/p/8875908.html
Copyright © 2011-2022 走看看