一、前言
去年(19年10月)在某技术沙龙上分享了《小程序工程化探索》后,陆续有网友联系到我询问一些实现方面的细节,虽然常年顶着黑眼圈修着“福报”,但还是决定抽出时间写一个小程序工程化系列,一是希望能帮到部分同学,二是希望能提升自己的总结与表达能力,由于是一个系列,所以每篇文章会尽量聚焦一个点,篇幅不会很长。闲话少述,本篇是小程序工程化系列第一篇,我将会详细介绍如何利用 Webpack 实现对小程序代码的文件依赖分析。
二、小程序早期的打包方式
我们知道,小程序开发者工具自带了 ES6 转 ES5 及压缩混淆能力,所以 js 这块,可以很方便地使用 ES6 进行开发,不需要额外使用什么工具来进行编译。但 css 这块缺少了 Sass/PostCSS 的支持,所以前端同学一般还会补充 Sass/PostCSS 的支持,打包上传时则只需要对源码中的 *.sass 文件进行转换并将其他源文件直接提取出来即可,Gulp 其实就很适合做这些,事实上,我们一开始也是使用的 Gulp,但随着项目的迭代,久而久之,仅依靠 Gulp 的 glob 规则,已经很难识别项目中哪些文件是真正需要的,从而导致漏传代码、多传代码等问题。这个时候就需要做文件的依赖分析,轮到 Webpack 上场了(并不是只有 Webpack 才能做依赖分析,我们选用它因为还需要做别的事情)。
三、小程序的依赖资源有哪些
以微信小程序举例,小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录:app.js、app.json、app.wxss。一个小程序页面由四个文件组成,分别是:js、json、wxss、wxml。除此之外,还有一些图片字体等文件以及脚本文件wxs。如何识别小程序项目中哪些文件是真正需要的?也就是如何做依赖分析,我们知道小程序是所有页面必须要在 app.json 里进行注册,通过这个信息就可以拿到所有页面的文件依赖及组件的文件依赖。想到这点并不难,接下来看看如何实现。
四、依赖分析如何实现
Webpack 有一个很重要的概念就是入口,你在编译时必须要指定一个入口,Webpack 会从入口开始分析它的所有依赖,在 Web 页面构建中,入口一般对应到页面的主 js。但小程序并没有这样一个 js 入口,所以我们需要根据小程序特性来动态生成一个入口,并从这个入口开始递归获取小程序的所有依赖资源,如下图。
注,阅读本系列文章需要有一定的 Webpack 基础知识。
![](https://i-blog.csdnimg.cn/blog_migrate/c10a596d1efa3adf48d2a6a6d07baaae.png)
入口的生成
假设小程序的目录结构如下
|-- pages
|-- index
|-- index.js
|-- index.wxss
|-- index.wxml
|-- index.json
|-- components
|-- nav
|-- nav.js
|-- nav.wxss
|-- nav.wxml
|-- nav.json
|-- common
|-- color.wxss
|-- app.js
|-- app.json
|-- app.wxss
app.json文件文件内容如下
{
"pages": [
"pages/index/index"
]
}
前面提到组成小程序主体所必须的三个文件:app.js、app.json、app.wxss,由于app.json仅注册了一个页面,由此我们可以生成这样一个入口文件 entry.js
。
require('./app.js');
require('./app.wxss');
require('./pages/index/index.js');
require('./pages/index/index.json');
require('./pages/index/index.wxss');
require('./pages/index/index.wxml');
我们知道 Webpack 默认只能对 js 文件进行识别,不管你的模块加载使用的是 CommonJS 还是 AMD 还是 ESM,它都能进行识别,并以此生成一个依赖树,依赖关系保存在编译实例对象中。但针对这样一个入口文件,还有 wxss、wxml、json 文件无法处理(json文件可以识别,但无法获得依赖),需要编写相应的 loader。
编写 wxss-loader 获取 wxss 的依赖关系
wxss-loader 的作用就是让 Webpack 能识别并处理 wxss 文件,识别 wxss 文件后需要做什么呢?也就是 wxss-loader 接收到 wxss 文件的内容后该转换成什么?如果不做处理,直接返回 wxss 文件内容,那下一步 Webpack 会将 wxss 文件内容当成 js 代码去解析,得到的自然是解析失败。其实这里我们只需要知道当前 wxss 文件的依赖即可,wxss 的依赖即通过 @import
语法引入的其他 wxss 文件。通过正则/@import\s+['"]([^'"]* "'"")['"]/gi
,我们可以很简单地解析出依赖路径。得到路径后我们如何告诉 Webpack 这个路径是我们所依赖的呢?刚刚提到本次 loader 返回后的内容会被 Webpack 当成 js 去解析,所以我们需要构造一个 js 模块返回,在 js 模块中将正则解析到的路径 require
进去即可,接着 Webpack 便会自动递归解析所有 wxss 文件。例如,app.wxss 文件内容如下:
@import './common/color.wxss';
.fixed{
position: fixed;
}
经过 wxss-loader 后返回的内容如下,至于样式代码,则直接丢弃即可,至于如何获取内容后文会讲到。
require('./common/color.wxss');
module.exports = '';
本系列涉及的所有内容,都会提供源码供大家参考,wxss-loader 源码详见wecteam/dm[1]。
编写 wxml-loader 获取 wxml 及 wxs 的依赖关系
同 wxss-loader,只不过依赖资源的解析稍微复杂一点,首先 wxml 的引入可以通过 import
和 include
标签引入,另外 wxml 还可以 通过 wxs
标签引入 wxs 文件,跟 wxss-loader 相比,主要就是解析依赖的正则不同了:/<(import|include|wxs)\s+[^>]*src\s*=\s*['"]([^'"]* "^>]*src\s*=\s*['"")['"][^>]*>/gi
。
编写 wxs-loader 获取 wxs 的依赖关系
wxs-loader 的作用就简单了,让 Webpack 能识别 wxs 文件就行,由于 wxs 文件内容就是一个单独的 js 模块,loader 直接返回文件内容,交给 Webpack 做递归解析即可。
编写 wxjson-loader 获取组件的依赖关系
wxjson-loader 是针对页面配置的 json 文件,我们知道 Webpack4 默认是可以识别 json 文件的,但对于 json 文件的识别,获取到的是序列化的 json 对象,这个不是我们想要的,我们要的是 json 文件中 usingComponents 字段记录的当前页面对自定义组件的依赖,比如 index.json 中记录了对 nav 组件的依赖:
{
"navigationBarTitleText": "首页",
"usingComponents" : {
"nav" : "../../nav/nav",
}
}
由此我们通过路径边可以解析到 nav 组件的依赖:nav.wxml、nav.js、nav.json、nav.wxss 这个 4 个文件,跟前面的 loader 一样,仍然是构造一个 js 模块,将这个 4 个文件的依赖 require
进去,下一步交给 Webpack 去解析这个构造好的 js 模块,进一步递归获取组件各文件的依赖。
需要注意的是,Webpack4 对 json 文件的最终处理默认会去做 json 解析,而我们在 wxjson-loader 里已经将内容转成了一个 js 模块,因此我们需要将 wxjson-loader 的 type 设置为
javascript/auto
来告诉 Webpack:你得把我的 json 文件当 js 模块来解析。
组件的依赖可以是插件形式,路径会以
plugin://
开头,插件的代码在外部,不需要做依赖分析,直接略过即可。
如何处理图片字体等资源的依赖关系
图片资源,其实不太好处理,app.json 和 wxml 都可以使用相对路径的图片,app.json 中用于导航的图片路径可以直接解析,但用于 wxml 文件中的图片路径,则不太好处理了,因为这时候的路径往往是通过 setData 动态设置的,这种动态设置的图片只有在运行时才能确定其路径,没办法提前解析。所以针对图片字体等资源,一是建议除了用于导航的图片,其他页面的图片全部转到 CDN,尽量减少本地图片的使用,不管是对于减少小程序体积也好,提升启动速度也好,都有很大帮助。二是对于不关心小程序体积的那部分同学,图片及字体资源,直接按路径拷贝即可。
注意 app.json 中的其他依赖项
全局的
Component
配置。用于小程序内搜索的
sitemap.json
文件。用于插件功能页的
./functional-pages
目录,注意此目录不能引用其他目录文件,也不可被其他目录引用。
不同操作系统的路径问题
前面提到我们在各 loader 中解析文件路径后,会转换成 require(/path/to/file)
,如果大家看了源码,应该有注意到在借助 Webpack 的 context 计算完路径后做了一个 replace(/\\/g, '/')
处理,这个主要是因为在 win 系统生成路径 d:\\path\\to\\file
在经过 Webpack 解析时,每解析一次就会消耗一次反斜杠,最终会导致前面的路径变成 d:path ofile
,其中\t
被转换成了制表符,因此需要将其转成 POSIX 风格避免这个问题。
不规范的路径写法兼容
主要是在资源引用时,写法不规范,有以下几点:
js文件的引用,比如
require('util')
,就会有很大歧义,从规范 CommonJS 或者 AMD 来讲,这代表引用的是 util 包,但在小程序里,它代表引用当前目录的 util.js,而且它还有另外两种写法:require('./util')
、require('/util')
、最后一个以/
开头,一般是代表文件系统根目录,但小程序里它与./
是等价的,这些都会导致 Webpack 解析依赖失败,所以这些都需要在解析时做兼容处理。wxml、wxss、组件的引用,如果以
/
开头,代表的是小程序根目录,比如上述小程序目录,不管你在哪个层级的目录文件下使用@import /app.wxss
这个写法,都代表与 app.js 文件平级的 app.wxss 文件。字母开头等价于./
,如@import app.wxss
与@import ./app.wxss
是等价的。往上层级有多余,最终会定位到小程序根目录,如上述目录结构 index.wxss 文件中
@import ../../../../../app.wxss
,最终会找到与app.js 文件平级的 app.wxss 文件。
解决路径问题的时机
各 loader 针对的 wxss、wxs、wxml、json 文件,直接在 loader 中处理好路径问题即可
js 文件的路径问题,可以在 Webpack 构建 normalModule 时的 beforeResolve 钩子中处理,源码详见wecteam/dm[2]
五、结语
本篇主要是讲小程序代码如何做文件依赖分析,虽然通篇是拿微信小程序举例,但其他小程序同理,针对不同文件类型添加不同的 loader 即可。如支付宝小程序的 acss 文件,写个 acss-loader 来处理就好。下篇会讲如何获取依赖分析的结果,并将所有依赖资源打包成小程序需要的目录结构,同时也会讲一讲单页抽取。
参考资料
[1]
wecteam/dm: https://github.com/wecteam/dm/blob/master/packages/dm-cli/src/plugins/plugin-build/webpack-loaders/wxss-loader.ts
[2]wecteam/dm: https://github.com/wecteam/dm/blob/master/packages/dm-cli/src/plugins/plugin-build/webpack-plugins/deps-plugin.ts
如果你觉得这篇内容对你有价值,欢迎点赞并关注我们前端团队的官网和我们的微信公众号(WecTeam),每周都有优质文章推送: