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

探寻 webpack 插件机制的更多相关文章

  1. 大前端工程化之写一个简单的webpack插件

    今天写一个简单的webpack插件,来学习一下webpack插件 webpack插件机制可以使开发者在webpack构建过程中加入自己的行为,来针对自己项目中的一些需求做一些定制化 首先我们得知道一个 ...

  2. delphi 的插件机制与自动更新

    delphi 的插件机制与自动更新 : 1.https://download.csdn.net/download/cxp_2008/2226978   参考 2.https://download.cs ...

  3. 从0开始编写webpack插件

    1. 前言 插件(plugins)是webpack中的一等功臣.正是由于有了诸多插件的存在,才使得webpack无所不能.在webpack源码中也是使用了大量的内部插件,插件要是用的好,可以让你的工作 ...

  4. webpack插件解析:HtmlWebpackPlugin是干什么的以及如何使用它

    HtmlWebpackPlugin是一个出现频率比较高的webpack插件,本文对其作用和配置作一番比较详细的分析(本文的配置均在webpack.config.js中进行). 为何使用它 简单来说,H ...

  5. 【学】jQuery的源码思路6——增加each,animaion,ajax以及插件机制

    each() 插件机制 animation ajax //each() //这里第一个参数指定将this指向每次循环到的那个元素身上,而第三个参数element其实就是this本身所以和第一个参数是一 ...

  6. ImitateLogin新增插件机制以及又一个社交网站的支持

    我的文章里已经多次介绍 imitate-login ,这是我最近一直在维护的一个使用c#模拟社交网站登录的开源项目,现在新增了对插件的支持以及一个新的网站(由于某种原因,会在文章结束部分介绍:而且仅会 ...

  7. Maven生命周期和插件机制

    Maven中的一个非常重要的概念是生命周期和插件,这篇文章重点介绍下Maven的生命周期. Maven的生命周期是抽象的,具体的功能是有具体的插件来完成的,Maven有相当多的功能插件,以至于Mave ...

  8. php中的钩子(hook插件机制)

    对"钩子"这个概念其实不熟悉,最近看到一个php框架中用到这种机制来扩展项目,所以大概来了解下. hook插件机制的基本思想: 在项目代码中,你认为要扩展(暂时不扩展)的地方放置一 ...

  9. winform插件机制学习

    这两天在看自定义控件,原来有太多知识没有掌握.今天看到插件机制,心里突然一亮,这个东西听了不少次,就是不知道是啥回事.这次有幸书里包含一个案例,我就跟着它一步步来.终于知道是什么回事了.这个应该在软件 ...

随机推荐

  1. JVM学习七:JVM之类加载器之类的卸载

    类加载的过程和原理,以及双亲委派机制都已经讲解完成,那么我们今天讲解类加载的最后一节,那么就是类的卸载. 我们知道,当一个类被加载.连接和初始化之后,他的生命周期就开始了,当该类的class对象不再被 ...

  2. java高并发锁的三种实现

    提到锁大家会想到Synchronized同步关键字,使用它确实可以解决一切并发问题,但是对于体统吞吐量要求更高,在这里提供了几个小技巧.帮助大家减少锁粒度.提高系统的并发能力 一.乐观锁 试用场景:读 ...

  3. 如何测试一个WEB的输入框?

    WEB输入框是B/S架构系统中页面使用非常频繁的控件,比如我们登录一个网站,输入 用户名和密码的控件都是输入框,比如使用百度搜索,在输入搜索内容的控件也是输入框,比如网购一个物品,我们需要输入购买的数 ...

  4. Python基本数据结构--列表

    列表: 1.有序的集合: 2.通过偏移来索引,从而读取数据: 3.支持嵌套: 4.可变的类型: 列表的操作: 1.切片: a = [1,2,3,4,5,6,7] 正向索引 反向索引 默认索引 2.添加 ...

  5. java虚拟机的内存分配与回收机制

    分为4个方面来介绍内存分配与回收,分别是内存是如何分配的.哪些内存需要回收.在什么情况下执行回收.如何监控和优化GC机制. java GC(Garbage Collction)垃圾回收机制,是java ...

  6. 刚入大学B. http://mp.weixin.qq.com/s/ORpKfX8HOQEJOYfwvIhRew

    自己对计算机还是比较感兴趣的,经过不断的努力,我相信我可以在这一专业中显露头角,我会努力向博主学习.理想的大学是自由,快乐,可以学到很多知识的地方,未来我想在lt行业进行软件开发等项目,为了梦想我会不 ...

  7. 20162320刘先润第三周Bag类测试

    前言 以下内容是本周Bag代码的课后作业,要求是完成伪代码.产品代码和测试代码,为了书写方便我将伪代码以注释的形式写在了产品代码的后面 测试步骤 1.首先对Bag类引用BagInterface的代码进 ...

  8. 关于mule中使用jdbc时报No Suitable Driver found错误的问题

    错误大概信息: Exception in thread "main" org.mule.module.launcher.DeploymentStartException: SQLE ...

  9. Linux基础常用命令

    Linux 下命令有很多,并且很多命令用法又有不同的选项,这里介绍一些常用的最基本的Linux命令的用法,希望给大家留下便利之处. 1.cd 切换目录.例如 cd /home 可切换到home目录,  ...

  10. 11-移动端开发教程-zepto.js入门教程

    Zepto.js是一个轻量级的针对现代浏览器的JavaScript库, 它与jquery有着类似的api. 如果你会用jquery,那么你也会用zepto. 1. Why Zepto.js? API类 ...