腾讯开心鼠英语 团队中有很多小程序的项目,且后续还会很多小程序的开发和迭代规划,因此我们团队是小程序的重度使用者。在小程序的开发中,团队积累了一些技术和经验,也遇到了一些困难和挑战,还踩了很多坑,因此有必要将我们团队的小程序实践进行总结和分享。

腾讯开心鼠英语

一、工程化探索

微信小程序的开发规范里,有一些工程方面的要求,例如可以通过项目的配置文件来设置根目录,每个页面或组件需要wxml、js、json、wxss 4个文件组成等,于此同时微信的开发者工具可以帮忙初始化项目,并设置好目录结构。除此之外对于项目工程方面的支持就比较薄弱了。为了提升团队的开发效率和质量,我们还需要在已有的基础之上进行一些列的优化。我们希望小程序的开发脚手架至少具备以下的能力:

  1. css预处理语言支持,可以使用例如sass、postcss等开发样式;

  2. typescript支持,可以更好地使用typescript进行开发;

  3. 更好的目录结构设计;

  4. 更好的npm包支持;

  5. 代码检验支持,可以使用eslint/tslint、stylelint等对代码进行规制校验;

因为现阶段的小程序开发的工程需求主要集中在文件编译和资源整理上,小程序开发者工具会帮我们处理文件打包,因此我们考虑使用gulp去搭建工程脚手架。

1. 目录结构设计

微信小程序的代码主要由4个放置在同一目录下的文件构成:

  1. .json 后缀的  JSON 配置文件;
  2. .wxml 后缀的  WXML 模板文件;
  3. .wxss 后缀的  WXSS 样式文件;
  4. .js 后缀的  JS 脚本逻辑文件;

微信开发者工具会对以上的文件进行监听,当其中任意一个文件发生改变时,开发者工具就会刷新预览。

如果使用ts进行开发,那在同一目录下,还将多出ts文件;如果参考这种方式引入css的预编译语言,那还会再多出一个待编译的样式文件。这样一个页面或组件的目录下,就至少会存在6个文件,显得非常臃肿,不仅降低了文件查找的效率,还有可能带来其他的误操作。因此这样的目录结构就不足以支撑我们后续开发的工程化要求。

我们希望将源文件和编译文件分离,只保留基本的4种类型文件,当源文件发生改变时,就触发编译,将其编译成对应的开发者工具可以监听的文件类型。因此设计了以下新的目录结构:

目录结构

src 目录下存放项目的源文件,使用gulp监听文件的变化,并触发对应的编译任务,将源文件编译为目标文件,或者拷贝不需要编译的文件到目标目录(dist文件夹),然后在project.config.json里指定小程序的根目录为dist文件夹,这样开发者工具就会去监听dist目录里的文件变化并更新预览了。通过这样的设计,整个目录结构更加清晰,开发者只需要关注src目录即可。

当然在实践的过程中,这样的目录结构也存在一定的问题:

  1. 从文件变化到最后开发者工具更新预览的整个链路变长了,有一定的时间上的损耗;

  2. 因为整个链路变长,也加大了引入其他问题的可能;

  3. 因为一些历史原因,团队一些老的项目还没有完全按照这样的目录结构去设计,团队中的小程序项目目录结构还没有完全统一;

以上的问题还需要我们团队在后续的开发中去解决和优化。

2. css预处理语言支持

微信小程序的样式代码主要是编写在wxss文件中,其语法和css是一样的,只有少量的css规则不适用。如果只是编写css样式,那只写wxss是完全没问题的。但是现在市面上还是有很多流行的css预处理语言可以帮助更好地开发css样式,提供了例如mixins、function、变量等功能。因此我们希望编写css预处理语言,并将其编译成wxss文件。

我们团队使用的是postcss。源文件编写的是css文件,通过设置gulp task的方式,将css编译成wxss。并通过postcss插件的方式,集成更多的其他功能。

在实践过程中,我们发现当项目规模开始变大时,在有些机器上,修改一次样式文件后触发更新的速度很慢,这个时候我们就引入cache去加速css的编译。第一次编译时会将所有的css编译,而后只会去编译修改过的文件。代码如下:

/**

* 将 css 编译成 wxss

*/

const cssCompile = () =>

src([`${mpDir}/**/*.css`, `!${mpDir}/**/_*.css`])

.pipe(cache('css-compile'))

.pipe(

// 防止编译中断

postcss().on('error', () => {

this.emit('end');

})

)

