众所周知,小程序主包的大小限制在 2M 之内,对于日益庞大的工程项目,开发者们无所不用其极地进行优化。原生小程序基本采用gulp
进行构建,可否在gulp
构建流程中作文章,达到“优化”的目的。css
首先明确gulp
构建优化的目的是压缩小程序包体积,提高用户开发体验。做者在熟悉gulp
开发后,推翻了原有的构建流程,从新设计,本着精益求精的目的进行优化,取得以下三点进展:vue
工具
--> 构建 npm
,生成小程序专用的npm
包才能成功编译代码,同时经常会由于各类路径引入问题致使不得不从新编辑或者从新构建 npm。优化后再无此类烦恼,gulp 构建时自动分析生成小程序专用的npm
包,大大提升了开发效率。对小程序来讲,除了app.js
做为程序入口以外,每一个 page 页面均可以做为一个页面入口,更倾向是固定路径模式的多页应用。gulp 构建的目的是将开发路径的代码翻译转到小程序专用路径,该路径下的代码可以被微信开发者工具读取、编译、构建。经过 gulp 工具可实现:node
.ts
文件编译为 .js
、 .less
文件编译为 .wxss
,以支持 TypeScript
、 Less
语法。sourcemaps
方便错误调试与定位。npm
包与小程序专用 npm
包。watch
,方便开发者调试。采用原生框架开发的小程序主要有.js
、.json
、.wxml
、.wxss
四种文件构成,为了提高开发效率,一般会引入.ts
和.less
文件。因为每种文件的编译构建方法不尽相同,所以须要为不一样类型的文件建立不一样的task
。webpack
const src = './src';
// 文件匹配路径
const globs = {
ts: [`${src}/**/*.ts`, './typings/index.d.ts'], // 匹配 ts 文件
js: `${src}/**/*.js`, // 匹配 js 文件
json: `${src}/**/*.json`, // 匹配 json 文件
less: `${src}/**/*.less`, // 匹配 less 文件
wxss: `${src}/**/*.wxss`, // 匹配 wxss 文件
image: `${src}/**/*.{png,jpg,jpeg,gif,svg}`, // 匹配 image 文件
wxml: `${src}/**/*.wxml`, // 匹配 wxml 文件
other:[`${src}/**`,`!${globs.ts[0]}`,...] // 除上述文件外的其它文件
};
// 建立不一样的task
const ts = cb => {}; // 编译ts文件
const js = cb => {}; // 编译js文件
...
const copy = cb => {}; // 除上述文件外的其它文件复制到目标文件夹
复制代码
如约定俗成通常,咱们一般在dev
模式下进行开发调试,在build
模式下进行发布,这两种的 gulp 构建方案是须要区分的。如代码所示:dev
模式下须要添加watch
来监听文件的变化,及时地从新进行构建,同时须要添加sourcemap
便于调试;而build
模式下更须要的是对文件进行压缩以减小包体积。git
// 默认dev模式配置
let config = {
sourcemap: true, // 是否开启sourcemap
compress: false, // 是否压缩wxml、json、less等各类文件
...
};
// 修改为build模式配置
const setBuildConfig = cb => {
config = {...}; cb();
};
// 并发执行全部文件构建task
const _build = gulp.parallel(copy, ts, js, json, less, wxss, image, wxml);
// build模式构建
const build = gulp.series(
setBuildConfig, // 设置成build模式配置
gulp.parallel(clear, clearCache), // 清除目标目录文件和缓存
_build, // 并发执行全部文件构建task
...
);
// dev模式构建
const build = gulp.series(
clear, // 清除目标目录文件
_build, // 并发执行全部文件构建task
...
watch, // 添加监听
);
复制代码
前面的篇幅讲述了 gulpfile 文件总体架构设计,下面讲述每一个task
的具体配置,以及优化之道。github
① 安装 npm 包web
安装 npm 包的方法有如下两种:typescript
package.json
所在的目录中执行命令npm install
安装 npm 包,此处要求参与构建 npm 的 package.json
须要在 project.config.js
定义的 miniprogramRoot
以内。gulp
的task
进行处理,建立一个task
,将根目录的package.json
文件拷贝到小程序所在目录(本文命名为miniprogram
),经过exec
执行cd miniprogram && npm install --no-package-lock --production
命令安装 npm 包。代码以下所示:gulp.task('module:install', (cb) => {
const destPath = './miniprogram/package.json';
const sourcePath = './package.json';
try {
// ...省略代码,判断是否有 package.json 的变更,无变更则返回
// 复制文件
fs.copyFileSync(path.resolve(sourcePath), path.resolve(destPath));
// 执行命令
exec(
'cd miniprogram && npm install --no-package-lock --production',
(err) => {
if (err) process.exit(1);
cb();
}
);
} catch (error) {
// ...
}
});
复制代码
② 构建 npm 包npm
众所周知,咱们使用npm install
构建的 npm 包都会在node_modules
目录,可是小程序规定node_modules
目录不会参与编译、上传和打包中,因此小程序想要使用 npm 包必须走一遍 「构建 npm」 的过程,即点击开发者工具中的菜单栏:工具
--> 构建 npm
。这时,node_modules
的同级目录下会生成一个 miniprogram_npm
目录,里面会存放构建打包后的 npm 包,也就是小程序真正使用的 npm 包。 json
如上图所示,小程序真正使用的 npm 包和node_modules
目录下的 npm 包的结构是有差别的,这个差别就是点击构建 npm
这个操做执行的打包过程,分为两种:小程序 npm 包会直接拷贝构建文件生成目录下的全部文件到 miniprogram_npm
中;其余 npm 包则会从入口 js 文件开始走一遍依赖分析和打包过程(相似 webpack
)。
显而易见,官方提供的 npm 构建方案暴露了如下两点问题:
gulp
打包构建小程序时,第一个task
就是在miniprogram
目录安装 npm 包,当依赖的 npm 包数量够多时,会消耗大量的时间。工具
--> 构建 npm
,对开发者十分不友好。所以咱们但愿能借助gulp
工做流,在小程序构建时分析每一个文件的依赖关系,将须要的 npm 包拷贝至 miniprogram_npm
目录。一步到位,直接省略了上述安装npm包
和构建npm包
两个步骤。
所幸,社区无所不能,有做者开发了一个用以小程序提取 npm 依赖包的 gulp
插件 gulp-mp-npm
,有如下特色:
前面提到过构建npm包
时,针对小程序 npm 包会直接拷贝构建文件生成目录下的全部文件到 miniprogram_npm
中;对于其它普通 npm 包则会从入口 js 文件开始走一遍依赖分析和打包过程(相似 webpack
),打包生成的代码在同级目录下会生成 source map
文件,方便进行逆向调试。而gulp-mp-npm
插件仅仅经过依赖分析,提取使用到的依赖和组件,将其复制到至 miniprogram_npm
目录下对应的 npm 包文件夹内,并不会对依赖进行编译与打包,这一步交由微信开发者工具完成。
gulp-mp-npm
的构建原理如上图所示,想要深刻了解能够参考: 小程序 npm 依赖包 gulp 插件设计。
插件的使用能够根据项目需求,在 gulpfile.js
中进行以下配置:
const gulp = require('gulp');
const mpNpm = require('gulp-mp-npm')
const js = () => gulp.src('src/**/*.js')
.pipe(mpNpm()) // 分析提取 js 中用到的依赖
.pipe(gulp.dest('dist'));
const less = () => gulp.src('src/**/*.less')
.pipe(gulpLess()) // 编译less
.pipe(rename({ extname: '.wxss' }))
.pipe(mpNpm()) // 分析提取 less 中用到的依赖
.pipe(gulp.dest('dist'));
...
复制代码
一般在.ts
、.js
、.json
、.less
、.wxss
等文件中都会有可能使用到npm 依赖。所以,插件 gulp-mp-npm
在上述 5 个 tasks
中都需执行。分析 .json
文件是由于插件会尝试读取小程序页面配置中 usingComponents
字段,提取使用的 npm 小程序组件。
众所周知,gulp
的task
都是单次执行的。某个文件变化后 gulp.watch
会从新执行整个 task
来完成构建,这样会致使未变化的文件重复构建,效能较低。能够采起如下两个措施进行效率优化:
task
,合理建立watch
前面提到,根据文件类型不一样,划分红copy
, ts
, js
, json
, less
, wxss
, image
, wxml
八种不一样的task
,分别为每种task
建立watch
。假设修改index.ts
文件,只会从新执行ts
这个task
,其它task
不受影响。代码以下所示:
const watchOptions = { events: ['add', 'change', `unlink`] };
const watch = () => {
gulp.watch(globs.copy, watchOptions, copy);
gulp.watch(globs.ts, watchOptions, ts);
gulp.watch(globs.js, watchOptions, js);
gulp.watch(globs.json, watchOptions, json);
gulp.watch(globs.less, watchOptions, less);
gulp.watch(globs.wxss, watchOptions, wxss);
gulp.watch(globs.image, watchOptions, image);
gulp.watch(globs.wxml, watchOptions, wxml);
};
复制代码
gulp.lastRun
实现增量编译在任何构建工具中增量编译都是一个必不可少的优化方式。即在编译过程当中仅编译那些修改过的文件,能够减小许多没必要要的资源消耗,也能减小编译时长。gulp 4
发布以前社区早早给出了一系列的解决方案,gulp-changed
、gulp-cached
、gulp-remember
、gulp-newer
等等。gulp 4
发布自带了增量更新的方案gulp.lastRun()
。
gulp.lastRun
方法返回当前运行进程中成功完成 task
的最后一次时间戳。将其做为 gulp.src
方法的参数 since
传入,将每一个文件的mtime
(文件内容最后被修改的时间)与since
传入的值进行比较,可实现跳过自上次成功完成任务以来没有更改的文件,实现增量编译,加快执行时间。使用方法以下所示:
/* 以 ts / less 为例, js / json / wxss / copy / image 同理 */
const ts = () => gulp.src(
'src/**/*.ts',
{ since: gulp.lastRun(ts) }
)...
const less = () => gulp.src(
'src/**/*.less',
{ since: gulp.lastRun(less) }
)...
复制代码
然而,该方法仅经过判别文件内容的修改来实现增量编译,那对于未修改的文件的移动又该如何?好比将某个 js 文件复制到另一个目录中,该文件的mtime
(文件内容最后被修改的时间)是不会发生变化的,这时,ctime
(写入文件、更改全部者、权限或连接设置的最后修改时间)派上用场。代码改造以下,封装了since
函数,当文件内容未变化,但文件路径发生改变时,返回时间戳为 0,将文件的mtime
(文件内容最后被修改的时间)与since
传入的值(此时为 0)对比,就能够增量编译该文件。
const since = task => file =>
gulp.lastRun(task) > file.stat.ctime ? gulp.lastRun(task) : 0;
const ts = () => gulp.src(
'src/**/*.ts',
{ since: since(ts) }
)
复制代码
gulp-sourcemaps
这是一款用来生成映射文件的一个插件,SourceMap 文件记录了一个存储源代码与编译代码对应位置映射的信息文件。咱们在调试时都是没办法像调试源码般轻松,这就须要 SourceMap
帮助咱们在控制台中转换成源码,从而进行 debug
。gulp-sourcemaps
主要用于解决代码混淆、typescript
和less
语言转换编译成js
和css
语言的问题。
使用 gulp-sourcemaps
插件,可为参与编译的 .ts
、 .less
文件开启 Source Map :
const sourcemaps = require('gulp-sourcemaps');
/* 以 ts 为例, less 同理 */
const ts = () => gulp.src('src/**/*.ts')
.pipe(sourcemaps.init())
.pipe(tsProject()) // 编译ts
.pipe(mpNpm()) // 分析提取 ts 中用到的依赖
.pipe(sourcemaps.write('.')) // 以 .map 文件形式导出至同级目录
.pipe(gulp.dest('dist'));
复制代码
注意:Source Map 文件不计入代码包大小计算,即编译上传的代码是不计算这部分体积的。开发版代码包中因为包含了 .map
文件,实际代码包大小会比体验版和正式版大。
编译 ts 的目标是将.ts
文件转换编译成.js
文件输出到目标文件夹。主要分为如下几个步骤:
Vinyl
对象。设置since
属性,利用gulp.lastRun
进行增量编译。gulp-ts-alias
插件处理路径别名问题,该插件根据tsconfig.json
文件中paths
属性的的配置内容,将别名替换为原路径。好比paths
有一条配置以下"@/*": ["src/supermarket/*"]
,那么对于任意.ts
文件中的import A from '@/components'
都会替换成import A from 'src/supermarket/components'
。gulp-if
来进行条件判断,当config.sourcemap
值为 true 时,才执行sourcemaps.init()
,不然流直接通向下一个 pipe。gulp-typescript
插件将.ts
文件转换编译成.js
文件。首先,在ts
这个task
外面使用gulpTs.createProject
建立一个 ts 编译任务,之因此在外面建立的缘由是当运行watch
时,有.ts
文件进行修改,须要从新构建ts
这个task
,在外面建立能够节省一半的时间。在默认配置下,有 ts 的编译错误会输出到控制台,而且编译器会由于编译错误而使这个构建任务崩溃,中断运行。因此须要在tsProject()
后面添加一个错误处理程序来捕获错误。gulp-mp-npm
插件提取.ts
文件引入的 npm 包。sourcemap
写入到目标目录。gulp-uglify
对.js
文件进行压缩。.js
文件输出到对应目录。const gulpTs = require('gulp-typescript');
const tsAlias = require('gulp-ts-alias');
const gulpIf = require('gulp-if');
const uglifyjs = require('uglify-js');
const composer = require('gulp-uglify/composer');
const minify = composer(uglifyjs, console);
const pump = require('pump');
const tsProject = gulpTs.createProject(resolve('tsconfig.json')); // 4. 外部建立一个ts编译任务
const ts = cb => {
const tsResult = gulp
.src(globs.ts, { ...srcOptions, since: since(ts) }) // 1. 增量编译
.pipe(tsAlias({ configuration: tsProject.config })) // 2. 将路径别名替换为原路径
.pipe(gulpIf(config.sourcemap, sourcemaps.init())) // 3. dev模式开启sourcemap
.pipe(tsProject()) // 4. 编译ts
.on('error', () => { // 4. 捕获错误,不添加会由于ts编译错误致使任务中断
/** 忽略编译器错误**/
});
pump(
[
tsResult.js,
mpNpm(mpNpmOptions), // 5. 分析依赖
gulpIf(config.sourcemap.ts, sourcemaps.write('.')), // 6. 写入sourcemap文件到对应的目录
gulpIf(config.compress, minify({})), // 7. build模式压缩js
gulp.dest(dist), // 8. 输出文件到目标目录
],
cb,
);
};
复制代码
眼尖的读者应该注意到了task
的后半段改用pump
将流连接在一块儿。Pump
是一个小型节点模块,可将流链接在一块儿并在其中任何一个关闭时将其所有销毁。通俗来说,就是可使咱们更容易定位错误的发生点,经常使用来替代pipe
,很是适合于修复gulp-uglify
报错。以下图所示,使用pump
后的报错能准肯定位到具体的位置,而pipe
抛出整个调用栈,让人无从下手。
编译 less 的目标是将.less
文件转换编译成.wxss
文件输出到目标文件夹。主要分为如下几个步骤:
const gulpLess = require('gulp-less');
const weappAlias = require('gulp-wechat-weapp-src-alisa');
const prettyData = require('gulp-pretty-data');
const less = cb => {
pump(
[
gulp.src(globs.less, { ...srcOptions, since: since(less) }), // 1. 增量编译
gulpIf(config.sourcemap.less, sourcemaps.init()), // 2. 开启sourcemap
weappAlias(weappAliasConfig), // 3. 将路径别名替换成原路径
/** 此处省略 步骤A */
gulpLess(), // 4. 编译less转换成css
/** 此处省略 步骤B */
rename({ extname: '.wxss' }), // 5. 文件.less后缀修改成.wxss
mpNpm(mpNpmOptions), // 6. 依赖分析
gulpIf(config.sourcemap.less, sourcemaps.write('.')), // 7. 写入sourcemap
gulpIf(
config.compress, // 8. build模式下压缩wxss
prettyData({
type: 'minify',
extensions: {
wxss: 'css',
},
}),
),
gulp.dest(dist), // 9. 输出文件到目标目录
],
cb,
);
};
复制代码
如上所示,使用gulp-less
插件将LESS
代码编译成CSS
代码,他的编译原理以下所示:
index.less
文件引入variable.less
变量文件,会将variable.less
的内容复制到index.less
文件中。index.less
文件引入style.less
纯样式文件,会将style.less
所有的内容复制到index.less
文件中。.less
文件编译,将使用到的变量替换成对应的值;样式嵌套平铺。style.less
和variable.less
文件的内容。设想一下,假设有 100 个文件引入style.less
纯样式文件,那么style.less
的内容便要复制一百份。对于复杂的工程项目,less
文件的依赖是十分复杂的,编译的结果是形成许多冗余的样式代码,与咱们“精益求精”(尽量地压缩小程序包)的理念背道而驰。基于此,咱们能够在gulp-less
插件编译前,将@import **
相关的代码注释掉,gulp-less
插件编译后,恢复注释的内容,并将引入路径的.less
后缀修改成.wxss
后缀,代码以下所示:
/** 前面代码片断省略的步骤A代码 */
tap(file => {
const content = file.contents.toString(); // 将文件内容toString()
const regNotes = /\/\*(\s|.)*?\*\//g; // 匹配 /* */ 注释
const removeComment = content.replace(regNotes, ''); // 删除注释内容
const reg = /@import\s+['|"](.+)['|"];/g; // 匹配 @import ** 路径引入
const str = removeComment.replace(reg, ($1, $2) => {
const hasFilter = cssFilterFiles.filter(item => $2.indexOf(item) > -1); // 过滤掉变量文件引入
let path = hasFilter <= 0 ? `/** less: ${$1} **/` : $1; // 将纯样式文件的引入 添加注释 /** less: ${$1} **/
return path;
});
file.contents = Buffer.from(str, 'utf8'); // string恢复成文件流
});
复制代码
这里须要注意的是,若是注释掉变量文件,好比上述提到的variable.less
文件,那么引入的变量例如@color-primary
就会取不到值,致使编译出错。所以处理时,能够写一个数组cssFilterFiles
过滤掉变量文件,而后再注释全部的样式文件,好比上述提到的style.less
纯样式文件。
执行步骤 A 代码后,紧接着使用gulp-less
将LESS
代码编译成CSS
代码,以后在执行步骤 B 代码,以下所示,将路径注释还原,并将引入路径的.less
后缀修改成小程序能识别的.wxss
后缀。
/** 前面代码片断省略的步骤B代码 */
tap(file => {
const content = file.contents.toString();
const regNotes = /\/\*\* less: @import\s+['|"](.+)['|"]; \*\*\//g;
const reg = /@import\s+['|"](.+)['|"];/g;
const str = content.replace(regNotes, ($1, $2) => {
let less = '';
$1.replace(reg, $3 => (less = $3));
return less.replace(/\.less/g, '.wxss');
});
file.contents = Buffer.from(str, 'utf8');
});
复制代码
优化后的效果以下图所示:
小程序主包目前只支持最多 2M 的大小,而图片一般是占用空间最多的资源。由于在项目中对图片大小进行压缩颇有必要的。
使用 gulp-image
插件,可压缩图片大小且能保证画质:
const gulpImage = require('gulp-image');
const cache = require('gulp-cache');
const image = () =>
gulp
.src(globs.image, { ...srcOptions, since: since(image) })
.pipe(cache(gulpImage())) // 缓存压缩后的图片
.pipe(gulp.dest(dist));
复制代码
以下图所示,是项目全部图片压缩的结果,平均能节省 50%左右的体积。
.js
、.json
、.wxml
、.wxss
编译的主要原理是将源文件拷贝至目标文件目录。代码以下所示:
const prettyData = require('gulp-pretty-data');
const wxml = () =>
gulp
.src(globs.wxml, { ...srcOptions, since: since(wxml) }) // 1. 增量编译
.pipe(
gulpIf(
config.compress, // 2. build模式下压缩文件
prettyData({
type: 'minify',
extensions: {
wxml: 'xml',
},
}),
),
)
.pipe(gulp.dest(dist)); // 3. 输出到对应目录
// .json、.wxml、.wxss代码参考如上
复制代码
在微信开发者工具中,点击右上角 详情
--> 本地设置
--> 选中上传代码时自动压缩样式
和 上传代码时自动压缩混淆
,那么在小程序构建打包时会压缩.wxss
文件和.js
文件,因此在实际的 gulp 构建工做流中,能够不对.ts
、.js
、.less
、.wxss
进行压缩处理,但对于.wxml
和.json
文件,可使用gulp-pretty-data
插件对文件进行压缩。
小程序开发最大的限制是主包大小必须控制在 2M 之内,经过本文的优化方法,将本来的小程序主包体积减少 11.9%,想要进一步压缩体积,能够考虑tree-sharking
,分析代码依赖关系,剔除未引用的代码。但现有的gulp
工做流架构模式是很难完美地实现这个功能的,tree-sharking
是rollup
、webpack
等构建方案的特长,这也是为啥有一部分红熟的小程序框架如taro
、mpvue
会选择webpack
构建。其实,各有所长,全看取舍。
附上代码连接: codesandbox.io/s/miniprogr…