小程序gulp构建优化之道

1. 背景

众所周知,小程序主包的大小限制在 2M 之内,对于日益庞大的工程项目,开发者们无所不用其极地进行优化。原生小程序基本采用gulp进行构建,可否在gulp构建流程中作文章,达到“优化”的目的。css

首先明确gulp构建优化的目的是压缩小程序包体积,提高用户开发体验。做者在熟悉gulp开发后,推翻了原有的构建流程,从新设计,本着精益求精的目的进行优化,取得以下三点进展:vue

  1. 小程序包体积减少:优化前包体积 2212KB,主包体积 1668.9KB;优化后包体积 2012KB,主包体积 1470.7KB;包体积减少 9.04%,主包体积减少 11.9%
  2. 构建时间缩短:优化前 dev 模式构建时间 58 秒,build 模式构建时间 62 秒;优化后 dev 模式构建时间 27 秒,build 模式构建时间 32 秒;dev 模式构建时间缩短 53.4%,build 模式构建时间缩短 48.4%
  3. 用户开发体验提高:优化前须要点击开发者工具中的菜单栏:工具 --> 构建 npm,生成小程序专用的npm包才能成功编译代码,同时经常会由于各类路径引入问题致使不得不从新编辑或者从新构建 npm。优化后再无此类烦恼,gulp 构建时自动分析生成小程序专用的npm包,大大提升了开发效率。

2. gulp 工做流架构

gulp构建优化

对小程序来讲,除了app.js做为程序入口以外,每一个 page 页面均可以做为一个页面入口,更倾向是固定路径模式的多页应用。gulp 构建的目的是将开发路径的代码翻译转到小程序专用路径,该路径下的代码可以被微信开发者工具读取、编译、构建。经过 gulp 工具可实现:node

  • .ts 文件编译为 .js.less 文件编译为 .wxss ,以支持 TypeScriptLess 语法。
  • 支持 sourcemaps 方便错误调试与定位。
  • 压缩图片和各种文件,减小小程序代码包大小。
  • 分析代码,依赖自动提取,支持提取普通 npm 包与小程序专用 npm 包。
  • 其他文件将直接拷贝至目标路径。
  • 添加watch,方便开发者调试。

2.1 拆分 task

采用原生框架开发的小程序主要有.js.json.wxml.wxss四种文件构成,为了提高开发效率,一般会引入.ts.less文件。因为每种文件的编译构建方法不尽相同,所以须要为不一样类型的文件建立不一样的taskwebpack

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 => {}; // 除上述文件外的其它文件复制到目标文件夹
复制代码

2.2 dev 和 build 模式区分

如约定俗成通常,咱们一般在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, // 添加监听
);
复制代码

3. 优化之道

前面的篇幅讲述了 gulpfile 文件总体架构设计,下面讲述每一个task的具体配置,以及优化之道。github

3.1 npm 构建优化

3.1.1 官方通用方案的不足之处

① 安装 npm 包web

安装 npm 包的方法有如下两种:typescript

  1. 手动档: 在小程序 package.json 所在的目录中执行命令npm install安装 npm 包,此处要求参与构建 npm 的 package.json 须要在 project.config.js 定义的 miniprogramRoot 以内。
  2. 自动档: 经过gulptask进行处理,建立一个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 包。 构建npm先后目录结构变化json

如上图所示,小程序真正使用的 npm 包和node_modules目录下的 npm 包的结构是有差别的,这个差别就是点击构建 npm这个操做执行的打包过程,分为两种:小程序 npm 包会直接拷贝构建文件生成目录下的全部文件到 miniprogram_npm 中;其余 npm 包则会从入口 js 文件开始走一遍依赖分析和打包过程(相似 webpack)。

显而易见,官方提供的 npm 构建方案暴露了如下两点问题:

  1. 耗时长:经过gulp打包构建小程序时,第一个task就是在miniprogram目录安装 npm 包,当依赖的 npm 包数量够多时,会消耗大量的时间。
  2. 开发流程繁琐:经过小程序开发者工具调试代码时,当引入新的 npm 包或者原有的 npm 包更新时,都要从新点击菜单栏:工具 --> 构建 npm,对开发者十分不友好。

所以咱们但愿能借助gulp工做流,在小程序构建时分析每一个文件的依赖关系,将须要的 npm 包拷贝至 miniprogram_npm目录。一步到位,直接省略了上述安装npm包构建npm包两个步骤。

3.1.2 gulp-mp-npm 实现依赖分析与提取

所幸,社区无所不能,有做者开发了一个用以小程序提取 npm 依赖包的 gulp 插件 gulp-mp-npm ,有如下特色:

  • 依赖分析,仅会提取使用到的依赖与组件。
  • 支持提取普通 npm 包与小程序专用 npm 包。
  • 不会对依赖进行编译与打包(交给微信开发者工具或者其余 gulp 插件完成)。
  • 兼容官方方案及原理,同时支持自定义 npm 输出文件夹。