// 去掉编译出来的 :root{}

.pipe(replace(/:root\s\{[^}]*\}?\s*/, ''))

.pipe(

rename((path) => {

path.extname = '.wxss';

})

)

.pipe(dest((file) => file.base));

3. typescript支持

原先的微信小程序对ts文件的支持,是通过预置编译脚本,使用 tsc 去编译ts文件的。我们的项目引入了gulp之后,对ts的支持就是通过设置gulp task,使用gulp-typescript 这个插件去编译ts文件,同时还需要使用gulp-sourcemaps这个插件去写入sourcemap。

在实践过程中,我们发现有的时候ts的文件编译比较慢,这个时候可以使用gulp-typescript提供的增量编译的功能。开启增量编译之后,第一次编译时的速度是一样的,而之后的编译速度就会提升约50%。代码如下:

const tsProject = ts.createProject("tsconfig.json");


// 编译 ts

const tsCompile = () =>

gulp

.src(tsPath)

.pipe(sourcemaps.init())

.pipe(

/* 增量编译 */

tsProject()

)

.js.pipe(sourcemaps.write())

.pipe(gulp.dest(dist));

4. 更好的npm包支持

微信小程序是支持使用npm包的,但是这个支持是有一些前提条件的。例如当引入某个包时:

import { abcRequest } from '@tencent/abcmouse-sdk-mp-tools';

小程序会去根目录下的miniprogram_npm这个文件夹下查找有没有@tencent/abcemouse-sdk-mp-tools这个包,如果没有则会提示找不到对应的包。而miniprogram_npm又是根据package.json里的dependencies字段里声明的依赖构建而成的。因此微信小程序要使用npm包的前提总结如下:

  1. 必须在package.json的dependencies字段里有声明 ;

  2. 小程序的根目录下必须有node_modules目录,其目录里有对应的包;

  3. 必须构建出miniprogram_npm;

因此要想使用npm包,整个过程是比较波折的。好在小程序官方有提供对应的ci构建可以帮助我们。

但在实践过程中发现,调用ci.packNpmManually这个接口构建出来的miniprogram_npm目录,不仅包含了dependencies里的依赖,还包含了其他的依赖,而miniprogram_npm这个目录里的代码在上传小程序代码时也是会上传的,引入其他多余的依赖会增大小程序的包体积,在小程序严格的代码大小要求下,这是不可取的。因此还需要对构建的包进行筛选。其大致流程如下:

流程

使用gulp监听package.json文件,当安装新的npm包,并指定 --save 时,package.json文件会发生变化,并触发对应的gulp task。在gulp task里去遍历package.json的denpendcies字段,并从顶层目录的node_modules里拷贝对应的npm包放入dist目录的node_modules中。最后再通过ci.packNpmManually方法去构建,这个时候构建出来的miniprogram_npm目录里就只有必须的npm包了。

通过这种方式,我们需要使用新的npm包时,就只需要npm install并在代码中import就可以了,其他的处理过程对开发者来说都是无感知的。相应的gulp task代码如下:

/**

* 在小程序根目录下生成package.json文件用于构建miniprogram_npm

* @param {Array<string>} deps denpendencies对象

*/

const generateSubPkg = (deps) =>

writeJsonFile(subPkgPath, { dependencies: deps })

.then(() => deps);


/**

* 获取必要的npm包目录路径

* @param {Array<string>} deps 依赖数组

*/

const getDepsModule = (deps) =>

Object.keys(deps).map((key) => `node_modules/${key}`);


/**

* 构建miniprogram_npm

* @param {Array<string>} modules npm包路径数组

*/

const packNpmManually = (modules) => {

const packPath = `${mpDir}/miniprogram_npm`;

const subNpmPath = `${mpDir}/node_modules`;


fsx.emptyDirSync(packPath);

fsx.emptyDirSync(subNpmPath);


modules.forEach((modulePath) => {

fsx.copySync(modulePath, `${mpDir}/${modulePath}`);

});


return ci.packNpmManually({

packageJsonPath: path.resolve(process.cwd(), subPkgPath),

miniprogramNpmDistDir: path.resolve(process.cwd(), mpDir),

});

};


/**

* 构建miniprogram_npm gulp plugin

*/

const packPkgManually = () =>

