vlambda博客
学习文章列表

加推Weex实践之路(上)

1、背景

1.1  为什么是Weex

在公司快速发展的大环境下,App的更新迭代高速、高频,技术团队平均两周便可诞生一款中型App,但App团队只有6个人(iOS 、Android各3人),在确保效率、质量的前提下,单纯依靠Native的能力显得步履蹒跚——我们亟需提升团队效率,希望单人可完成原本2~3人的工作量。

  • 其一,接入Web页面,一个页面适配两端;

  • 其二,选择Weex、React Native、Flutter、Chameleon等跨平台开发框架,主流框架对比如下:

对比内容

React Native

Flutter

Weex

上手难度

一般

一般

容易

接入特点

适合开发整体App

适合开发整体App

适合单页面

维护难度

一般

一般

容易

开发语言

React

Dart

Vue、Rax

框架体量

较重

重

较轻

Bundle大小

较大

不需要

较小

社区

丰富

新起之秀

不够完善

支持终端

Android、iOS

Android、iOS、Web等

Android、iOS、Web

引擎

JSCore、V8

Flutter Engine

JSCore、V8

通过对比,最终选择了Weex,有以下几个主要原因:

  1. Weex的上手成本较低,且单页面的支持更符合项目规划

  2. Vue框架,契合团队的大前端环境;

  3. Weex承接了淘宝、飞猪等App的大量页面,给予外界充足的信心。

1.2  Weex与Web

虽然Web页面的上手、维护成本更低,但与Weex相比,同等页面的Web包体积要比Weex大,Web页面无法做到纯Native的体验,且页面加载速度很难极致化,在某些设备上容易出现白屏。Weex所具备的下列优势,足以让一个追求极致的团队所青睐。

Weex的优势:

  • 完全的Native体验

  • 更小的包体积

  • 页面加载速度更快

  • 长列表性能更优

  • Native扩展性更加

2、Weex基本原理

Weex支持 Vue 和 Rax两个前端框架,由于前端团队使用Vue进行日常开发,为了降低上手成本,我们选择Vue框架进行Weex开发。Weex的基本工作流程如下(来自官网):

Weex we 文件 --------------前端(we源码)↓ (转换) ------------------前端(构建过程)JS Bundle -----------------前端(JS Bundle代码)↓ (部署) ------------------服务器或本地在服务器或本地上的JS bundle ----服务器或本地↓ (编译) ------------------ 客户端(JS引擎)虚拟 DOM 树 --------------- 客户端(Weex JS Framework)↓ (渲染) ------------------ 客户端(渲染引擎)Native视图 ---------------  客户端(渲染引擎)

除去前端页面的编写,Weex可划分为DOM、Render、JSBridge三大块(线程)。DOM负责JSBundle的解析、绑定、映射等处理,并通知Render线程进行UI的更新。而Render线程,即UI线程,负责Component的渲染,例如前端编写的<text>的标签,将被渲染成原生封装的UI组件TextComponent。JSBridge负责JS端与Native端的双向通信,通信的具体逻辑处理则由原生实现的一个个Module来完成。下图是一个页面被渲染的完整流程:

3、客户端系统架构

依托于Native,借助于前端,演变成大前端的架构,整体结构如下图。我们希望App作为终端,提供容器的能力,做好底层服务,完美融合Weex、Web等跨平台技术。

加推Weex实践之路(上)

4、实践与解决方案

以下记录了实践中遇到的一些较为重要、常见的问题,及其解决方案,它们贯穿了Weex页面的生命周期。

4.1  页面间通信

4.1.1  路由

Weex自身提供的navigator只支持简单的在线资源,而不支持本地文件的加载,同时无法满足动态化的呈现方式和复杂参数传递,需要自行实现一套完整的页面跳转规则。

在大量使用了Web、Weex技术的前提下,为了保证页面跳转、页面间参数传递、回调等的统一和便捷性,以及模块间的解耦,跳转目标可配置化,采用了路由的形式来作为页面间跳转的技术方案。基本的路由形式以及完整的过程如下:

// 打开Web页面scheme://web/open?bundleUrl=xxx
// 打开Weex页面scheme://weex/open?bundleUrl=xxx
// 打开native页面scheme://native/open?url=xxx
// 备注:这种形式的路由除了完成App内的跳转业务外,也能够完美的支持应用间的跳转

