【第1635期】青桔单车 chameleon 跨平台实践分享
前言
青桔单车 chameleon 跨端实践,来看看。今日早读文章由滴滴@不懂小彬投稿分享。
@不懂小彬,一个不像程序员的程序员,在分布式架构、前后端分离模式上有一定研究,喜欢折腾适用不同业务的框架,爱好踢足球,Coding。
正文从这开始~~
目录
前言
背景
行业现状——百家争鸣
业务要求——高效稳定多入口
框架期望
实践
组件调用-父子组件通信
数据的管理-store
cml 框架设计
数据 mock
CML 配置
跨平台框架——chameleon
青桔单车业务技术架构
跨端技术方案设计
多端界面一致性
差异化(定制化)
工程化
框架设计
性能
性能提升
包大小
总结
开篇
近些年,整个前端领域发展迅速,效率型的前端框架也层出不穷,每个团队选择的技术解决方案都不太一致,因为互联网的特性及中国自身的特色,各个产品对于多端的投放的需求是一致的。像小程序这种跨端场景和现有的开发方式也不一样,为了满足业务的需求技术人员在日常开发中会因为投放平台的不一样而进行多套代码的维护,效率较低且成本也高。跨端框架应景而生(这里就不过多介绍各个跨端的框架了)。
在我看来大前端是趋势,跨端是趋势的第一步,而对于技术人员来说在不影响功能体验的情况下能解决维护多套代码的痛点非常重要,改编一下比较流行的一句话:「lean once,write once,run anywhere」
背景
行业现状 — 百家争鸣
经过将近两年的发展,小程序已经深入用户的日常生活,小程序应用数量超过了百万量级,覆盖200多个细分的行业,日活用户达到两个亿。与此同时,像支付宝、百度、头条、手Q等等都开始了自家的小程序生态,百家争鸣应景而生。青桔单车作为便民的出行工具,对于用户使用方式上也是成本越低体验越友好,即用即走的小程序已然成为平台选择的趋势。
业务要求 —高效稳定多入口
高效、稳定、多入口就是业务现在的要求,青桔单车是日活相对较高的小程序(目前在阿拉丁小程序 TOP 榜前10),这也要求我们对小程序的性能(加载、渲染、响应的时间)、稳定及安全有较高的标准。
同时业务也需要在各个平台上获得更多的入口,这就直接导致我们在选择框架,业务开发时要求比较严谨。
框架期望
从用户角度出发,为了减少用户使用成本(下载安装或更新 APP ),我们选择了市场上比较符合单车特性的平台作为入口。那么这时候对于研发来说就会有很多问题,我们在选择框架时,会对以下点有较高的期望或要求:
各端开发效果要高度一致,不能开发一次后用大量时间兼容多端
针对不同的端有差异化的实现方式,能较容易去扩展,以应对产品对不同端的差异化需求
工程化要好,因为前端过程式的开发比较低效,需要使用软件工程的技术和方法进行开发、管理
框架设计,接入不用改动太多代码,具有较好的抽象度
对性能上要求很高,包大小、加载、渲染、响应都需要严格考虑
实践
跨端技术方案设
☉ 跨平台框架——chameleon
引用 chameleon 官网的简介:
Chameleon 不仅仅是跨端解决方案,让开发者高效、低成本开发多端原生应用。基于优秀的前端打包工具 Webpack,吸收了业内多年来积累的最有用的工程化设计,提供了前端基础开发脚手架命令工具,帮助端开发者从开发、联调、测试、上线等全流程高效的完成业务开发。
从chameleon框架的架构设计图看:
在各个平台下chameleon基于其本身的运行池增加一层包括路由、自定义生命周期、组件、数据事件绑定及管理、样式渲染等相对完整的 DSL;
chameleon-tools 做编译(基于 webpack),提供语法检查、转译、依赖组装等;
chameleon-UI、chameleon-API 做各个平台样式的不一致、API差异化的抹平;
提供多态协议的方式解决组件及 API 的差异化;
综合来看 chameleon 的设计模式比较适合我们做跨端项目的开发,提升我们的效率,不维护多套重复的代码…
☉ 青桔单车业务技术架构
青桔单车简单的业务流程图:
青桔单车业务相对复杂,包括登录、认证、电子围栏、宣教、状态扭转等超过 30 个页面(不包括各种 H5 实现),用户主动打开/微信扫码进入小程序,完成登录后开始扫码开锁,开锁成功后 === 发单成功,用户开始骑行,骑行结束后完成支付整个流程结束(这里只提到核心流程),因为业务需要,我们需要维护微信、支付宝、高德(快应用、百度接入中)等众多小程序,对于研发来说最快的是 copy 一套代码然后针对性进行修改,当进行新功能开发的时候就痛苦了。
基于业务和多端的差异抹平方案考虑,最终CML青桔单车小程序技术方案设计图如下:
从青桔单车现在模块看,为了能真正的实现跨端开发,需要解决各平台间的差异问题:
各平台提供给小程序的 SDK 差异化,例如微信、支付宝的 getAuthCode,API 方法名不一样,返回也不一样,类似的问题如何解决?
业务中台 API 根据渠道/接入方案不同造成的问题如何解决?
页面内部-组件调用-父子组件通信、store 数据的管理如何处理?
硬件底层实现方案如何解决?例如微信、支付宝 BLE 底层实现的差异;百度小程序IOS 不支持 BLE,类似问题如何解决?
各平台规则限制导致功能无法使用,例如 webview 某些端考虑稳定性限制了 h5 — data 上行到端的问题如何解决?
其他问题, utils 组件差异化……
多端界面一致性
由于各端组件化的实现方式不一样 ( 微信 webcomponents 、百度模拟的组件化、支付宝是 react 框架),多端界面的一致性是一个比较麻烦的事情,从 cml 本身的设计及实际的体验来说,不论样式的单位换算还是组件的统一封装都做了较好的统一。
来点 gif 图,预览一下青桔单车小程序基于 cml 改版后三端的效果吧:
差异化(定制化)
我们按照业务场景拆分了几个公用的模块包括用户相关、发单、硬件/蓝牙通信、订单管理、营销等,每个模块都单独 store、action 及暴露的 commonApi,配合各个页面逻辑实现整个产品功能,针对差异化我们列举一个登录的例子通过多态方式来兼容微信、支付宝登录接口。我们在项目中 src/componets 中建立一个 API 的空间作多态管理 API,针对 login 我们按照 cml 的规范建立一个 login.interface 文件。
实现如下:
<script cml-type="interface">
// 定义入参
type RESObject = {
code: string,
errMsg: string
}
type done = (res: RESObject) => Void
interface MethodsInterface {
login(timeout: String, opt: done): Void
}
</script>
<script cml-type="wx">
class Login implements LoginInterface {
login (timeout, opt) {
wx.login(timeout, code => done({code: ''12', errMsg: '成功'}));
}
}
export default new Login();
</script>
<script cml-type="alipay">
class Login implements LoginInterface {
login (timeout, opt) {
my.getAuthCode(timeout, code => done({code: ''12', errMsg: '成功'}));
}
}
export default new Login();
</script>
这里想着重提一个非常容易被忽略的问题:interface 中一定要定义输入输出,规范各端实现规范,这是为了避免在修改某个端的方法入参(增加或减少)却没有考虑其他端的实现,如果全量测试会浪费很大人力且也不能保证都能覆盖到测试,为了可维护性,建议从一开始就坚持写多态组件的 interface,在程序层面上亡羊补牢有时候真的为时已晚……
接口定义后就可以用统一的方式进行调用:
import login from '../../components/Api/login/login.interface';
export const login = function ({commit}) {
return new Promise((resolve, reject) => {
login.login()
.then(({code}) => {
commit(types.SET_USER_CODE, {code});
resolve({code});
})
.catch(e => {
reject();
});
});
};
再比如,针对蓝牙通信的 API 微信需要端来做 ArrayBuffer 到 HexString 的转换而支付宝不需要,这里我们也采用多态进行接口方式抽离,微信进行转换,支付宝直接 return 原数据,整个 BLE 的过程非常复杂,为了提高连接,通信的成功率我们做了很多优化,如果直接在代码中 hack 会影响整个流程甚至造成整个蓝牙动作的不稳定,影响开锁率,做这层多态封装既不影响原有逻辑,改动也相对较少成本很低。
另一方面,PM 如果在某一端提个需求,例如针对支付宝用户在登录时候加一些特殊功能,我们能在不影响其他流程比进行改造,同时这是在物理上进行了隔离,可以发布单独 npm 包,可维护性比较高。
工程化
工程化是使用软件工程的技术和方法对项目的开发、上线和维护进行管理,因为前端过程式的开发比较低效,可以通过模块化、组件化、本地开发、上线部署自动化来提高研发效率。
☉ 常用执行命令
cml dev
编译全部
,cml wx dev编译微信
,启动开发模式,监听文件变化动态打包cml build
编译全部
,cml wx dev编译微信
, 构建生产环境
☉ 数据 mock
前端开发的过程中,数据 mock 是一个比较重要的功能,在验证逻辑、研发效率上及线上线下环境环境切换都起着很重要的作用。cml 这里也提供数据 mock 的功能。
import cml from "chameleon-api";
cml.get({
url: '/api/getUserInfo'
})
.then(res => {
// ...省略部分实现逻辑
cml.setStorage('user', ...res)
},
err => {
cml.showToast({
message: JSON.stringify(err),
duration: 2000
})
});
调用方法的参数 url 中只需要写 api 的路径。那么本地 dev 开发模式如何 mock 这个 api 请求以及 build 线上模式如何请求线上地址,就需要在配置文件中配置 apiPrefix。
// 设置api请求前缀
const testApiPrefix = 'http://test.api.com';
const apiPrefix = 'http://prod.api.com';
cml.config.merge({
wx: {
dev: {
apiPrefix: testApiPrefix
},
build: {
apiPrefix
}
}
})
☉ CML 配置
cml 支持本地 mock,使用方式是在 /mock/api/ 文件夹下创建 mock 数据的js文件,启动dev模式后(apiPrefix留空表示本地ip+端口),可以直接实现本地mock。
chameleon的构建过程是配置化的,项目的根目录下提供一个 chameleon.config.js文件,在该文件中可以使用全局对象 cml 的 api 去操作配置对象,针对不同端的配置方法,添加一个端还是比较简单的,在编译层配置的扩充性也有暴露
页面路由配置,chameleon 项目内置了一套各端统一的路由管理方式,为区分小程序,用 url 表示 h5,path 表示小程序,同时提供mock服务接口,在路由的管理上相对容易,在小程序端构建时会将 router.config.json 的内容,插入到 app.json 的 pages 字段,实现小程序端的路由。
框架设计
☉ 组件调用-父子组件通信
针对首页的消息流卡片我们会有登录、认证、订单、骑行卡等等不同的形式,我们封装了一个组件来处理,静态效果如下:
相对来说这个组件比较通用,我们大致分为 2 种类型,通知型与动作型,如上图 2 就是一个纯显示的消息,其他的是带有按钮的消息,在组件设计上我们根据调用组件时传的type 不一样来区分,组件使用上用 props 进行 data 传递。
通知型:TipsCard
动作型:ActionCard
下面是动作型的实现方式:
<template>
<view class="action-card">
<view class="{{'content '+(remindActive && 'bigSmall')}}">
// ...
</view>
</view>
</template>
<script>
class ActionCard {
mounted () {
EventBus.on(REMIND_CARD, ({index}) => {
if (index === this.index) this.remind();
});
}
methods = {
callback (e) {
EventBus.emit(ACTION_CARD, {index: this.index});
}
}
}
export default new ActionCard()
</script>
调用上,我们动态传递 component is 中的type来指定不同的消息流类型。
在自定义事件的处理上,我们通过 c-bind:action 绑定了一个 componentAction,通过 EventBus 事件来传递执行
<template>
<view>
<component is="{{item.type+'-card'}}" />
// ...
</view>
</template>
<script>
class Home {
beforeMount () {
this.tipsList = []
EventBus.on(ACTION_CARD, (data) => {
this.componentAction(data)
});
}
methods = {
componentAction(data) { ...// 执行实例 }
}
}
export default new Home();
最终实现的效果如下图
☉ 数据的管理-store
cml 框架作为数据驱动视图的框架,应用由数据驱动,数据逻辑变得很复杂,必需要一个好用高效的数据管理框架,我们可能在各个页面都需要 mapState,mapAction 来做组件的状态获取,事件分发等。
import createStore from 'chameleon-store';
import user from './user';
import location from './location';
// …………这里省略其他状态管理器
const store = createStore({
modules: {
user,
location,
bicycle,
// …………这里省略其他状态管理器
}
});
export default store;
这里就列出对 user 状态的入口操作
import mutations from './mutations';
import * as actions from './actions';
import * as getters from './getters';
const user = {
state: {
},
mutations: {
...mutations
},
actions: {
...actions
},
getters: {
...getters
}
};
export default user;
cml 提供了的 chameleon-store 模块包,且用法、写法和 vuex 一样,这点非常感人,手动点赞。
☉ cml 框架设计
cml 提供了计算属性—— computed、侦听属性—— watch、类 vuex 的数据管理及 DSL 语法,从学习成本上来看较小,符合当前的开发习惯。
<script>
class Home {
data = {
remindActive: false
}
watch = {
lockState: function(cur, old) {
return this.remindActive
}
}
computed = {
...store.mapState(['location', 'user'])
}
}
export default new Home();
</script>
基于青桔单车日活相对较高的特性,高效、稳定、多入口就是业务现在的要求,这要求我们对小程序的性能(加载、渲染、响应的时间)、稳定及安全有较高的标准。
性能
☉ 性能提升
性能上,cml 做了 array diff,因为小程序的主要运行性能瓶颈是 webview 和 js 虚拟机的传输性能,cml 尽可能 diff 出修改的部分进行传输,性能相对会更好,贴一下源码。
源码实现比较简单,但带来实际的用处还是比较大的,单车小程序有不少列表页面,当对于列表 data 进行重新赋值先进入 diff 函数过滤出实际改变的每一项,再进行 view-render,性能上会提高不少
☉ 包大小
包大小上,cml 相对之前开发的版本并没有大的变化,1.2mb 的编译后包大小基本保持不变,这归功于代码的压缩及其本身的 runtime 代码并不大,给个好评。
总结
“苦尽甘来”是青桔单车小程序接入 chameleon 跨端框架的总体感受。
苦:这里的”苦“我们到现在看其实是因为一种为未来做铺垫而需要做更多工作的“苦”,原本只需要考虑一端的 CASE 但现在要自己抽象出来一系列例如接口参数,通用组件,以及需要掌握更多的平台技术方案的“苦”。
甘:当我们通过 chameleon 将青桔单车小程序上线后,应对业务抛出来的新需求,我们再也不用维护多套代码了,也把因为维护多套代码导致可能某一端不一致性的风险给彻底排除了。不仅仅是 RD 同学受益,QA 同学验收的时候因为是一套代码逻辑所以可以节省一半的时间。本来3天要完成的需求现在1.5天就可以完成也极大的满足了业务方的需求,我们认为这就是技术驱动业务发展的一个例子,技术不能只是一个工具而是要从业务角度出发去实现才有真正的价值。
cml 做了 DSL 编译转化从根本上实现了各端的统一,引入组件多态、方法多态并抽象各端的配置可以更灵活抹平差异,方便配置、扩展。通过深入了解 chameleon 框架及项目实践,我们基于 cml 可以再抽象出一些公共层的组件、接口,当然这里是指的跨平台的组件、接口,因为只有这样才能更大化提升开发的效率,给业务带来更多的可能。
我们在选择框架的时候要考虑不仅要考虑其本身的性能、稳定、安全,包大小等,还需要看一下复用性及发展,比如是否能基于框架本身扩展出适合业务甚至公司级别的通用组件、API,是否方便随着业务的变化而能做到及时响应式的扩展、变动。
关于本文
作者:@不懂小彬
原文:https://mp.weixin.qq.com/s/AvwF6X5tXcBE2uHA4B0Tbw
为你推荐