through.obj(function (chunk, enc, cb) {

const filepath = path.resolve(process.cwd(), 'package.json');


if (!fsx.pathExists(filepath)) {

cb(null, chunk);

}


const pkgData = fsx.readJSONSync(filepath);


const dependencies = pkgData.dependencies || {};


// denpendencies 没有发生变动则不需要构建

if (isEqual(cached, dependencies) || packing) {

cb(null, chunk);

}


const spinner = ora('开始构建npm包...').start();


packing = true;


generateSubPkg(dependencies)

.then(getDepsModule)

.then(packNpmManually)

.then((result) => {

cached = dependencies;

spinner.succeed('构建成功,构建结果:');

packing = false;

console.log(result);

cb(null, chunk);

})

.catch((err) => {

spinner.fail('构建失败');

packing = false;

console.error(err);

});

});


const pkgPack = () => src(pkgPath).pipe(packPkgManually());


const pkgWatch = () => {

watch(pkgPath, pkgPack);

};

5. 代码校验

我们团队还接入了imweb团队的eslint和stylelint规则去做代码校验,这里就不具体暂开讲了,感兴趣的可以参考:

  1. eslint-config [ 1 ];

  2. stylelint-config [ 2   ]

6. 后续展望

其实在小程序的工程化这一块还有很多工作可以做,例如:

  1. 当项目体积变大时,小程序开发者工具的刷新预览速度变慢,除了等待小程序官方的优化之外,是否可以通过工程化的手段,提升开发者工具的预览刷新速度;

  2. 合理的分包可以提升小程序的加载速度,是否可以通过工程化的手段,将项目中用到的通用组件,智能合理地分配到主包和子包中;

  3. 引入命令行工具生成页面和组件的文件模板;

  4. 尝试引入tree-shaking剔除无用的代码;

  5. 尝试引入purifycss剔除无用的样式;

  6. 集成官方的ci工具,完善开发体验;

...

我们团队会继续在小程序工程化方面进行探索和尝试,如果有新的成果我们也会及时分享。

二、性能优化

因为微信小程序是运行在微信app里的,所以其运行环境是比较苛刻的,因此要想使小程序流畅地运行,提供良好的用户体验,对其进行性能优化就至关重要。

对于小程序优化来说,一些传统的前端优化方案也适用于小程序。而其双线程的设计模式,又和传统前端的单线程有所不同,因此也有一些新的优化点,下面主要从几个大的方面归纳总结:

  1. 加快网络请求 ;

  2. 加快页面渲染;

  3. 提升渲染性能 ;

  4. 内存优化 ;

  5. 其他优化。

1. 加快网络请求

1.1 减小代码包大小

小程序在冷启动时,会首先下载对应的代码包,然后解压执行代码,所以减小包大小可以加快代码下载和解压的速度。一些方法可以减小代码包的大小:

  • 代码复用,尽量将可以复用的代码提取封装做出npm包或通用函数;

  • 剔除无用的样式,可以使用类似 purifycss的库将无用的css样式剔除以减小样式代码的大小;

  • 剔除无用的函数,可以尝试引入tree-shaking剔除无用的代码;

  • 静态资源走cdn,尽量不要将静态资源打包到代码中;

  • 分包,可以将一些和首页渲染无关的代码分发到子包中从而加快主包的下载和执行。

1.2 预请求

  • 数据预拉取:小程序提供了一个接口,可以让用户在进入到小程序之前就去请求接口,当然这个方法有一定的限制,需要深入调研和谨慎使用;

  • 分包预下载:可以配置进入某个页面时下载可能会用到的分包,可以有效避免进入某些页面”白屏“时间过长。

1.3 缓存

没有什么请求比不请求更快的了,合理利用缓存可以有效减少网络请求的数量,加快整体的加载速度。对于一些实时性不高的数据,我们可以利用微信提供的缓存能力,将一些数据存储在本地,从而避免一些网络请求。

1.4 图片优化

  • webp格式图片;

  • 图片剪裁和降质;

  • 图片懒加载和雪碧图;

  • 渐进式加载大图资源:在不得不使用大图资源的场景下,可以适当使用“体验换速度”的措施来提升渲染性能。小程序会把已加载的静态资源缓存在本地,因此,对于大图资源,我们可以先呈现高度压缩的模糊图片,同时利用一个隐藏的 <image> 节点来加载原图,待原图加载完成后再转移到真实节点上渲染。整个流程,从视觉上会感知到图片从模糊到高清的过程,但与对首屏渲染的提升效果相比,这点体验落差是可以接受的。

