阅读 794

从无到有,实现一个webpack插件

作者:孙梦舸

webpack已经成为开发web应用不可或缺的工程化工具,而作为webpack支柱功能的插件系统,是我们深入学习webpack绕不过的一道坎。站在插件开发的视角,你可以更细粒度地控制webpack的工作,实现那些通过修改配置文件做不到的事情。

小程序 & webpack

最近公司团队使用webpack来给微信小程序项目做工程化,每个页面的js文件作为webpack的一个独立entry,经过编译打包后,每个entry都对应生成一个output js文件,引用了node_modules文件夹下面的模块也会被打包进来,因此不需要执行小程序构建npm这一过程。但是造成了模块重复打包的问题:

如上图,A模块被1、2两个页面引用,最终A模块被重复打包进了两个页面的输出文件里,造成小程序打包体积过大。

经过修改webpack配置、提取公共模块之后解决了该问题。但毕竟是跳过了“构建npm”这一过程,因此我决定开发一款webpack插件,模拟小程序的这一过程。

What to do?

先看一下小程序在“构建npm”这一步骤做了什么:

  1. 首先 node_modules 目录不会参与编译、上传和打包中,所以小程序想要使用 npm 包必须走一遍“构建 npm”的过程,在每一份 miniprogramRoot 内开发者声明的 package.json 的最外层的 node_modules 的同级目录下会生成一个 miniprogram_npm 目录,里面会存放构建打包后的 npm 包,也就是小程序真正使用的 npm 包。
  2. 构建打包分为两种:小程序 npm 包会直接拷贝构建文件生成目录下的所有文件到 miniprogram_npm 中;其他 npm 包则会从入口 js 文件开始走一遍依赖分析和打包过程(类似 webpack)。
  3. 寻找 npm 包的过程和 npm 的实现类似,从依赖 npm 包的文件所在目录开始逐层往外找,直到找到可用的 npm 包或是小程序根目录为止。

构建后的目录结构大致如下:

|--node_modules
|--miniprogram_npm
|    |--testComp // 小程序 npm 包
|    |    |-index.js
|    |    |-index.json
|    |    |-index.wxss
|    |    |-index.wxml
|    |--testa // 其他 npm 包
|         |--index.js // 打包后的文件
|         |--miniprogram_npm
|              |--testb
|                   |--index.js // 打包后的文件
|                   |--index.js.map
|--pages
|--app.js
|--app.wxss
|--app.json
|--project.config.js
复制代码

简单来说:先将node_modules目录下的模块,拷贝到同级的miniprogram_npm目录;然后将所有对node_modules模块的引用,修改到miniprogram_npm,例如require('node_modules/abc')改为require('miniprogram_npm/abc')

我们要做的就是用webpack插件复刻上面的步骤:

  1. 遍历所有webpack解析出的模块,找出所有公共Modules(node_modules目录、以及作为参数传给插件的目录)
  2. 公共Modules与其被引用的所有Chunks解绑
  3. 给每个公共Module创建新Chunk,一个Chunk关联一个Module,并插入Chunks列表
  4. 生成Assets阶段,修改文件源代码,根据本模块的dependencies和目标模块的reasons,插入require语句

How to do?

1. 对官方文档有个大概的了解

只是大概,着手开发之前不需要看的特别细,因为大部分知识都没有通过文档整理出来。可以看下这篇:webpack.js.org/contribute/…,作为入门。

tapable,懂得用哪种类型的挂钩即可,原理暂不必深究。

(一定要看webpack英文官网,中文官网内容更新较晚!)

2. 善用断点调试

因为是在node端编程,我们在webstorm、vscode这样的编辑器里面就可以打断点,然后启动debug模式

各变量的数据结构一目了然。

3. 了解webpack常用变量的结构

例如Compiler、Compilation、Module、Chunk等,官方文档对这部分并没有说明,因此还是要通过断点+源码的方式。

  • 盯断点:可能需要在断点模式下,盯着数据结构看很久、很多次
  • 看源码:看类的常用方法,ts文件(有些ts类型相对于源码少了东西,应该是官方没来得及更新)

4. 增删改内部变量时,优先调用封装好的函数

例如解除module和chunk的绑定,如果执行module._chunk.delete(chunk),仅仅是解除了module对chunk的依赖,而两者是双向依赖,就意味着还需要反向解除chunk对module的依赖。

但不需要这么麻烦,通过源码我们发现,可以直接调用Module类提供的一个方法:

removeChunk(chunk) {
    if (this._chunks.delete(chunk)) {
        chunk.removeModule(this);
        return true;
    }
    return false;
}
复制代码

解除了module和chunk的双向依赖。

5. 选对时机:hooks

Compiler和Compilation都提供各种钩子函数,可以参考这篇文档:webpack.js.org/api/compile…。列表里的函数是按照执行顺序来排列的,注意区分回调函数传入参数的不同。

Compiler:我理解为webpack的实例,全局唯一 Compilation:一次编译,run模式下只执行一次,watch模式下每次文件变化执行一次

各个钩子函数的解释文档里面都有,但我还是不知道从何入手,怎么办? 这时可以找一个比较成熟的插件,参考它的源码

比如我要做的功能,和SplitChunkPlugin的功能非常相似,于是我去github搜索,没有找到...然后发现它已经被webpack内置了,在webpack的源代码中找到了它。然后就是参考它的hooks切入点,模仿它。这是对新手最友好的方式,特别是看hooks文档看的一头雾水的时候。

6. 修改webpack config

当插件要实现的功能,需要修改webpack的配置文件怎么办?其实可以在插件内部修改:

compiler.hooks.environment.tap('HelloWorldPlugin', () => {
    // 修改webpack config,必须抽出runtime.js
    compiler.options.optimization.runtimeChunk = {
      	name: 'runtime'
    }
});
复制代码

在适当的hooks里面,通过compiler.options对象访问、修改。

或者不这样实现,而是手动修改配置文件,两种方式,自由选择,这里只是说明插件拥有这个功能。

成果

插件引用方式:

new HelloWorldPlugin({
    libPaths: ['src/utils']
}),
复制代码

引入了插件之后的打包结果:

|--mp_node_modules	// 类似小程序的miniprogram_npm目录
|--pages
|--utils
|--components
|--app.js
|--app.wxss
|--app.json
|--runtime.js
|--sitemap.json
复制代码

根据依赖关系,在文件头部插入require语句:

小程序打包缺陷

声明 subpackages 后,将按 subpackages 配置路径进行打包,subpackages 配置路径外的目录将被打包到 app(主包) 中 ————小程序文档/使用分包/打包原则

因为该原则的原因,所有node_modules模块,都会被打包到主包,因此对于稍大型的应用来说,随着工程规模扩大,主包随时可能超过2M上限,需要将业务拆分至多个小程序才能解决。

不过换个角度一想,可能小程序的初衷就是“小”,有可能是微信官方故意而为之。

文章分类
前端
文章标签