作者:孙梦舸
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”这一步骤做了什么:
- 首先 node_modules 目录不会参与编译、上传和打包中,所以小程序想要使用 npm 包必须走一遍“构建 npm”的过程,在每一份 miniprogramRoot 内开发者声明的 package.json 的最外层的 node_modules 的同级目录下会生成一个 miniprogram_npm 目录,里面会存放构建打包后的 npm 包,也就是小程序真正使用的 npm 包。
- 构建打包分为两种:小程序 npm 包会直接拷贝构建文件生成目录下的所有文件到 miniprogram_npm 中;其他 npm 包则会从入口 js 文件开始走一遍依赖分析和打包过程(类似 webpack)。
- 寻找 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插件复刻上面的步骤:
- 遍历所有webpack解析出的模块,找出所有公共Modules(node_modules目录、以及作为参数传给插件的目录)
- 公共Modules与其被引用的所有Chunks解绑
- 给每个公共Module创建新Chunk,一个Chunk关联一个Module,并插入Chunks列表
- 生成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上限,需要将业务拆分至多个小程序才能解决。
不过换个角度一想,可能小程序的初衷就是“小”,有可能是微信官方故意而为之。