你好,我是陈天忱,来自腾讯CSIG在线教育部。我所在的团队主要负责腾讯课堂平台的开发和维护,我从加入团队以来就围绕着小程序做了很多探索和优化,目前也是腾讯课堂小程序的负责人。
腾讯课堂小程序的技术演进路线
在我刚进入团队的时候,腾讯课堂小程序的工具链还处在比较原始的阶段。除了在编码层面利用了web比较成熟的scss、postcss、lint、typescript结合gulp做一些语法层面的编译以外,在测试、构建npm、上传、设置体验版、发布等阶段都是依赖的小程序开发者工具和管理后台,人工手动操作来完成的.
石器时代
在这个阶段,大部分都是简单地利用一些现有的工具,我们称之为腾讯课堂小程序的石器时代。
这个阶段存在几个明显的问题:
构建和上传依赖人工操作,有可能会因为流程操作失误而导致现网事故;
由于发布流程的不规范,需求并行时经常会出现发布撞车的情况,导致体验版相互覆盖造成预发布验证成本。
为了解决人工操作带来的隐患,我们从零开始基于小程序提供的命令行工具打造了小程序CI。让源码编译、构建npm、上传、生成开发版/体验版二维码、自动化测试等流程在CI流水中自动流转。
为了解决发布流程不规范的问题,我们将小程序的发布也接入到了业务发布平台。在发布平台上进行CheckList、CodeReview、发布评审、发布环境管理、发布静态资源等流程的流转,确保需求发布的质量、合规和有序。
工业时代
解决了开发流程中的问题之后,我们将更多的精力放到了小程序的研发效能与性能上。开发和构建阶段我们打造跨端的公共模块,通过kbone进行同构开发,利用云开发来辅助首屏性能优化,以及代替部分后台的开发,在构建方面将构建工具从gulp迁移到webpack,能对构建常务进行更细致的优化。
在发布之后,通过完善监控告警,将发布质量做到可视化的体现,并能够对出现的问题得到及时的接收和感知,减少用户的反馈。
近现代
到这里我们可以看到整个技术演进的过程,它涵盖了小程序开发、构建、测试、部署发布以及监控,形成了小程序的DevOps开发模式,这其中具体是怎么做的呢?
DevOps
构造“爽”的开发体验
首先是开发阶段,我们想要形成一个可以称之为“爽”的开发体验,并不仅仅是指coding阶段,还需要覆盖到测试以及发布阶段:
编码阶段——业务逻辑跨端可复用
测试阶段——改动持续集成、测试与开发解藕
发布阶段——规范化、流程化、可追溯
为此我们分别通过打造跨端可复用的公共模块、小程序CI、统一业务发布平台来提升开发体验。
提升开发体验
跨端公共模块
这里从一个实际场景出发,我们之前有这样一个需求:产品希望在各端的课程详情页有一个提示当前此机构正在直播的课程,可以引导用户跳转到直播间听老师讲解课程的细节。
公共模块
梳理一下这个需求的流程,发现其实还是挺简单的:
详情页渲染完成后->调用接口拉取直播间数据->渲染引导模块->用户点击跳转直播
可以看到,业务的主逻辑在各端都是一样的,但如果去看这些逻辑的细节就会发现其实各端需要的是不一样的实现,比如发起请求的api在浏览器和在小程序中是不一样的;提示的疲劳度控制需要用到本地的缓存能力,浏览器和小程序的api也是不一样的;然后在业务上,直播间在三端的页面地址也是不一样的。
各端逻辑的不同
通过ifelse或者switch的方式,在运行时判断当前的执行环境,然后调用不同的分支逻辑当然是能够实现需求的,但是这种方式会让一个端同时存在三端的逻辑,这样的逻辑多了之后,会造成比较明显的代码冗余,而在小程序端由于有2M的包大小限制,对于代码冗余是比较敏感的。
要解决代码冗余的问题,大家会很自然地想到构建时注入一个环境变量,通过tree-shaking的能力在不同端构建出对应端所需要用到的逻辑。但这个方案对于构建工具有着一定的要求,而在实际的工作场景中,新老项目往往由于历史的原因,不仅仅是源码,在构建上的技术栈也是有很多历史包袱的,比如gulp、fis、webpack等,如果要全部统一起来成本和风险都会比较大。
所以我们需要一个能够跨端复用,按需打包,而且不依赖项目构建体系的公共模块。
我们基于gitsubmodule的方式从组件、业务、工具三个维出发,每个维度根据具体的逻辑按照执行环境将其拆分成同构目录(isomorph)、浏览器目录(lib)、小程序目录(wx),各个项目将lib目录或者wx目录作为引用的入口,而入口文件会继承或者透传导出isomorph目录下的逻辑,对环境有依赖的特殊逻辑则在lib目录和wx目录下分别实现。
在开发阶段通过路径别名来统一引用路径,例如小程序的项目中设置tsconfig.json的paths为"ke-modules/*":"submodules/ke-modules/*/wx",这样就可以统一业务层面的代码逻辑。在构建阶段submodule会通过ts单独编译成js,如果是h5和PC的项目就只会将isomorph和lib目录构建到产物中,小程序的项目就只将isomorph和wx目录构建到产物中。
公共模块的编译阶段
这样就确保了在保证兼容性的基础上不会产生冗余的代码,以此来满足我们之前提出的几个需求。
小程序CI/CD建设
我们搭建小程序的CI/CD的起因,是由于开发者工具中很多人工操作带来的一系列问题,比如:
在构建过程中,很容易漏掉构建npm依赖
在上传时的版本信息和版本号也不规范
不同需求的体验版需要管理后台切换,需求并行非常不友好
开发版二维码需要开发者实时提供,影响测试进度要解决以上这些问题就需要从自动化和流程控制来入手。
我们先是基于小程序官方提供的一个命令行工具进行了封装和扩展,支持小程序的npm构建、上传、获取二维码、自动获取版本号、版本信息等功能,并作为小程序CI流水线中的核心插件。
CI流水线支持通过githook、OpenAPI、手动的方式触发执行。在流水线的流转执行中,完成代码拉取、分支检查、版本号迭代及版本信息更新、小程序代码包上传、开发/体验版二维码获取,同时归档小程序产物、sourcemap等文件便于对性能和错误的分析。
小程序CI/CD的流程建设
建设了这样一套CI/CD的流程之后,之前遇到的问题就都得到了解决。
通过在构建过程中获取依赖的npm信息来判断是否需要更新及构建npm,并自动执行;
上传时根据Angular的gitcommit规范,自动迭代major、minor、patch的版本号,更新changelog;
CI使用机器人账号上传小程序,通过业务发布平台对小程序的发布环境进行管理,避免发布冲突;
这是我们在CI/CD上面的一些实践经验,以及在开发体验上面的一些处理方案。
小程序性能优化
小程序的启动方式分为冷启动和热启动,而小程序的性能瓶颈大部分也都集中在冷启动这一阶段。
小程序的冷启动
体积优化
业务代码的体积优化需要通过构建来解决,以一个项目的常规结构来看,我们一般会将一些有可能复用的模块放置到公共模块中。如下图所示,引用关系如果只进行编译的话,根据小程序的规则,公共模块和组件的大小都会被计算到主包中,我们希望通过构建来优化产物结构,避免主包太大的问题。
主包太大
再者,随着需求的迭代,可能某一个组件的引用就丢失了,这种情况在小程序的规则下,依然会被计算在主包里面,可以看下面这张图。我们希望能通过构建将未使用的模块或者组件进行过滤。
未使用模块未过滤
另外,如果某一个分包与主包引用了同一个模块,这时候将这个模块计算到主包中是OK的,但如果这个分包是一个独立分包的情况下,再去引用主包的模块,是有可能报错的。上面这种情况需要通过构建的方式将模块复制一份放到独立分包下面才能保证小程序的正确执行。
独立分包引用报错
我们面对的上面三个问题有一个核心思路是需要在构建的过程中,针对小程序的规则进行依赖分析。下面是我们对比目前市面上比较成熟的构建工具,从四个维度进行了分析:
支持组件的依赖分析
根据对比的结果,webpack对于需求的支持还是比较成熟的,我们最终决定选择webpack作为小程序的构建工具,但是webpack也不支持小程序的组件,这一点就需要我们自己进行支持了。
以app.js作为入口文件,根据小程序的配置规则找到对应的json文件,逐层递归就可以将整个小程序所使用到的页面和组件分析出来,并将所有的页面和组件都作为webpack的entry,就可以获取到小程序中js模块的引用信息了。
通过plugin对引用信息根据一定策略进行计算chunk:
分包处理
例如,某一个页面引用了一个模块,先判断模块是否在分包内,如果在分包内则按照常规方案打包;不在分包内则判断引用它的分包是否为独立分包。如果是独立分包则复制一份(新建一个chunk);是普通分包则收集是否被多个分包引用。若不是,则将模块移动到分包下(新建一个chunk,并将原来的删除)。
计算完chunk之后就可以通过webpack的load去处理另外的资源文件,包括css、image、font,提取静态资源文件,替换引用路径。
处理资源文件
处理完成后的效果也相当明显,我们的主包从1900多kb优化到了900多kb,优化幅度达到50%,总包的一些体积也优化了27%。
体积优化效果
优化的体积主要来自以下三个方面:
模块下沉到了分包
对未使用到的组件和模块进行了过滤
静态资源文件上到CDN
我们的优化在实际的启动耗时上也有比较显著的效果,主包下载耗时优化了43%,js的注入耗时优化了18%。
启动耗时优化效果
兼容小程序的SDK方案
另一方面,当小程序需要通过npm的形式使用一个比较复杂的SDK时,由于小程序的npm包需要单独构建一次,无法做到编译时按需打包,这也会遇到体积较大的问题。
在我们的实际业务场景中就有这样的问题,腾讯课堂作为在线教育业务,有个核心能力是直播互动,就是用户在线上上课的过程中聊天、举手、连麦、抽奖等形式的交互行为。
为了让这个核心能力能够达到跨端跨业务的复用效果,我们团队开发了一个直播互动的SDK,对外抛出简单的API,内部设计了接口层、适配层、通道层、策略层,结构非常清晰,使用起来也很方便,初始化后开发只需要监听或请求对应的命令字即可,无需关心内部的转化,并且能够利用ts的类型推断能力直接拿到通道返回的数据类型。
改造前架构及使用方式
但是当我们在小程序端进行接入时,遇到了几个问题:
为了支持跨端跨业务,SDK内置了所有功能的逻辑,在小程序端使用会造成大量的包体积浪费
针对web设计,不兼容小程序;单独维护一个小程序的版本成本比较大
不同项目和业务对SDK的迭代会导致版本管理混乱
要解决上面这些问题,我们必须对SDK进行升级改造。
我们改造的方案是进行插件化处理,将接入层(业务层)和适配层作为SDK的内核抽离出来,并添加了pluginAdaptor对插件进行适配管理,将策略层和通道连接层的逻辑进行抽象处理,制订好对应的规范,根据抽象类和业务需求实现对应的策略插件和通道插件。
插件化改造
在业务中的改造非常简单,只需要初始化之前注册当前场景和功能所需要的插件,后续在使用上与之前完全一致,业务的改造成本非常低。
改造后使用方式
兼容性上通过rollup打包,在构建时注入不同的环境变量,输出对应端所需要用到的bundle。
rollup打包
插件化改造之后,好处就显而易见了:
按需引入,运行时它的体积是最小的,改造前后SDK运行时体积从384KB减少到42KB,优化了近90%
多包结构迭代比较清晰,内核和抽象通道也很稳定,各个插件可以进行单独的版本迭代
跨端复用能力得到了扩展,统一维护
运行时的请求优化
请求的优化也是小程序性能优化中很重要的一环,在冷启动和页面跳转的过程中,我们分别对请求时机以及弱网阻塞两种情况进行了优化。
请求时机上,可以利用小程序的全局app实例将数据请求的时机提前到页面加载之前,进一步利用小程序的数据预加载能力,将首屏数据的请求时机提前到启动小程序时:
请求优化
页面加载前发起请求的流程如下,在onLaunch或者页面跳转时就直接发起下一个页面的请求,并将请求的Promise挂载在app实例上,当页面加载完成出发onLoad的时候则直接通过app上的Promise返回进行渲染,根据我们的统计平均可以优化100ms的耗时,而且相对静态的数据可以通过本地缓存的方式,在二次加载此页面时通过缓存数据渲染,达到秒开的效果。
在页面加载前发起请求的流程
而数据预拉取则类似于web的服务端渲染,在启动小程序时通过云函数根据启动参数调用业务后台的服务获取数据并返回给小程序,小程序启动后就可以直接使用预拉取的数据进行渲染,预拉取成功可以平均优化90%的首屏数据请求耗时。
数据预拉取流程
除了上面常规情况的请求优化,我们还注意到小程序有一个网络使用限制,最大的并发限制是10个,这就会造成隐患。因为在小程序加载和用户交互的过程中会产生很多的上报请求,例如PV上报、错误日志等,在弱网的情况下,很容易出现上报请求响应慢而阻塞了业务请求的发送导致超时,而我们也确实收到了类似情况的反馈。
为了优化弱网情况下存在的隐患,我们对请求队列进行了优化,通过设置请求池与等待队列,并劫持wx.request,在发送请求时对请求的url进行优先级排序,将业务请求设置为高优先级的请求,上报请求的优先级降低。当请求通道相对紧张时会将高优先级的请求优先发送,低优先级的请求在请求通道空闲时再进行补发。
弱网下的请求排队
在实际运行过程中的逻辑如下图:
弱网下的请求排队运行逻辑
渲染优化
首屏优先,分步渲染
但由于小程序的双线程模式,通过setData的方式更新视图是同步更新逻辑层数据,异步更新视图层数据,所以并不能简单地在处理完一部分数据后调用setData再继续处理其余的数据,甚至通过Promise也做不到分步渲染,而使用setData的回调或者setTimeout的方式又会出现逻辑嵌套的问题,降低代码的可读性和可维护性。
针对这个问题,我们的解决方案是基于setTimeout根据Promise的表示封装了一个PromiseMacro的类,这样我们就可以向使用Promise一样通过then方法将小程序的渲染拆分成多个步骤达到渐进式渲染的效果。
渐进式渲染
质量保证的监控体系
一个产品的质量不仅仅是靠好的产品设计和代码质量,还有很大一部分需要通过收集操作和性能日志,为技术优化提供方案,而对现网报错的比例及数量进行监控,能让我们及时响应并修复。之前我们的小程序上报也依赖了好几个上报系统来完成:
通过BadJS来收集前端报错和操作日志
通过Wang进行测速上报
通过Monitor进行打点监控告警
通过Tdw进行产品需求上报
通过不同的系统进行上报会存在一些问题:
依赖的SDK比较多,每一个SDK的API都不一致,学习和维护的成本会比较高,这一点对于新人来说尤其明显;
每个SDK上报的数据结构也不一样,想要查找对应的数据,就必须去对应的统计平台进行搜索。
解决这些问题的首要任务就是需要对这些上报的SDK进行整合,根据产品和业务需求将日志、测速、监控、上报收归到一个SDK里面,并统一上报的数据结构,部分功能会在SDK中进行备份转发,保证原上报系统的功能也能得到利用。
imlog监控体系
再结合小程序提供的几个API就可以在日志收集的同时对用户进行多维度的统计:
Grafana性能监控
在统一上报数据结构的前提下,就可以自定义制定多维度的统一看板,降低质量监控的成本。
图片性能监控可视化
总结
随着我们课堂小程序的技术演进,围绕小程序逐渐形成了DevOps的开发模式:开发阶段,我们打造了兼容小程序端的公共模块,提升了小程序30%~40%的研发效率,同时在CI/CD建设方面,减少了人工操作的风险,充分利用工程化自动化来解放开发的生产力,而且小程序CI建设的很通用化,公司内部有140+的项目接入。
开发体验提升
在性能优化方面,我们通过在体积、请求、渲染方面的优化,将冷启动下的首屏性能优化了1.5s,达到了42.7%的优化比例。
性能优化提升
在监控告警方面,通过小程序的API再结合收拢的上报SDK,可以让统计力度更细,而且可视化可定制。
图片监控告警提升
展望
1.异步化打包策略
分包异步化可以极大地缩小首屏包的大小,目前分包异步化的特性已经适配了2.11.2的基础库版本,兼容性的问题也已经得到了解决;接入分包异步化的能力,可以尝试在小程序构建打包时将一个页面拆分成首屏包+异步逻辑包+异步组件包的形式,结合分包预加载功能,将首屏的代码包下载耗时和加载耗时降至最低。
2.基于录制回放的自动化测试
小程序团队很早就支持了小程序的自动化测试,但是在UI自动化测试方面,测试用例的维护成本是最大的痛点,我们尝试在本地录制操作流程,按照一定的约定转换成测试用例,可以极大地降低测试用例的维护成本,提高代码质量。
作者简介
陈天忱腾讯CSIG在线教育部前端高级开发工程
2019年加入腾讯,目前是腾讯前端高级工程师,IMWeb团队成员。负责腾讯课堂平台的教学教研方面的开发和维护,作为腾讯课堂小程序负责人,在小程序性能优化、持续集成等方面有深入的见解和丰富的实践经验。2019年在公司内部开发推出了小程序CI并持续优化迭代,其跨系统的兼容性及优秀的使用体验,被各个BG超过150个项目接入并使用,赋能开发测试及产品,依托于腾讯CI开发的小程序插件获得2020年度腾讯CI年度优秀插件。