从小程序子包脚手架到工程化
在小程序子包定制开发的实施交付过程中,如何提高定开的交付效率,保证定开子包的质量?本文展示了如何设计这一套子包脚手架及工程化方案。
背景
交付中心在交付客户的业务形态中,小程序联合开发的交付方式占比很高,其中又以定开子包交付为主。即很多客户都有自己的小程序,交付开发组只需将业务闭环(与主包无耦合)在自己的小程序子包中,通过小程序分包的模式,交付子包合入客户的小程序即可。目前团队里现有的开发模式存在以下一些问题:
- 开发过程中缺乏可复用的小程序定开子包脚手架模板。意味着每新接入一个客户,开发人员在创建新的项目时,都需要从零开始重复搭建项目来开发,这无疑增加了开发周期和潜在的错误几率;
- 缺乏明确的开发规范,如
js
代码为原生编写、本地及远端均没有做提交拦截检查等,降低了开发效率,同时也无法有效保证质量; - 缺乏流水线自动化构建,图片等静态资源需要手动上传
CDN
、也需要人肉手动构建不同环境、不同需求的产物等,容易出错; - 无法保证子包
css、js
隔离性(sandbox
),css
会受主包全局样式影响,js
里缓存、全局变量如global
等也会被污染; - 目前交付业务中优码侧业务占比较高,并且不同优码客户用到的能力或页面功能都大同小异,但对这块业务核心逻辑并没有做通用化梳理,提取核心链路并复用,导致不同客户的核心业务重复编写,效率低下耗费人力;
基于以上现状,我们设计并完善了一套可移植、可扩展的并内含优码通用业务的子包脚手架模板的前端工程化解决方案。
子包开发工程化总览
如图所示,通过 cli
工具提供开箱即用的能力,可一键生成并选择是否需要子应用隔离(sandbox
)等优码定开业务的子包脚手架模板。安装依赖后,即可在本地运行项目。
通常情况下,开发者无需关注项目模板里通用层代码,可将精力聚焦在定开层的开发与交付,提交交付效率。开发完成后,通过流水线可以进行不同模式的产物构建并交付客户。以下分别从子包脚手架模板及 cli
工具、优码业务层通用化剥离、流水线 3 个部分来介绍整套工程化方案的特点与能力支持。
定开子包脚手架模板及 cli 工具
我们对子包脚手架模板项目进行了简单的分层设计,从下到上依次为项目基础结构、构建层、服务层、组件层、业务层,共同完成小程序子包项目结构的搭建。
项目基础结构
整体项目由 typescript + less
编码支持,提高编码效率并保证代码质量。开发规范沿用腾讯编码规范(代码、目录、命名、协作、提交、注释等)并引入了 eslint
、stylelint
、prettier
、editorconfig
、vscode
等配置进行代码及风格约束统一,通过 git commit hook
完成本地提交时对 commit msg
及代码检查,并按照协作规范(如下图)完成开发协作。
整体项目结构如下图:
mock
文件夹为接口列表对应的各接口的mock
数据,协议先行前置本地联调,可以提前暴露协议风险,在后续真正接口联调阶段可以减少沟通成本,提高联调效率;config
文件夹为客户项目配置文件夹,存放env.js
,由cli
工具配置生成,构建时强依赖。该文件与各层解耦,存放了本业务的各环境对应的所有配置,包括分配的CDN
地址、api
地址、监控上报地址、验签key
等,统一收敛避免分散在各业务及服务中,方便调整维护。
其中主要关注 src
:
mock
文件夹为当前客户相关的模拟数据,如登录态、授权信息、微信步数等数据,用于未合包之前的本地联调开发及本地测试;pages
文件夹为当前客户相关的模拟主包部分页面,用于模拟分包跳转及一些主包的功能页;sub-package
文件夹为当前子包全部内容,文件夹名则为子包名,由cli
工具配置生成。该文件夹即为整个定开业务,内部功能闭环,打包构建会将其编译压缩,最终以子包形态交付给客户;test
文件夹为单元测试用例集;src
里其他app.json
,app.ts
,project.config
等为小程序本地分包开发模拟的必要文件;
可以看到,项目基础结构为一个完整的小程序工程,但根据业务交付特殊场景,实际的交付只交付子包 sub-package
,其他都是为了方便在本地进行开发联调及测试。所以需保证 sub-package
与其他模块无耦合,同时保证应用隔离(css、js
),且具备完整闭环功能。
构建层
目前通过通过 gulp
进行小程序的开发监听和构建编译处理,通过读取环境配置文件 env.js
,支持
- 多构建模式:整包模拟、生产子包;
- 多环境:
qa、green、prod
; - 资源处理:本地开发时的兼容性处理和生产模式中的 CDN 转换路径、混淆压缩等;
- 应用隔离
sandbox
:css、js
- npm 构建:整包模拟构建、子包构建
子应用隔离
小程序框架系统分为视图层和逻辑层。对于视图层,不同页面的 DOM
和 CSS
天然隔离,但各页面的样式都会受到全局样式(app.wxss
)影响,如何隔离 CSS
不受影响呢?对于逻辑层,不同页面的局部 JS
变量是隔离的,但全局的变量如 global
,globalThis
如何隔离,本地缓存 storage
如何隔离?
JS
隔离
通过闭包作用域来模拟构建一个 JS
沙箱环境:
!((wx) => {
Page({
onLoad() {
const location = wx.getLocation;
console.log('经过代理的wx对象及方法', location);
},
});
})(wxProxy);
// wxProxy为模拟wx对象
经调研发现,小程序微前端框架(guru)的部分能力契合我们的述求,就不再重复造轮子。通过查阅源码发现,基于 guru
导出的 @tencent/guru-runtime
npm
包加以开发适配,能够编译我们的业务代码并注入对应的运行时 JS
沙箱环境,打包后的页面 JS
代码为:
!(function (e) {
var o = e.getWx(),
e =
(e.getGlobal(),
e.getGlobalThis(),
e.getAppConstructor(),
Object.defineProperty(exports, '__esModule', { value: !0 }));
Page({
data: {},
onLoad: function () {
//...
},
});
})(require('../guru.js').getMicroApp());
guru.js
为对应的 runtime
代码,各页面需要引入。业务代码被包裹在一个闭包环境中,全局变量 wx、global、globalThis、App
等均被代理,完成了 JS
隔离;
CSS
隔离
独立分包里的页面 CSS
不会被主应用全局样式 app.wxss
影响,重点是普通分包的处理。经测试发现,在子包页面 json
配置 styleIsolation:page-shared
或 page-apply-shared
则可以隔离主包全局样式,故遍历各页面 json
添加该属性即可:
const setCssSandBox = (file, source) => {
const { path: filePath } = file;
const appJsonPath = path.resolve(source, 'app.json');
const appJsonContent = fs.readFileSync(appJsonPath, 'utf-8');
const appJson = JSON.parse(appJsonContent);
const pages = getPages(appJson);
const shouldUpdate = pages.some((page) => filePath.includes(page));
if (shouldUpdate) {
const fileContent = file.contents.toString();
const jsonData = JSON.parse(fileContent);
jsonData.styleIsolation = 'page-shared';
file.contents = Buffer.from(JSON.stringify(jsonData, null, 2));
}
return file;
};
根据当前业务场景,因为是独立子包交付,故无需处理路由隔离、应用间通信等,最终将以上 sandbox
功能封装为 npm
包 @tencent/uma-sp-sandbox
,支持自定义配置小程序不同全局变量的代理,同时对项目代码零侵入,开发者无感知。后续持续迭代专注于子包隔离。
NPM
构建
子包需要闭环交付,故该模式下的 npm 构建路径需在子包内并被正确引用;同时构建后的 npm 也需要通过 @tencent/guru-runtime
添加沙箱环境并修正引用路径。
服务层
代码路径:除接口 mock
在文件夹顶层目录外,其他均在 sub-package/common
服务层与具体业务解耦,主要提供了以下能力:
请求会话
请求模块以 flyjs
+自定义 Http Engine
为基础完成请求层的构建,同时设置请求/响应拦截器,如设置 request header
,统一处理错误码,登录重试等;
会话模块对登录授权等会话状态相关的管理。该模块会读取客户定开层里对相关登录态的获取,同时还支持请求重试,更新用户数据等会话相关的底层能力。同时使用单任务队列机制
和保险丝机制
保障相关业务接口如登录、重试等调用稳定:
declare class Session {
status: Status;
retrying: boolean;
/**
* 初始化客户用户信息
*/
initUser(): Promise<void>;
/**
* 优码登录
*/
umaLogin(): Promise<void>;
/**
* 通用登录
*/
login(option?: LoginOption): Promise<void>;
/**
* 退出登录
*/
logout(): void;
/**
* 登录完成
*/
ready(option?: LoginOption): Promise<void>;
/**
* 刷新会话登录态
*/
refresh(): Promise<void>;
/**
* 更新用户信息
*/
setUser(user: CustomUserInfo): void;
/**
* 即时获取最新用户信息,会请求刷新用户数据
*/
getNewUser(): Promise<CustomUserInfo>;
/**
* 获取用户信息
*/
getUser(): CustomUserInfo;
/**
* 获取token
*/
getToken(): string;
/**
* 调API更新个人信息(头像、昵称、手机号)
*/
updateInfo(headUrl: string, nickName: string, phone: string): Promise<void>;
/**
* 是否会员,存在手机号即开通会员
*/
isMember(): boolean;
/**
* 登录时候后重试弹窗
*/
retry(content: string): void;
}
安全验签
引入优码验签 npm
包,用于预防 crsf
,请求伪造、篡改、重放等,提高破解门槛,增强通信安全性;
监控埋点
通过对接口调用、关键路径等进行埋点,方便定位查找问题;同时将获取到 UV PV
并上报到小马 BI 进行数据统计分析;
接口 mock
通过 express+mockjs
开启本地服务并注入模拟数据,通过 whistle
将本地服务代理到小程序实际请求的接口即可:
'/userc/v1/user/core/jhLogin': {
code: 200,
data: {
'result|10': [
{
'id|+1': 1,
account: '@email',
name: '@cname',
},
],
'totalCount|10': 1,
},
},
单元测试
使用 jest
+miniprogram-simulate
完成单元测试的用例编写执行,保证应用质量;
工具能力
包括一些常见的工具函数库及一些通用功能,如:
- 状态机
status
- 数据缓存
storage
- 消息订阅
event
- 上传、预加载等
可根据项目实际需求进行删减;
组件层
代码路径:sub-package/components
组件层提供了一些各项目都会用到的常用的底层组件如弹窗 modal
、header
标题栏、分页 paging
等,可以根据项目各自要求而进行统一的扩展定制,另外还有两个特殊的小程序页面组件:
debug
页
debug
页用于前端相关的调试与测试。一般包含以下功能:
- 当前用户信息、应用信息的展示,用于定位用户测试;
- 环境切换;
- 泳道切换;
- 测试企业切换;
- 清理功能;
- 自定义测试功能;
通常在业务页面上某个角落设置连击 5 次进入 debug
页面进行调试。
扫码中转页
扫码中转页为一个空白页,依赖客户微信管理后台里的扫普通链接二维码跳转小程序功能。当该功能配置后,扫带参码进入扫码中转页,中转页获取相关参数可以设置当前小程序的环境、泳道、企业等,并且立即跳转到指定页面或默认页面,小程序销毁时失效。可以方便的通过微信扫码完成各种 case
及多人同时测试的场景。
业务层
上述的部分通常无需做代码逻辑调整,各客户的配置也都收敛在 env.js
里,而业务层则是不同客户主要的定开逻辑,这部分的开发是整个项目开发的核心,也是每一个参与的开发人员重点关注的地方。
其他层与业务层无耦合,意味着子包脚手架模板不仅仅只支持优码定制化业务(下文的优码业务层通用化剥离),而是可插拔式的支持任何其他业务,是一套可移植、可扩展的脚手架模板。
业务层其中主要包括 3 大块,客户 SDK
获取、业务逻辑,业务通用组件。
客户 SDK
获取
作为子包交付,是无法拥有独立获取小程序用户登录态或其他需要解密数据的能力,所以需要用到客户主包提供的获取相关数据的方法。对于不同的客户,提供的方法也不尽相同,如有的客户小程序提供了专门的 sdk
方法供子包调用获取,而有些小程序则是直接将相关数据注入到子包页面等等,所以这部分需要针对各客户进行定制化的开发。
通用会话模块会调用定开层暴露的统一获取数据的方法,而定开业务层则在内部自行完成客户数据的获取,与服务层解耦;
定开层代码示例:
const getCustomUserInfo = async (): UserInfo => {
const userInfo = await getApp().getSdk().getUserInfo();
return userInfo;
};
通用服务层 session
代码示例:
async getNewUser(){
try {
const userInfo:UserInfo = await getCustomUserInfo();
//...
} catch (error) {
//...
}
}
而在本地开发模式,在服务层(SDK mock
)模块里则通过 mock
一份客户数据如登录等,并通过模拟方法来完整模拟客户 sdk
的接口类型,在定开业务层的调用保持与真实环境的一致性。
本地 MOCK SDK
代码示例:
app.js
import { JsSdk } from './mock/index';
const jsSdk = new JsSdk();
App({
//...
getSdk() {
return JsSdk;
},
});
mock/index.js
import { mockdata, UserInfo } from './data';
class JsSdk {
getUserInfo: () => Promise<UserInfo>;
constructor() {
this.getUserInfo = () =>
new Promise() <
UserInfo >
((resolve) => {
setTimeout(() => {
resolve(mockdata.userInfo);
}, 500);
});
}
}
export { JsSdk };
其中 data.ts
即为模拟客户数据。
业务逻辑
代码路径:sub-package
/各页面模块
这部分是实际交付的定开业务,包含各定开小程序页面模块,完成客户定制化需求。其中用到的路径均为相对路径,防止合包引起的路径不一致的问题;
其中约定需要上传的静态资源存放在各自页面模块下的 @cdn
文件夹里,构建时会将所有 @cdn
文件夹里的资源合并到一个文件夹里,在流水线的时候会统一上传。
业务通用组件
代码路径:sub-package/components/biz
本业务相关通用的一些定制组件,可由通用组件层扩展而来,为本项目定制化服务。如活动统一的规则弹窗,统一的活动列表组件等等;
cli 工具
通过 cli
工具,可一键生成带有优码特定业务场景的子包脚手架模板或组件库等,同时可以配置是否子应用隔离,帮助开发者提升开发体验,让开发更聚焦于业务。
优码业务层通用化剥离
基于企业交付业务的独特场景,而其中又以优码扫码业务模块功能交付居多,故对优码业务层进行通用化梳理并提取核心链路逻辑,用于快速开发支持优码业务。
通过对多个客户接入优码的业务主链路进行分析,得到以下主要链路模块:
- 扫码链路(码着陆处理、活动匹配)
- 抽奖链路(抽奖逻辑、中奖状态)
- 领奖链路(领奖逻辑、奖品类型)
- 奖品类型:劵码、实物、积分、红包
- 实名认证
- 收货地址
- 个人中心(列表详情、二次领取)
如图(扫码抽奖活动):
将业务进行最核心的 UI
与逻辑抽离,得到包括通用 API
接口在内的完整链路的’页面与逻辑‘绑定的小程序页面模板,结合 API
返回将各边界条件都已通过了完备的处理。在实际开发中,开发者只需关注与标准链路的差异点,主要完成 UI
开发及新增或调整的功能,根据客户不同诉求进行二次开发即可;
相关的通用固定的业务相关的逻辑,如抽奖方法,抽奖弹窗等,即抽离为公共方法或组件直接复用。
这里为什么不将各页面业务功能高度抽象配置化呢?是因为各客户需求差异巨大,UI
风格迥异,将业务组件配置化反而会增加学习成本和开发成本,增加使用者心智负担,不如直接二次开发效率高。
未来根据优码通用化需求不断丰富其主链路功能模块,如加入积分商城模块、游戏互动模块等等。
流水线
根据业务定开子包交付的特殊场景,流水线结合模板 npm scripts
支持了以下能力:
- 多企业:每新增一个客户,只需要新建一个
task
来录入客户对应项目相关信息(如appid
,git
项目地址等),后续只需运行即可不再做任何调整; - 多环境:支持打包为
QA
、GREEN
、PROD
环境; - 多模式:支持打包为整体模拟和生产子包;整体模拟则是包含
mock
数据在内测试小程序,用于和包前的开发、测试同学在本地的功能测试,而不需要依赖客户方的合包发布;产品子包则是最终交付给客户的子包模块,用于合入客户小程序。 - 代码审查:接入
codecc
完成代码审查,存在代码缺陷则禁止 MR; CDN
上传:将静态资源统一上传到CDN
;- 企微推送:构建动态信息同步并将构建后的子包发布到企微群;
实现完成一条流水线,搞定所有客户的定开构建全链路支持,大幅度提高交付效率与正确性。
总结
通过脚手架模版的构建到前端工程化的实施,我们在以下几个方面取得了成果:
- 新增定开子包脚手架模板及其对应的
cli
工具,使得开发者能够轻松地一键生成子包项目,同时支持普通分包和独立分包,帮助开发者提升开发体验,让开发更聚焦于业务; - 采用分层设计的脚手架,使得开发者能将重心聚焦在业务层,无需关注其他功能,从而提高交付效率;
- 落地并实施统一的开发规范,通过在本地及远端都设有门禁检查,保障代码质量;
- 通过构建层接入子应用隔离,避免了在合包之后因全局污染冲突产生一些难以调试的
BUG
;同时因其对代码无侵入,开发者在开发过程中无需刻意关注; - 针对优码业务的核心链路通用化剥离复用,使开发者能够快速支持优码业务相关的客户定制化需求;
- 构建完善的自动化流水线,实现构建编译全链路优化,通过机器简化发布过程的重复劳动,通过流程保证发布的质量;
整体脚手架模板结构较为简单,这是根据当前的定开交付形态来决定的。在实践中要避免过度设计从而增加了开发者使用的心智负担,反而增加了交付的复杂性。一切要以提高交付效率,保证交付质量为核心目标。
麻雀虽小五脏俱全,作为一个可移植、高扩展性的脚手架模版及其工程化的配套,未来交付中心开发组会根据实际应用及业务场景不断优化改善,继续提高开发体验及交付效率,保障交付质量。