2. 加快页面渲染

其实小程序的页面渲染的优化思路和传统前端的优化思路是一致的,主要思想是: 关键渲染路径渲染

关键渲染路径(Critical Rendering Path)是指完成屏幕渲染的过程中必须发生的事件。

我们可以分析页面上哪些部分是主要模块,哪些部分是次要模块,例如一些提示性的组件,我们可以稍后渲染。

于此同时,还可以在主要模块中还可以优先渲染主屏模块,不在主屏内的模块可以延迟加载或者滚动加载。

3. 提升渲染性能

提升渲染性能可以有效减少交互时的卡顿,让用户在交互的时候体验”如丝般流畅“。小程序采用的双线程模型,即渲染和逻辑分散在两个不同的线程中。所以在小程序的环境下,加快用户响应主要从以下3点出发:

  1. 降低线程之间的通信频次;

  2. 减少线程之间通信的数据量;

  3. 减少wxml的节点数量。

可以采用的办法有:

  1. 合并setData调用;

  2. 只把与界面渲染相关的数据放在data中;

  3. 去掉不必要的事件绑定;

  4. 去掉不必要的节点数据;

  5. 事件总线管理数据;

  6. 滚动渲染长列表节点;

...

4. 内存优化

因为小程序毕竟是一个程序(微信)中的程序,可提供给其运行的环境资源是十分有限的,这对于小程序的设计和开发来说就比较苛刻了,因此要在实现的时候小心翼翼。可以从以下几个方面注意内存的优化:

  1. 内存预警;

  2. 回收后台页面计时器;

  3. 避免触发事件中的重度内存操作;

  4. 大图、长列表优化。

5. 其他优化

还可以采用一些其他的方式来对小程序进行优化:

  1. 逻辑后移:可以让后台承担更多的业务实现。小程序端主要承担展示相关的职责,这样可以避免在小程序端进行过多的数据操作,占用过多的内存。也可以避免因为复制的业务实现而带来的潜在的程序崩溃;

  2. 骨架屏:可以利用骨架屏来提升整体的加载体验。

最后总结输出一下小程序性能优化相关的脑图如下:

脑图

三、自动化探索

1. 自动化测试

小程序官方提供了自动化地工具去模拟小程序的操作,搭配常用的测试框架,可以很容易地实现小程序的前端测试。小程序的模拟器提供了4个级别的操作api:

  1. 模拟器级别的 AutoMator;

  2. 小程序级别的 MiniProgram;

  3. 页面级别的 Page;

  4. 元素级别的 Element。

我们可以利用这几个api对象去模拟小程序的行为,例如模拟元素点击、页面数据修改、页面跳转等操作。具体api可以参考:小程序自动化工具 [ 3 ]。

在实践接入的过程中遇到了一些坑。要想使用小程序模拟器是有一些前提条件的,首先是要开启开发者工具安全设置中的 CLI/HTTP 调用功能。并通过以下代码开启调用:

automator.launch({

cliPath: 'path/to/cli', // 工具 cli 位置

projectPath: 'path/to/project', // 项目文件地址

})

其中 cliPath 是我们的开发者工具的文件路径,如果没有更改过默认安装的位置则可以忽略。projectPath是项目目录的地址,这里的项目目录指的是project.config.json里指定的目录,而且这个路径必须是绝对路径才可以调用成功。

其次小程序的模拟器是有一些使用限制的,它不能调用和操作微信系统的原生组件的,例如授权、位置、支付等功能。当我们希望利用脚本去测试整个页面的流程时,当涉及到授权和支付等操作时,往往就跑不下去,因此小程序的模拟器更适合去做一些关键操作的测试,例如测试某些操作之后,页面的样式、行为是否符合预期。

2. CI接入

小程序的代码打包、上传等功能可以交由CI来操作。我们团队主要使用蓝盾CI,配合蓝盾上的小程序插件,可以很轻松地接入,其流程比较简单,主要是拉取代码、安装依赖和构建上传。蓝盾流水线如下:

蓝盾流水线

小程序代码上传需要用到秘钥,我们可以将其秘钥托管在蓝盾,即方便又安全。

References

[1] eslint-config:  https://github.com/imweb/eslint-config-imweb

[2] stylelint-config:  https://github.com/imweb/stylelint-config-imweb

[3] 小程序自动化工具:  https://developers.weixin.qq.com/miniprogram/dev/devtools/auto/

相关文章