前面提到过构建npm包时,针对小程序 npm 包会直接拷贝构建文件生成目录下的全部文件到 miniprogram_npm 中;对于其它普通 npm 包则会从入口 js 文件开始走一遍依赖分析和打包过程(相似 webpack),打包生成的代码在同级目录下会生成 source map 文件,方便进行逆向调试。而gulp-mp-npm插件仅仅经过依赖分析,提取使用到的依赖和组件,将其复制到至 miniprogram_npm目录下对应的 npm 包文件夹内,并不会对依赖进行编译与打包,这一步交由微信开发者工具完成。

gulp-mp-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 小程序组件。

3.1.3 数据对比

npm构建优化数据对比

3.2 watch 增量编译

众所周知,gulptask都是单次执行的。某个文件变化后 gulp.watch 会从新执行整个 task 来完成构建,这样会致使未变化的文件重复构建,效能较低。能够采起如下两个措施进行效率优化:

3.2.1 拆分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);
};
复制代码

3.2.2 gulp.lastRun实现增量编译

在任何构建工具中增量编译都是一个必不可少的优化方式。即在编译过程当中仅编译那些修改过的文件,能够减小许多没必要要的资源消耗,也能减小编译时长gulp 4发布以前社区早早给出了一系列的解决方案,gulp-changedgulp-cachedgulp-remembergulp-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) }
)
复制代码

3.2.3 数据对比

增量编译数据对比

3.3 开启 sourcemap

gulp-sourcemaps这是一款用来生成映射文件的一个插件,SourceMap 文件记录了一个存储源代码与编译代码对应位置映射的信息文件。咱们在调试时都是没办法像调试源码般轻松,这就须要 SourceMap 帮助咱们在控制台中转换成源码,从而进行 debuggulp-sourcemaps主要用于解决代码混淆、typescriptless语言转换编译成jscss语言的问题。

使用 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 文件,实际代码包大小会比体验版和正式版大。

3.4 编译 ts

编译 ts 的目标是将.ts文件转换编译成.js文件输出到目标文件夹。主要分为如下几个步骤:

  1. 建立一个流,用于从文件系统读取 Vinyl 对象。设置since属性,利用gulp.lastRun进行增量编译。
  2. 引入gulp-ts-alias插件处理路径别名问题,该插件根据tsconfig.json文件中paths属性的的配置内容,将别名替换为原路径。好比paths有一条配置以下"@/*": ["src/supermarket/*"],那么对于任意.ts文件中的import A from '@/components'都会替换成import A from 'src/supermarket/components'
  3. dev 模式下开启 sourcemap, 引入gulp-if来进行条件判断,当config.sourcemap值为 true 时,才执行sourcemaps.init(),不然流直接通向下一个 pipe。
  4. 引入gulp-typescript插件将.ts文件转换编译成.js文件。首先,在ts这个task外面使用gulpTs.createProject建立一个 ts 编译任务,之因此在外面建立的缘由是当运行watch时,有.ts文件进行修改,须要从新构建ts这个task,在外面建立能够节省一半的时间。在默认配置下,有 ts 的编译错误会输出到控制台,而且编译器会由于编译错误而使这个构建任务崩溃,中断运行。因此须要在tsProject()后面添加一个错误处理程序来捕获错误。
  5. 引入gulp-mp-npm插件提取.ts文件引入的 npm 包。
  6. sourcemap 写入到目标目录。
  7. build 模式下引入gulp-uglify.js文件进行压缩。
  8. 将编译完成的.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抛出整个调用栈,让人无从下手。

pipe和pump的区别

3.5 编译 less

编译 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.lessvariable.less文件的内容。
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-lessLESS代码编译成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');
});
复制代码

优化后的效果以下图所示:

less编译优化

3.6 图片压缩

小程序主包目前只支持最多 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%左右的体积。

image图片压缩

3.7 其它文件编译

.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插件对文件进行压缩。

4. 最后

小程序开发最大的限制是主包大小必须控制在 2M 之内,经过本文的优化方法,将本来的小程序主包体积减少 11.9%,想要进一步压缩体积,能够考虑tree-sharking,分析代码依赖关系,剔除未引用的代码。但现有的gulp工做流架构模式是很难完美地实现这个功能的,tree-sharkingrollupwebpack等构建方案的特长,这也是为啥有一部分红熟的小程序框架如tarompvue会选择webpack构建。其实,各有所长,全看取舍。

附上代码连接: codesandbox.io/s/miniprogr…

相关文章
相关标签/搜索