加推Weex实践之路(上)

Weex中发起一次页面跳转的示例:

navigator.openUrl({ url: 'scheme://weex/open', params: { bundleUrl: '/dist/about.js', name: '这是下一个页面所需的参数', },})// 备注: 而下一个页面只需要在data中声明一个与参数同名的属性来接收具体的参数即可。

4.1.2  反向传值

我们在考虑反向传值的问题时,主要涉及两种场景,一是Weex页面反向传值给Weex页面,二是Native反向传值给Weex页面。我们可以使用Weex提供的基于W3C 规范的BroadcastChannel来轻松满足第一种场景。但是目前并没有现成的API能满足第二种场景,我们需要不断的尝试:

  • 在页面跳转时将WXModuleKeepAliveCallback传入下一个页面,然后在合适的时候执行该回调。这对于iOS客户端很容易实现,但由于Android在页面间传值时需要将参数序列化到内存中,然后在对应的页面从内存中反序列化出来,这会导致生成一个新的对象,从而无法完成回调。

  • 我们有尝试借鉴BroadcastChannel的实现,通过Weex项目全局的JSContext对象来触发广播完成反向传值,但最后无疾而终。

我们最后选择使用fireGlobalEvent,步骤如下:

1.Weex添加事件监听const globalEvent = weex.requireModule('globalEvent');globalEvent.addEventListener("eventName", (e) => { // ...});
2.原生页面发送事件weexInstance.fireGlobalEventCallback("eventName", params); // Android
[weexInstance fireGlobalEvent:seventName params:params]; // iOS

代码中的weexInstance,可以理解为一个页面的实例对象,该流程需要在发送监听的页面需要获取上一个Weex页面对应的weexInstance,可以以一种相对优雅的方式告知下一级----在跳转的路由中添加一个needListen来告知下一个页面:

navigator.openUrl({ url: 'scheme://weex/open', params: { bundleUrl: '/dist/about.js', needListen: true, },})

4.2  页面配置文件

起初Weex页面导航栏、跳转方式等配置,均在自建Module中进行处理,例如一个页面要设置导航栏,我们需要在mounted方法中加入如下代码:

created () { navigator.setTitle('导航栏标题') navigator.setItems([{ title: '按钮', }])}

这种不够工程化、标准化的方式给后期的维护、跨项目移植、架构升级造成了极大的干扰。

我们参考小程序的设计思路进行了优化升级,为每一个需要特有化配置的Weex页面添加一个json格式的配置文件,配置文件包括导航栏的配置、页面级别的配置、跳转的配置等,将配置工程化、标准化。

例如我为about页面添加了配置文件,json文件的类容为:

{ "navigationBarTitle": "导航栏标题", "navigationItems": [{ "title": "按钮" }]}

打包后的资源文件目录如下,about.js、about.json文件在同级目录:

加推Weex实践之路(上)

那么一个完整的页面打开步骤如下:

加推Weex实践之路(上)     

经过扩展,配置文件变得更加丰富,先前麻烦的跳转处理、弹框等都可以通过配置文件实现,下面是一些常用的属性介绍:

4.2.1 部分属性

属性

类型

默认值

描述

backgroundColor

HexColor

同项目配置

窗口背景色

navigationBarBackgroundColor

HexColor

同项目配置

导航栏背景色

navigationBarHidden

Bool

false

隐藏导航栏

navigationBarTitle

String


导航栏标题

navigationBarTitleColor

HexColor

同项目配置

导航栏标题颜色

4.2.2 iOS特殊形式

针对于iOS客户端存在页面需要Present等情况,可以设置以下属性:

present

Bool

false

Present页面

presentWithNavigationBar

Bool

false

Present页面,并携带导航栏

transition

Map

false

以转场的形式呈现,并规定转场的动画表达式(默认背景alpha从0到1)

优先级:transition > presentWithNavigationBar > present

transition中需定义动画的表达式,Native则需要解析该表达式,并按照表达式执行动画。

4.2.3 设置导航栏按钮

navigationItems

Array

[]

包含按钮样式的数组

通过fireEvent完成按钮事件的回调。

按钮样式说明:

{     "type": "TEXT", // "TEXT", "IMG", "TEXT_IMG",必传      "title": "标题", // 文字标题或者      "image": "刷新", // 是图片地址,图片地址支持本地图片和网络图片           "textColor": "FFFFFF", // 16进制, 默认为白色,可不传           "backgroundColor": "", // 16进制, 默认透明色,可不传           "borderColor": "", // 16进制, 默认无边框,可不传           "borderWidth": 1, // 默认无边框,可不传           "cornerRadius": 1, // 默认无圆角,可不传           "font": 16, // 默认16号字体,可不传           "position": 0, // 默认0,可不传, 0-左侧显示,1-右侧显示               "imagePosition": 0, // 0-图片在左,文字在右,默认, 1- 图片在右,文字在左,  2-图片在上,文字在下, 3-图片在下,文字在上}

4.2.4 自定义导航栏

例如满足导航栏分段选择的需求:

navigationBarTitleComponent

String

无

对应的自定义Component的名称

Component由原生自行实现,并暴露API与Weex进行交互

4.3  阴影处理

Weex对于iOS的支持比较友好,然而Android 端无法显示阴影。 虽然文档有明确指出此问题,但是Android sdk却提供相关方法。也许是阿里的工程师尝试解决,但效果并不理想。比较明显的一点是,如果列表中的item使用阴影, 在列表滑动的时候会把阴影残留在最初绘制的位置。Android的同学一直在尝试解决此问题,最终也没达到一个理想的效果。最后的降级方案是通过图片来替代阴影,以下是Weex官方的注释:

目前仅 iOS 支持 box-shadow 属性,Android 暂不支持,可以使用图片代替。每个元素只支持设置一个阴影效果,不支持多个阴影同时作用于一个元素。

4.4  网络请求

Weex提供了Stream模块来完成网络请求,如果依赖于该模块,请求头、签名等配置以及请求结果校验都必须在Weex端完成,这对于一个全量Weex App而言无可厚非。但很多App的核心业务是使用Native完成,甚至会嵌套很多Web页面,我们必须将所有的请求统一至Native,让Weex更关心的是UI的呈现而非底层业务。

因此我们提供了自己的网络请求模块,Weex端调用Native提供的方法,并通过参数来决定请求,一些可选的参数:

参数

类型

必填

描述

path

String

是

请求的路径

method

String

否

请求的方式,默认为’GET‘,支持’POST‘、’DELETE‘等

params

Map

否

请求所需的参数

timeout

Number

否

请求超时时间

customHost

String

否

自定义请求的Host

callback

Function

是

请求的回调

请求示例:

// 1. 获取原生Moduleconst nativeStream = weex.requireModule('nativeStream')
// 2. 设置基本参数const options = { path:'/....',  method: 'POST',  params: {      id: '123'    }}    // 3. 发起请求  nativeStream.fetch(options, (res) => {    if (res.code === 0) {        succesCallback(res)            return      }      failCallback(res)})

4.5  图片加载

<image>标签图片的加载需要客户端提供handler,目前支持远程链接和打包生成的Bundle资源,并不直接支持相册图片以及拍照生成的图片。其对Base64的支持,为我们显示相册图片提供了一种思路。下图表明了Weex页面中选择相册图片、拍照并进行显示的流程:        加推Weex实践之路(上)

通过上图我们可以知道,一个简单的图片显示流程,其实并不简单,其中最为关键的是在进行第5步时所选择方案。先上传图片对于程序员而言是最为便捷的方案,但是比较影响用户体验,图片本应该在需要上传的时候进行上传,而非因为技术隘口而干扰业务。

转换为Base64可以提升用户体验,但是却比较影响性能。在iOS端,将一张1M的图片转换为Base64所需要的时间≥45ms,第6、7步所消耗的时间则是30ms左右,这种时间消耗随图片大小以倍数增加。

综上所述,我们设计了一种本地化方案,为每一个添加的图片生成一个唯一性的ID,Native负责图片的存储、加载。

加推Weex实践之路(上)

4.6  刷新组件

由于Weex所提供的<refresh>组件形式较单一,且存在交互Bug,我们自实现了一套刷新组件。通过属性来确定刷新的显示与否,并提供相应的接口实现Weex与Native之间的交互。

4.6.1 属性

属性

类型

必填

描述

showRefresh

Bool

否

是否添加下拉刷新

showLoading

Bool

否

是否添加上拉刷新

refreshAtCreated

Bool

否

是否在初次显示的时候,自动进行下拉刷新

4.6.2 事件

  • refresh 事件:当 <scroller>、<list>、<waterfall> 被下拉完成时触发,该事件由原生回调到Weex。

  • loading 事件:当 <scroller>、<list>、<waterfall> 被上拉时触发,该事件由原生回调到Weex。

<list ref="list"      :show-loading="loading"      :show-refresh="true"            class="list"            @refresh="refreshList"            @loading="loadMoreList"></list>
  • beginRefresh 事件:开始下拉刷新,由Weex调用。

  • beginLoading 事件:开始上拉刷新,由Weex调用。

  • endRefresh 事件:结束下拉刷新,由Weex调用。

  • endLoading 事件:结束上拉刷新,由Weex调用。

this.$refs.list.beginRefresh()this.$refs.list.endRefresh()

4.6.3 refreshAtCreated属性

如果不使用该属性,则需要在created或者mounted函数中手动调用刷新的方法以触发下拉刷新,然而在某些Android设备上面出现了白屏的情况。Weex对此做出了解释:

和浏览器不同的是,Weex 的渲染流程是异步的,而且渲染出来的结果都是原生系统中的 View,这些数据都无法被 javascript 直接获取到。因此在 Weex 上,Vue 的 mounted 生命周期在当前组件的 virtual-dom (Vue 里的 VNode) 构建完成后就会触发,此时相应的原生视图未必已经渲染完成。

4.7 截屏

Weex中完成截屏,只需要获取到对应的视图层即可,Weex页面在渲染时会为每一个组件生成一个唯一的ID,在JavaScript中更直观的体现是ref,尽管Weex并不存在真正的DOM,但其依然支持ref的使用。具体的做法如下:

// 1. 标签生命ref<div ref="poster"></div>
// 2. 获取到该element,实际上是一个Mapconst poster = this.$refs.poster
// 3. 获取Map中对应的refsaveViewShot({ ref: poster.ref })
// 4. 获取Component// iOSWXComponent *component = [weexInstance componentForRef:ref];// AndroidWXComponent component = WXSDKManager.getInstance().getWXRenderManager().getWXComponent(mWXSDKInstance.getInstanceId(), ref);
// 5. 获取View,进行截图

4.8 优雅的弹窗

这是一个很简单的弹框需求,视图渐渐变大最后全屏展示。然而基于Weex现有的能力是无法实现的,第一点:Weex页面默认的布局是从导航栏下面开始的,第二点:路由的跳转方式并不能直接支持此种弹出方式,页面默认是从右向左推出。

为了实现弹框的功能,我们需要四个步骤:

  1. Native定义PopView组件

  2. Weex搭建页面,并以PopView为基础进行布局

  3. 全屏呈现页面并隐藏导航栏

  4. 执行动画

原生定义好PopView组件后,Weex页面可以这样布局:

<template> <pop-view class="pop-view"> <text>测试弹框</text> </pop-view></template>

结合第三点所提出的配置文件,我们将2、3步骤的控制放在配置文件当中,最后写出的配置文件为:

{ navigationBarHidden: true, // 隐藏导航栏 transition: { property:'scale', // popView的尺寸将由(0,0)变为显示大小 duration: 2, // 动画时间,单位(秒) },// 转场显示,并规定popView的显示动画}

这只是最为简单的例子,更复杂的动画需要客户端支持即可。

5、To Be Continued

以上概括了Weex接入的心路历程,以及在实践中遇到的基本问题,表明了Weex在团队中的运用已经畅通并日趋规范化,但是更深入的性能优化、热更新等需要我们继续前行,以下是下一期文章将涉及的知识点:

  • 热更新

  • 资源预加载

  • 配置文件动态化

  • Weex资源打包自动化,自动加入终端仓库


作者介绍:

本文由赵一定及其团队的小伙伴一起撰写,非常感谢APP团队的倾力之作,使得我们有幸窥探加推的APP是如何实现的。

想要了解更多细节的朋友,可以进入一定的加推名片进行深入探讨: