加推Weex实践之路(上)

1、背景

1.为何是Weex

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

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

其二,选择WeexReact NativeFlutterChameleon等跨平台开发框架,主流框架对好比下:前端

对比内容vue

React Nativejava

Flutterreact

Weexgit

上手难度程序员

通常github

通常web

容易

接入特色

适合开发总体App

适合开发总体App

适合单页面

维护难度

通常

通常

容易

开发语言

React

Dart

Vue、Rax

框架体量

较重

较轻

Bundle大小

较大

不须要

较小

社区

丰富

新起之秀

不够完善

支持终端

Android、iOS

Android、iOS、Web等

Android、iOS、Web

引擎

JSCore、V8

Flutter Engine

JSCore、V8

经过对比,最终选择了Weex,有如下几个主要缘由:

  1. Weex的上手成本较低,且单页面的支持更符合项目规划;
  1. Vue框架,契合团队的大前端环境;
  1. Weex承接了淘宝、飞猪等App的大量页面,给予外界充足的信心。

2.Weex与Web

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

2、Weex基本原理

Weex支持 VueRax两个前端框架,因为前端团队使用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等跨平台技术。

4、实践与解决方案

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

  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中发起一次页面跳转的示例:

navigator.openUrl({
  url: 'scheme://weex/open',
  params: {
    bundleUrl: '/dist/about.js',
    name: '这是下一个页面所需的参数',
  },
})


// 备注: 而下一个页面只须要在data中声明一个与参数同名的属性来接收具体的参数便可。复制代码

  1. 反向传值

咱们在考虑反向传值的问题时,主要涉及两种场景,一是Weex页面反向传值给Weex页面,二是Native反向传值给Weex页面。咱们可使用Weex提供的基于W3C 规范

来轻松知足第一种场景。可是目前并无现成的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,
  },
})复制代码

2. 页面配置文件

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

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

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

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

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

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

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

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

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

1.部分属性

属性

类型

默认值

描述

backgroundColor

HexColor

同项目配置

窗口背景色

navigationBarBackgroundColor

HexColor

同项目配置

导航栏背景色

navigationBarHidden

Bool

false

隐藏导航栏

navigationBarTitle

String



导航栏标题

navigationBarTitleColor

HexColor

同项目配置

导航栏标题颜色

2.iOS特殊形式

针对于iOS客户端存在页面须要Present等状况,能够设置如下属性:

present

Bool

false

Present页面

presentWithNavigationBar

Bool

false

Present页面,并携带导航栏

transition

Map

false

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

优先级:transition > presentWithNavigationBar > present

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

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-图片在上,文字在下,复制代码

4.自定义导航栏

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

navigationBarTitleComponent

String

对应的自定义Component的名称

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

3. 阴影处理

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

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

4. 网络请求

Weex提供了

模块来完成网络请求,若是依赖于该模块,请求头、签名等配置以及请求结果校验都必须在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. 获取原生Module
const 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)
}复制代码

5. 图片加载

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

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

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

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

6. 刷新组件

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

1.属性

属性

类型

必填

描述

showRefresh

Bool

是否添加下拉刷新

showLoading

Bool

是否添加上拉刷新

refreshAtCreated

Bool

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

2.事件

  • refresh 事件:当 <scroller>、<list>、<waterfall> 被下拉完成时触发,该事件由原生回调到Weex。
  • loading 事件:当 <scroller>、<list>、<waterfall> 被上拉时触发,该事件由原生回调到Weex。
<list ref="list"
      c
      :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()复制代码

3.refreshAtCreated属性

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

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

7. 截屏

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

// 1. 标签生命ref
<div ref="poster"></div>


// 2. 获取到该element,其实是一个Map
const poster = this.$refs.poster


// 3. 获取Map中对应的ref
saveViewShot({ ref: poster.ref })


// 4. 获取Component
// iOS
WXComponent *component = [weexInstance componentForRef:ref];
// Android
WXComponent component = WXSDKManager.getInstance().getWXRenderManager().getWXComponent(mWXSDKInstance.getInstanceId(), ref);


// 5. 获取View,进行复制代码

8. 优雅的弹窗

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

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

1. Native定义PopView组件

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

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

3. 执行动画

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

<template>
  <pop-view class="pop-view">
    <text>测试弹框</text>
  </pop-view>
</template>复制代码

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

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

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

5、To Be Continued

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

  • 热更新
  • 资源预加载
  • 配置文件动态化
  • Weex资源打包自动化,自动加入终端仓库

加推科学院公众号

mp.weixin.qq.com/s/LxdQ6Eq2R…

相关文章
相关标签/搜索