基于 weex 的有赞无线开发框架
出于对开发效率和动态化的要求,无线端的开发框架也一直在更新,从 Hybrid、结构化 Native View、React Native、Weex,再到现在正在大受关注的 Flutter。什么样的框架才是适合自己的团队?不仅要有技术追求,而且要考虑实际业务需要。最近,有赞移动选择了 weex 作为无线开发框架,搭建了从开发、Debug、构建、发布、数据一个闭环的流程。本文将对此进行分享。
一、什么是 weex
Weex 是阿里巴巴开源的一套构建高性能、可扩展的原生应用跨平台开发方案。首先总结一下 weex 的特点:
Weex 也不是只支持 Vue 和 Rax,你也可以把自己喜欢的前端框架集成到 Weex 中,有一个文档扩展前端框架描述了如何实现,但是这个过程仍然非常复杂和棘手,你需要了解关于 js-native 之间通信和原生渲染引擎的许多底层细节。
- 一次编写,三端(Android、iOS、前端)运行
前提是都集成了 weex sdk,另外视觉表现做不到完全一样,有的会有一些差异,需要做一下适配。所以写 weex 页面的时候,如果支持三端,便需要在三端都进行自测。
- UI 的绘制通过 native 的组件,JavaScript 逻辑在 JS 引擎里运行,两者通过 JavaScriptCore 通信
weex 里使用组件都需要在 native 端注册,这样 weex 里才可以使用,运行的时候通过注册时记录的 map 进行查找。weex sdk 内置注册了一些基础的组件,包括 list、text、input 等。WXJSCoreBridge 封装了 JavaScriptCore 实现 native 和 js 之间的通信。
- 支持 Native 扩展
可以将 native 的 UI 组件封装成 component,将 native 的逻辑代码封装成 module。从而在 weex 里可以进行使用。这里的 natiev UI 组件包括 modal、webview、image 等,这里的 native 逻辑代码包括 storage、network 等。
- 每个 weex 页面会被打包成一个 js 文件,weex sdk 将 js 文件渲染成一个 view
weex 的打包通过 webpack,将每个页面打包成独立的一个 js 文件,weex sdk 会将 js 进行解析,将 UI 部分绘制成一个 view, 再绑定 view 的事件与 js 代码绑定。
二、为什么要使用 weex 进行无线开发
1. 效率问题
1)开发的人力成本
如果不算 web 端,一个页面本来需要 Android 和 iOS 2 个人开发;使用 weex 后只需要 1 个开发页面。
2)开发的编译速度
随着项目渐渐变得庞大,Android 项目一次编译需要 2-3 分钟,机器不好的还需要 10 分钟,iOS 可能会快一点,也需要 1-2 分钟。使用 weex 后,界面修改,只需要十几秒。
3)测试效率
提测之后,发现 bug,修复完成,测试总需要重新下载一个包进行安装;使用 weex 后,跟原生无关的 bug,只要测试重启 App 就可以进行验证。
2. 动态化
weex 页面最后打包完是一个 js 文件,只要能做到动态下发 JavaScript,那便可以实现动态化,可以热修复,甚至可以热部署,完全替换或者新增页面。
3. 成熟度
在 2016 年阿里双十一中,Weex 在阿里双十一会场中的覆盖率接近 99%,页面数量接近 2000,覆盖了包括主会场、分会场、分分会场、人群会场在内几乎所有的阿里双十一会场业务。阿里双十一主会场秒开率 97%,全部会场页面达到 93%。
2016 年 12 月 15 日,阿里巴巴宣布将移动开源项目 Weex 捐赠给 Apache 基金会开始孵化。
2017 年,weex 在阿里业务里增长如下图,来自 WeexConf 2018。
4. 接入成本
经过实践,一个移动端开发,一周时间就可以开始进行使用 weex 进行业务开发。
三、如何使用 weex 进行无线开发
weex 其实是一套方案,各个流程很多东西需要自己建设,把它建设得让小伙伴可以以较小成本开始使用 weex,把它建设得融入已有的系统。这方面,我们目前做了下面这几个方面,还任重道远。
1. 开发工具 zweex-toolkit
这是一个脚手架工具,基于 weex 官方的 weex-toolkit,用于新建 weex 工程,目前只支持 vue。
随着页面的增多,业务的复杂,工程会慢慢变得庞大,每次运行的时候如果全部页面都运行起来比较慢。为了解决这个问题,使用 zweex-toolkit 创建建的工程模板支持运行的时候,支持只运行指定目录下的页面,只要在 npm start 后加上参数即可,如:
npm run start hi,helloworld
这样就表示只运行 hi 目录下和 helloworld 下的页面。
另外,我们支持:
- 新增页面
zweex page
- 开启调试
zweex debug
2. ZanWeex SDK 的实现
官方 weex sdk 做的事情,就是输入一个 js 文件,然后返回一个 view。考虑到每个应用的路由和个性化的需要,这一点,ZanWeex SDK 没有做其他工作,也还是返回了一个 view,业务方可以根据自己的需要将 view 添加到自己想要展示的地方。ZanWeex SDK 做的事情主要有如下几方面:
1)支持下发配置,支持动态化,可以完成整个页面的替换
weex 页面打包后的结果是一个 js 文件,所以可以进行下发进行动态更新,那么就需要有一份配置,来关联页面路由和 js 文件的关系,于是我们设计了这样的数据结构:
h5:页面路由地址,可以直接使用发布平台生成的 h5 地址
js:打包后的 js 文件地址
version:支持的最低 App 版本,因为新页面如果需要 native 扩展,那就需要发布新版本进行支持
md5:为了校验完整性,我们在配置里添加每个 js 文件的 md5。
2)支持多模块独立配置,互不影响
一个 App 里会有多个模块,每个模块可能由独立的团队进行负责,所以为了减少耦合,我们将配置独立,每个模块可以独立管理自己的配置,独立接入 weex,不依赖于宿主 App。
3)预加载页面模板,支持页面模板缓存和配置缓存
- 如果没有缓存,每次都从服务端拉取页面模板,那么是不可能达到秒开的,跟没有做缓存的 H5 页面就区别不大了。我们 SDK 会预加载页面模板到本地,打开过的页面会缓存到内存。这样渲染的时间就更接近原生的渲染时间了。
4)支持开发时的 hot reloading,前端开发般的体验
- 如果没有 hot reloading,那么每次修改完页面,都得退出页面重新进入。为了省去这个操作,hot reloading 是必须的。
- weex 工程里本地开发时候,通过 webpack-dev-server 来启动一个 websocket,zan weex sdk 打开一个 weex 页面后,去与它建立连接。webpack-dev-server 将工程的编译状态发送给 ZanWeex SDK,当接收到渲染完成的指令时,就重新渲染页面,从而达到 hot reloading 的目的。
5)支持页面的适配,提供环境变量
ZanWeex SDK 会提供以下四个变量共 weex 页面使用,方便完成页面配置。
- 容器的高度:weex.config.yzenv.viewHeight
- 容器的宽度:weex.config.yzenv.viewWidth
- 状态栏高度:weex.config.yzenv.statusBarHeight
- 底部栏高度(针对 iPhone X,其他为 0):weex.config.yzenv.bottomHeight
6)开发阶段日志的查看
在开发阶段,weex sdk 源码里输出的日志以及 js 里通过 console.log 输出的日志,还有 js 运行的报错,都只能通过 XCode 和 Android Studio 进行查看。这对于一个只了解一端的开发人员是非常不方便的。于是我们做了一个入口,在打开 weex 页面的时候,会显示该入口,点击即可查看所输出的日志。
7)参数传递
正向传参:从 A 页面跳转到 B 页面,参数传递是开发过程肯定会遇见的一个场景。SDK 对外提供的渲染接口 renderByH5 的参数包括 url,params,data。业务方进行渲染的时候,可以将参数直接跟在 url 后面,或者通过 params、data 传入,不同方式,取的方式也不一样:
-
url 后面的参数,会传入 data,weex 页面里直接在 data 里定义参数就会自动赋值;
-
params 的参数,在 weex 页面里可以通过 weex.config.name 来获取;
-
data 传入的参数,获取方式同第一种。
-
反向传参:从 B 页面返回到 A 页面的时候,携带参数返回也是很常见的一个场景。SDK 提供了统一的存储类 ZParamStorage 来临时存储参数。页面 B 要返回的时候先把数据存入存储区,A 页面显示的时候再从存储区获取,然后清空存储区。
- 非跳转的参数传递:weex 页面之间,可以采用 BroadcastChannel 进行传参,weex 与 native 之间的传递可以通过自己封装 Module 进行实现。
3. 页面的开发
前面有提到,weex 的页面目前可以采用 vue 或者 Rax 编写。对于 Vue 和 Rax 的语法这里不做陈述。这里主要总结了容易在实际开发中卡住小伙伴的几个问题。
1)如何判断一个页面是否用 weex 来实现?
可以认为所有的新页面都可以采取 weex 来开发,区别在于这个页面使用的 native 能力有多少。可以通过自定义 Module 来调用 native 的能力,通过自定义 component 来使用 native 的组件;
2)什么时候需要自定义 Module?
- 需要原生的能力的时候,比如:
- 要调用系统选择图片的接口
- 调用打电话、发短信的功能
- 打开其他应用
- 调用已有的业务逻辑,比如:
- 加密、解密逻辑
- 登录逻辑
3)什么时候需要自定义 component?
- 如果一个组件已经使用 native 实现,为了保持统一一致,那么可以将原有的组件封装成 component
- 如果一个组件不能使用 weex 实现,比如地图组件、超长图显示等
4)多个弹层的布局如何实现?
weex 页面渲染的层级,是从上而下的,越在下面的布局,显示越上层。所以要作为弹层的布局,就把它放到最下面。
5)页面的动画如何实现?
官方 weex sdk 已经封装了 animation 的 module 可以直接使用,复杂的动画可以使用 BindingX 实现。
6)weex 的代码如何复用?
代码都可以抽离出组件。
- 作为一个 UI 组件,抽离成一个组件,向外暴露属性参数和事件接口;
- 作为独立的 js 函数,抽离成一个 js 供其他页面引入;
- css 样式也可以抽离成一个 css 文件,供其他页面引入;
- 如果包含多个组件形式,可以通过 mixins 来引入。
4. 构建和打包平台
我们开发了以项目为单位的构建平台:
- 每个项目可以添加多个分支,可以是不同仓库的分支。因为一个项目有可能是跨团队跨模块的,但是需要一起发布。
- 构建通过 webpack 构建,构建之后,支持发布线下存储和线上 cdn
我们还开发了以应用为单位的 weex 发布平台:
- 这里的应用是一个抽象概念,不是传统的“应用”,可以理解成模块
- 业务方可以在构建平台构建完成后,一键跳转到发布平台进行发布,除了需要第一次填写最低支持的版本号,其他均无需操作。
- 发布平台支持灰度发布、全量发布和回滚。
- 发布平台会展示 weex 在端上的使用情况,渲染时间、渲染错误、下载时间等
四、遇到的问题以及解决方案
在开发过程中,很多问题,可以通过阅读源码来解决,比如:
- 使用 iconfont 的时候,是否已支持缓存?
答:已支持,包括内存缓存和文件缓存,内存缓存使用 familyname 来做 key,文件缓存使用 md5(url) 来做本地文件名
- module 实现的函数能不能返回参数?
答:module 的函数氛围 UIThread 和 JSThread,JSThread 对于 js 线程来说是同步的,支持直接返回参数;UIThread 对于 JS 线程来说是异步的,不支持直接返回参数,只能使用 callback
另外,很多常见的问题,我们已经在 ZanWeexSDK 进行了解决,包括实现动态化、多模块的支持、缓存管理、Hot Reloading、日志查看、页面适配、参数传递等。
此外,还会有一些常见的问题,在此罗列一下:
- 配置的更新机制是怎样的?更新失败,如何打开 weex 页面?
答: 配置的更新接口开放给业务方调用,由业务方决定什么时候调用更新接口;SDK 里做了三种处理,来尽量保证配置可以更新成功:
1)配置接口拉取失败后,会有三次重试;
2)网络从无网变成有网时,sdk 会检查配置是否已拉取,如果未拉取就主动拉取
3)允许业务方内置配置和 js 文件,当拉取失败后,SDK 里会从内置配置里读取
- 配置的版本管理是怎样的?
答:配置每次发布的时候,都会指定该发布支持的 App 最低版本号。每次请求,会携带 App 版本号,服务端只会返回符合该版本号的最新配置。
- 支持不支持屏幕旋转?
答:答案是支持的。旋转之后,屏幕变成了横屏,weex 就按照横屏的尺寸来渲染,问题是只要你写的页面符合这种变化就可以了,跟 native 来实现页面没有什么区别。
五、未来还要继续做的事情
- 组件库的建设
- 性能统计,比如帧率、内存、CPU
- 配置和 js 文件的增量更新、推送更新
- 降级处理