小程序时代:如何跨多小程序开发协作?
导语
小程序端越来越多,跨平台开发框架逐渐成为开发小程序的主流,目前跨平台开发有较多的开源解决方案,本文介绍一种简单而有效的方法,解决复杂小程序的跨团队开发协作,希望对你有所帮助。
背景
目前市面上小程序端越来越多,跨平台开发框架逐渐成为开发小程序的主流。目前跨平台开发有较多的开源解决方案,比如美团点评的mpvue、滴滴的Chameleon、凹凸实验室的Taro等,都做得比较好。这些框架帮我们解决了一处开发,多处运行的难点。但是在复杂的业务场景中,最终落地也存在着许多困难,需要自己解决。以58房产的新房业务小程序为例,实际的业务场景中,既需要有独立承载功能的“58同城新房楼盘精选”小程序,也有依托于其他流量平台的入口,比如在“58同城”、“安居客买房”等都有相应的新房业务,同时还有交叉的业务场景,比如在同经纪人业务结合的“安居客经纪人网店”中展示新房楼盘。这其中既有微信小程序,也有百度小程序。
这些业务有较多的共同点,比如基础功能逻辑是一致的,但是也存在很多差异性,比如商业逻辑,页面皮肤以及一些差异功能点。
新房首页对比
上图为三个小程序的首页对比,可以看到独立的小程序“58同城新房楼盘精选”集成了账号、城市、消息,在“58同城”和“安居客买房”这些能力则是依赖主体小程序。另外三个小程序之间还有些细微差别,但是楼盘相关的基础功能确是相同的。
一处开发多处运行的难题
作为业务方,我们希望业务代码也可以一处开发,到处运行。方案设计之时,我们的目标便是业务代码在同一仓库管理,同时方案具备较大的灵活性以适配各种环境。
在上述的背景下,实际开发中会遇到如下困难:
a) 各个小程序归属的开发团队不一样,使用的开发方案也不一样,有原生开发、wepy、Taro、mpvue等,意味着在源码层面是难以进行协作开发的;
b) 业务方与平台方之间是跨团队协作,需要尽量减少耦合,提高协作效率,同时避免相互影响;
c) 需要具备在各个小程序环境中的差异化开发方案;
d) 所有业务代码同一地方管理,意味着会有不必要的代码,需要有机制保障最终的打包结果大小是最优的;
e) 在不同平台小程序中,会依赖他们各自提供的基础能力,比如账户体系,消息等,这部分在各平台小程序中也存在着一定差异性;
f) 在不同场景下需要具备不同的接入方案,支持微信插件方式接入平台小程序,也要支持业务分包方式接入平台小程序。
整体架构设计
本方案基于Taro 1.3版本实现,其他小程序框架也可使用相同的方法做改造。在现有Taro基础上,无法支持到一份源码打包成多个同类型的小程序,因此在现有配置层进行扩展处理,并添加适配层,对于各个小程序不同点进行处理,最终实现直接打包到多个不同的小程序中,整体的架构主要分为四层:
a) 配置层,用于解决在不同场景下的差异化,包括环境变量、主题样式、页面配置等;
b) 源码层,为具体的业务代码,常见方案,不做具体介绍;
c) 适配层,用于对接不同方案下小程序提供的接口,并牟平不同小程序提供的接口差异,为源码层提供统一的接口;
d) 打包层,与配置层相结合,用于打包最终交付结果;
以新房为例的架构图:
1. 配置层处理
1) 打包脚本配置
若要支持多小程序开发,需在package.json中增加scripts,用于区分环境。这里我们用的是cross-env这个包来设置,比如在打包58同城小程序时,加入环境变量WEAPPSOURCE=wbweapp。
{
"build:weapp": "taro build --type weapp",
"build:wbweapp": "cross-env WEAPPSOURCE=wbweapp taro build --type weapp",
"dev:wbweapp": "cross-env WEAPPSOURCE=wbweapp npm run build:weapp -- --watch",
}
然后在`config/index.js`配置defineConstants,用来配置一些编译时的全局变量供代码中使用,这里的配置会用于做打包的差异化处理。大部分的差异化配置,我们都放到了编译时来进行配置,有助于降低代码打包后的大小。其原理是通过webpack的define-plugin和uglifyjs-webpack-plugin两个插件配合来删除掉不可达代码,保证不使用的代码不会被打包。
config.defineConstants = {
WEAPPSOURCE: JSON.stringify(process.env.WEAPPSOURCE),
WBWEAPP: '"wbweapp"',
AJKWEAPP: '"ajkweapp"',
}
2) 差异性样式处理
在现有业务中,需要同时支持58同城和安居客两个品牌。二者之间页面结构是一致的,但各自有些主题色,我们将这部分差异抽取出来,变成Sass变量,然后整合至一个scss文件中,通过编译时引入不同的scss文件,来达到切换主题的作用。这里主要是配置`config/index.js`中的`config.plugins.sass.resource` 。
const sassConfig = {
wbweapp: '../wbweapp.scss',
ajkweapp: '../ajkweapp.scss'
}
config.plugins.sass.resource = path.resolve(__dirname, sassConfig[process.env.WEAPPSOURCE])
3)差异化页面处理
源码层中会包含所有场景下的全量页面,但每个场景所需的页面只是其中的一部分,需要做差异化处理。处理方法同上,略有差异点,通过编译打包时pages的配置不同,在`app.tsx`中的pages是决定引入哪些页面,我们通过传入环境变量找到对应的配置页面,实现按需配置打包。
`config/index.js`中配置:
const pagesConfig = {
wbweapp: ['pages/a'],
ajkweapp: ['pages/b']
}
config.defineConstants = {
PAGES: JSON.stringify(pagesConfig[process.env.WEAPPSOURCE])
}
`app.tsx`中配置:
class App extends Component {
config: Config = {
pages: PAGES,
}
}
2. 适配层处理
1) 差异化功能处理
功能的差异化处理,使用配置层定义的全局变量来做,伪代码如下:
import TabBar from '../components/tabbar';
export default class _C extends Component {
render() {
return {(WEAPPSOURCE == WBWEAPP) && <TabBar/>}
}
}
这样写的话,当WEAPPSOURCE !== WBWEAPP时,TabBar组件不会被打包到最终代码中,wxml文件中TabBar的代码块也不会有。上面的import是不需要做特殊处理,打包时会分析依赖关系,没有被最终使用的文件不会被编译。
2) 接口统一封装处理
在各个平台方小程序中,通用功能都应该是统一管理的。比如用户信息,用户在58同城小程序内进行登录,各业务都能拿到统一的用户信息,而不是进入新房页面后再做一次新房的登录。这些功能,由平台方提供接口,供业务方调用。但各个平台存在差异性,这些差异性就由适配层做统一的封装,对业务开发提供一致的接口。
比如获取城市信息:
export const getCityInfo = () => {
if (WEAPP_SOURCE == WBWEAPP) {
city_info = WBIndex.WB.getCityInfo()
} else if (WEAPP_SOURCE == AJKWEAPP) {
city_info = AJKIndex.Common.getCityInfo();
}
}
原理解析
通过以上介绍,已经解决了我们对差异化开发的要求,同时适配层将平台接口差异牟平,业务开发也不需要关心所处环境。大家可能比较好奇,所有的小程序代码都放在一起管理,最终打包出来的代码大小是不是最优的?主要是以下两点:
依赖分析优化工作主要是由@tarojs/cli包来完成的,简化后的流程图如下:
首先是解析入口文件`app.tsx`,通过两次语法转换,一次语法遍历,得到了依赖的样式文件、依赖的js文件、app的配置等,以及入口文件app.js。样式文件编译成最终的app.css,依赖的js文件,通过拷贝或生成,放到指定的目录中,app配置生成app.json。
两次语法转换是不一样的,第一次是通用的语法转换,比如jsx语法的处理。第二次是差异化的转换,会根据当前转换的类型是入口文件、页面文件或组件文件做一些特殊处理。第二次转换时使用了babel-plugin-danger-remove-unused-import插件,会删除不必要的依赖引入。上文提到的TabBar组件,虽然是被引入了,但在不需要的场景下TabBar组件就不会被打包。这里需要注意引入的文件,不应该存在副作用。
解析完入口文件后,会得到app配置的pages列表,页面文件列表循环通过同样的过程,得到页面的样式、js、配置等,以及所依赖的组件列表。
组件文件的打包过程跟页面是基本一致的,区别点在于组件会依赖其他组件。
理解了整个打包的流程,上面的问题答案就比较清晰了,不在pages配置里的页面是不会最终打包输出的,没有被依赖到的文件也是不会经过打包处理的。
与平台小程序集成
小程序最终的集成发布有三种方式:独立发布、插件集成、分包集成。
多个小程序的不同集成方案
1. 插件集成发布
如果是通过小程序插件方式集成,平台小程序可以将接口统一挂载到插件的变量中,二者就桥接上了。
插件的index.js设置(上文WBIndex即为引入的此文件):
module.exports = {
WB: {},
}
平台小程序接口注入方法:
const plugin = requirePlugin("xinfang");
plugin.WB = {
getCityInfo: function() {}
}
2. 分包集成发布
如果是分包集成的话,可以考虑将接口直接挂载在App中。
平台小程序接口注入方法(上文AJKIndex即为getApp()):
getApp().Common.getCityInfo = function() {}
采用分包集成方案的话需要注意,因为双方是在各自仓库下分别开发的,最终需要和到一起进行打包发布。目前我们采用的方案是配置`config.outputRoot`将结果代码打包到平台小程序仓库中,通过git管理,再由平台小程序做发布。
3.独立小程序发布
方案跟分包集成发布是一致的,不过API由自己提供,也挂载在App中,同时扮演了平台方和业务方。
实践经验分享
问题解决
前面提到一处开发多处运行的难题,得到了一一解决,整理如下:
a) 源码层面无法进行跨团队协作开发?
团队间分仓库开发,最终代码通过微信插件方式,或者分包方式进行集成。
b) 业务方与平台方之间的如何解耦?
通过统一的API,进行桥接,无其他耦合,API根据集成方式的不同,有不同的挂载方案。
c) 如何进行差异化开发?
针对样式差异化,配置差异化,功能差异化均给出了方法。
d) 如何保证打包结果是最优的?
尽可能的利用编译时的条件编译方法,排除不必要代码。
e) 平台方接口的差异性如何牟平?
增加了适配层,对业务提供一致的输入输出接口。
f) 支持不同平台小程序的多种接入方案?
支持了插件接入与分包接入。
总结与规划
本文介绍了在较复杂的小程序业务场景中,跨多小程序跨团队的协作方案,该方案帮助了新房业务在多小程序中的快速落地及迭代。
在实现了“58同城”小程序中的新房业务接入后,我们又做了“58同镇”的新房业务对接。只需要“58同镇”小程序提供一致的基础能力接口,即可轻松接入。
本文内容主要为业务经验积累,整体方案易于实施,带来的业务开发提效却是显著的,希望能帮助到大家。实际业务落地过程中,还有较多的细节需要处理,无法一一列举,欢迎提问或咨询。
文中仅介绍了业务在微信小程序的实践情况,实际上在百度小程序以及H5也已有相应落地实践,具备了一定的通用性,可以放心使用。
随着业务覆盖的范围越来越广,适配层会越来越复杂,不利于维护,更有效的方案是把业务实践总结为一套通用的接口标准,各个小程序按统一标准来实现API,业务方可以不关心所处环境的差异性,进一步提高跨团队开发的协作效率。
参考文献
1. https://www.npmjs.com/package/cross-env
2. https://nervjs.github.io/taro/docs/config.html
3. https://webpack.js.org/plugins/define-plugin/
4. https://www.npmjs.com/package/uglifyjs-webpack-plugin
5. https://github.com/mishoo/UglifyJS2#compress-options
作者简介
陈志庆 58同城架构师
叶春喜 58同城高级研发工程师
live
沙龙活动直播
2020年58技术沙龙活动在线直播第一弹——《大数据平台建设实践与探讨》已准备就绪,欢迎你强势围观!
详情🔎请戳👆图片查看,2月22日本周六19:00,我们不见不散。
END
阅读推荐