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是如何实现的。

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