小程序 - 代码压缩实践
起因
随着业务的发展,项目不断地迭代,功能模块越加越多,项目代码和静态资源文件体积已经超过了微信小程序限定的 2M 范围。虽说小程序支持分包操作,然而用户进入分包模块时会有一个比较长的加载时间,整体体验还是不友好的,万不得已不要分包。既然不分包,那么我们可以从哪方面来进行项目体积的“瘦身”呢?
文件层
通过分析小程序的项目目录我们可知,小程序的文件主要有 .js
.wxs
.wxss
.wxml
.json
后缀的文件和一些图片资源文件,如果对这些文件进行压缩,项目的体积至少能缩减 10%。微信开发者工具有提供代码压缩的配置,只要上传代码前勾选对应配置即可。这对于一般小程序项目来说是挺方便的,然而对于使用了第三方框架(如:wepy、taro)的项目,这些功能基本框架代码编译层面就进行来处理,无需在开发者工具中勾选。
既然这么完备了,我们还有必要自行编写代码来对这些文件进行压缩么?其实,实际上上面提到的都会有些不足的地方,如官方提供的压缩做不到图片的压缩,第三方方框架不会对部分文件进行压缩。所以根据我们的需求,自行写一套自动化压缩流程才是最稳妥的。
自动化压缩
要实现自动化压缩流程,可使用 gulp、grunt、webpack 等构建工具来进行构建。由于自己比较熟悉 gulp 故使用它来进行构建流程的编写。gulp 的使用比较简单,只要在项目根目录新建一个 gulpfile.js
文件,安装相关依赖后就能运行。
// 安装
npm install gulp-cli -g
npm install --save-dev gulp
// 执行(默认执行根目录 gulpfile.js 文件)
gulp
因为 gulpfile.js
文件还没写内容,所以打印只能看到 ‘default’ 任务的执行记录。下面我们使用 gulp 4.x 的版本进行开发,旧版本写法可能会有些出入,不过大致原理是一样的。
// gulpfile.js
'use strict'
const { task, dest, src, series } = require('gulp')
task('js', function(){
// 压缩 .js
})
task('wxml', function(){
// 压缩 .wxml
})
// 省略...
task(
'default',
series([
'js',
'wxml',
//...
])
)
.js 压缩
原本是使用 gulp-uglify 这个库进行压缩的,后发现其不支持 es6语法,所以转用 gulp-uglify-es,两者配置有些出入。小程序的 js 文件因其语法和标准的 JavaScript 语法是一样的,故无需额外配置,直接默认配置压缩即可。
const uglify = require('gulp-uglify-es').default
task('js', function () {
return src('dist/**/*.js')
.pipe(uglify())
.pipe(dest('dist/'))
})
第三方框架 wepy 或者 taro 都会对 .js
文件进行压缩,正常情况下我们无需二次压缩。
.wxs 压缩
WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。
WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。
正常情况下 .wxs
文件可以和 .js
一样可通过 gulp-uglify-es 来进行压缩,不过由于微信坑爹地搞特殊化,wxs 就像严重阉割版的 js,在压缩后会出现各种问题。
task('wxs', function () {
return src(['dist/**/*.wxs', '!dist/components/vant/wxs/add-unit.wxs'])
.pipe(uglify({
compress: {
ie8: true, // 支持 ie8,为了禁止 a === undefined 自动转换为 void 0 === a
join_vars: false // 禁止合并 var
}
}))
.pipe(dest('dist/'))
})
代码中 "!dist/components/vant/wxs/add-unit.wxs" 是排除掉了此文件,原因是暂无方法解决压缩后部分正则无法正常使用的问题。
// 压缩前
var REGEXP = getRegExp('^\d+(\.\d+)?$');
// 压缩后
var REGEXP = getRegExp('^d+(.d+)?$');
微信的 getRegExp 并不像 new RegExp 那样支持双反斜杆进行字符转义,巨坑!!!老实说,如果官方不出专门的压缩库,感觉最好还是不要压缩这个东西。
.wxss 压缩
样式和常规的的 css 文件的差不多,因此可以通过一般的样式工具进行压缩,比如 gulp-clean-css 第三方库。
const cleanCss = require('gulp-clean-css')
const replaceimport = require('./replaceImport')
task('wxss', function () {
return src('dist/**/*.wxss')
.pipe(cleanCss({ compatibility: '*', inline: false }))
.pipe(replaceimport())
.pipe(dest('dist/'))
})
由于压缩老版本 wepy 项目样式时,组件引入样式 @import "xxx"
会被转成 @import url('xxx')
在开发者工具中会报错,故写了 replaceimport 转回来,taro 项目无需加。
// replaceimport.js
'use strict';
/**
* 由于使用 gulp-clean-css 进行样式压缩时
* 会将 `@import "xxx";` 转成 `@import url(xxx);`
* 而转化后的 import 在小程序开发者工具会报错
* 所以用此方法,将 @import url(xxx) 转回 @import 'xxx'
*/
const through = require('through2')
module.exports = () => through.obj(function (file, enc, next) {
if (file.isNull()) {
next(null, file);
return;
}
try {
const reg = /(?:^|\s)?(?:@import)(?:\s)(?:url)?(?:(?:(?:\()(["'])?(?:https?:)?([^"')]+)\1(?:\))|(["'])(?:.+)\2)(?:[A-Z\s])*)+(?:;)/ig
file.contents = new Buffer.from(file.contents.toString().replace(reg, '@import "$2";'))
next(null, file)
} catch (error) {
next(null, file)
}
});
.wxml 压缩
WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。
其实跟 HTML 类似的标签语言,我们可以通过现有的 HTML 压缩库来对其进行压缩。这里我们可以用 gulp-htmlmin 。
const htmlmin = require('gulp-htmlmin')
task('wxml', function () {
return src('dist/**/*.wxml').pipe(htmlmin({
caseSensitive: true, // 大小写敏感
removeComments: true, // 删除 HTML 注释
keepClosingSlash: true, // 单标签上保留斜线
collapseWhitespace: true,// 压缩 HTML
ignoreCustomFragments: [
/<input([\s\S]*?)<\/input>/
],
})).pipe(dest('dist/'))
})
用来压缩 taro 的 base.wxml 文件时会报错,原因是 <input></input>
标签被压缩成 <input>
了,小程序 input 标签必须要有斜杠结尾,否则会报错。因此,我们在 ignoreCustomFragments
通过设置正则排除掉 <input></input>
这种情况。
.json 压缩
要压缩 json 文件,最简单的方法是直接 JSON.parse(JSON.stringify('jsonString'))
,既然我们用了 gulp ,那就用第三方库 gulp-jsonminify 吧,方便维护。
const jsonminify = require('gulp-jsonminify')
task('json', function () {
return src('dist/**/*.json')
.pipe(jsonminify())
.pipe(dest('dist/'))
})
图片压缩
一般情况下第三方框架都会有对图片进行压缩处理的,如果是小程序原生的项目,可以安装 gulp-imagemin 对图片进行压缩。
const imagemin = require('gulp-imagemin')
task('json', function () {
return src('dist/*')
.pipe(jsonminify())
.pipe(dest('dist/'))
})
代码层
微信开发者工具自带的代码分析工具,我们可以很直观地知道哪个文件夹、哪个文件比较大,分析后专门对比较大的文件进行优化即可。
替换较小第三方库
有些第三方包是可以替换的,比如早期常用的 moment.js 可用 dayjs 替换,只需要做一些转化,体积能减少将近 40K。再比如加解密用的 crypto-js,我们通常只用到其中部分模块,旧版本 wepy 是没有做 tree-shaking 的,需要手动移除无用模块。还有些第三方库,项目中只用到其中一些简单的功能,大可自行写一个,不必使用第三方库。
样式引入方式更改
通过分析打包后的 .wxss
文件,发现 wepy 编译压缩 less
样式时,less 会将 @import
引用的文件直接编译进来,造成文件体积的增大,比如
/* a.less */
.a{
color: red;
}
/* b.less */
@imoprt 'a.less';
.b{
color: black;
}
/* output: b.wxss */
.a{
color: red;
}
.b{
color: black;
}
要解决这个问题,只需改成 [@import (css)](https://lesscss.org/features/#import-atrules-feature)
/* b.less */
@imoprt (css) 'a.less';
.b{
color: black;
}
/* output: b.wxss */
@import 'a.wxss';
.b{
color: black;
}
改完这个,估计项目体积又减少了 200K 左右。在改的时候页面众多,要进行更改,比较合理的做法是通过正则表达式进行全局替换。还可能涉及到批量重命名文件后缀,由于不同的编辑器和操作系统不同,就不展开说了。
删减无关配置
删除某个页面或者模块后,记得同时删除其页面配置路径。由于 wepy 会根据页面路径配置去生成页面,如果页面已删除,页面配置 pages 中的路径没删除,执行 build 打包后它会自动生成一个空白的基础页面。
简化文件层级
这个其实既是文件层亦是代码层,原因是,如果你的文件层级越多,在引入的时候路径就越长,占用的字节数就会越多。如果有上百处引用的化,还是挺占体积的。
字体图标提取
分析样式文件还发现一个问题,就是存在比较多的零散的字体图标,其使用方式是通过 @font-face
以字体 base64 的形式进行引入。这样做的好处是图标不会失真,存放在样式中页面样式加载完成即可显示,不好的地方是不方便管理,不好复用。要解决这个问题常用的方法是使用第三方的 UI 组件库,一般成熟的组件库都会有配套的图标组件,还有一种做法是自行在阿里图标网站新建一个图标目录将用到的字体图标上传上去,然后通过阿里图标网站生成字体图标文件,将文件下载下来放到服务器存着,用的时候使用链接引入就好。当然,为了方便管理最好是在本地写脚本去自动执行下载和上传,同时做版本管理方便后续维护更新。
总结
经过一顿操作,老项目的体积从 2M 变成了 1.5M,既兴奋又有点不好意思,不好意思是因为之前写的代码太多不足了。不过,好在如今小程序的生态越来越完备了,很多东西都无需从零开始,现在在做的新项目都会考虑比较全面。虽然时常想吐槽微信小程序的各种不友好,可回过头来看看这几年的小程序开发,自己何不尝是从一个编程小白一步步地在成长,说起来还是要感谢微信赏饭吃呢。