在公司快速发展的大环境下,App的更新迭代高速、高频,技术团队平均两周即可诞生一款中型App,但App团队只有6我的(iOS 、Android各3人),在确保效率、质量的前提下,单纯依靠Native的能力显得步履蹒跚——咱们亟需提高团队效率,但愿单人可完成本来2~3人的工做量。javascript
其一,接入Web页面,一个页面适配两端;html
其二,选择Weex、React Native、Flutter、Chameleon等跨平台开发框架,主流框架对好比下:前端
对比内容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,有如下几个主要缘由:
虽然Web页面的上手、维护成本更低,但与Weex相比,同等页面的Web包体积要比Weex大,Web页面没法作到纯Native的体验,且页面加载速度很难极致化,在某些设备上容易出现白屏。Weex所具有的下列优点,足以让一个追求极致的团队所青睐。
Weex支持 Vue 和 Rax两个前端框架,因为前端团队使用Vue进行平常开发,为了下降上手成本,咱们选择Vue框架进行Weex开发。Weex的基本工做流程以下:
Weex we 文件 --------------前端(we源码)
↓ (转换) ------------------前端(构建过程)
JS Bundle -----------------前端(JS Bundle代码)
↓ (部署) ------------------服务器或本地
在服务器或本地上的JS bundle ----服务器或本地
↓ (编译) ------------------ 客户端(JS引擎)
虚拟 DOM 树 --------------- 客户端(Weex JS Framework)
↓ (渲染) ------------------ 客户端(渲染引擎)
Native视图 --------------- 客户端(渲染引擎)
除去前端页面的编写,Weex可划分为
依托于Native,借助于前端,演变成大前端的架构,总体结构以下图。咱们但愿App做为终端,提供容器的能力,作好底层服务,完美融合Weex、Web等跨平台技术。
如下记录了实践中遇到的一些较为重要、常见的问题,及其解决方案,它们贯穿了Weex页面的生命周期。
Weex自身提供的navigator只支持简单的在线资源,而不支持本地文件的加载,同时没法知足动态化的呈现方式和复杂参数传递,须要自行实现一套完整的页面跳转规则。
在大量使用了
// 打开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中声明一个与参数同名的属性来接收具体的参数便可。复制代码
咱们在考虑反向传值的问题时,主要涉及两种场景,一是Weex页面反向传值给Weex页面,二是Native反向传值给Weex页面。咱们可使用Weex提供的基于W3C 规范的
来轻松知足第一种场景。可是目前并无现成的API能知足第二种场景,咱们须要不断的尝试:咱们最后选择使用
1.Weex添加事件监听
const globalEvent = weex.requireModule('globalEvent');
globalEvent.addEventListener("eventName", (e) => {
// ...
});
2.原生页面发送事件
weexInstance.fireGlobalEventCallback("eventName", params); // Android
[weexInstance fireGlobalEvent:seventName params:params]; // iOS复制代码
代码中的weexInstance,能够理解为一个页面的实例对象,该流程须要在发送监听的页面须要获取上一个
navigator.openUrl({
url: 'scheme://weex/open',
params: {
bundleUrl: '/dist/about.js',
needListen: true,
},
})复制代码
起初Weex页面导航栏、跳转方式等配置,均在自建Module中进行处理,例如一个页面要设置导航栏,咱们须要在mounted方法中加入以下代码:
created () {
navigator.setTitle('导航栏标题')
navigator.setItems([{
title: '按钮',
}])
}复制代码
这种不够工程化、标准化的方式给后期的维护、跨项目移植、架构升级形成了极大的干扰。
咱们参考小程序的设计思路进行了优化升级,为每个须要特有化配置的Weex页面添加一个json格式的配置文件,配置文件包括导航栏的配置、页面级别的配置、跳转的配置等,将配置工程化、标准化。
例如我为about页面添加了配置文件,json文件的类容为:
{
"navigationBarTitle": "导航栏标题",
"navigationItems": [{
"title": "按钮"
}]
}复制代码
打包后的资源文件目录以下,about.js、about.json文件在同级目录:
那么一个完整的页面打开步骤以下:
通过扩展,配置文件变得更加丰富,先前麻烦的跳转处理、弹框等均可以经过配置文件实现,下面是一些经常使用的属性介绍:
属性 |
类型 |
默认值 |
描述 |
backgroundColor |
HexColor |
同项目配置 |
窗口背景色 |
navigationBarBackgroundColor |
HexColor |
同项目配置 |
导航栏背景色 |
navigationBarHidden |
Bool |
false |
隐藏导航栏 |
navigationBarTitle |
String |
|
导航栏标题 |
navigationBarTitleColor |
HexColor |
同项目配置 |
导航栏标题颜色 |
针对于iOS客户端存在页面须要Present等状况,能够设置如下属性:
present |
Bool |
false |
Present页面 |
presentWithNavigationBar |
Bool |
false |
Present页面,并携带导航栏 |
transition |
Map |
false |
以转场的形式呈现,并规定转场的动画表达式(默认背景alpha从0到1) |
优先级:transition > presentWithNavigationBar > present |
transition中需定义动画的表达式,Native则须要解析该表达式,并按照表达式执行动画。
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-图片在上,文字在下,复制代码
例如知足导航栏分段选择的需求:
navigationBarTitleComponent |
String |
无 |
对应的自定义Component的名称 |
Component由原生自行实现,并暴露API与Weex进行交互 |
Weex对于iOS的支持比较友好,然而Android 端没法显示阴影。 虽然文档有明确指出此问题,可是Android sdk却提供相关方法。也许是阿里的工程师尝试解决,但效果并不理想。比较明显的一点是,若是列表中的item使用阴影, 在列表滑动的时候会把阴影残留在最初绘制的位置。Android的同窗一直在尝试解决此问题,最终也没达到一个理想的效果。最后的降级方案是经过图片来替代阴影,如下是Weex官方的注释:
目前仅 iOS 支持 box-shadow 属性,Android 暂不支持,可使用图片代替。
每一个元素只支持设置一个阴影效果,不支持多个阴影同时做用于一个元素。
所以咱们提供了本身的网络请求模块,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)
}复制代码
<image>标签图片的加载须要客户端提供handler,目前支持远程连接和打包生成的Bundle资源,并不直接支持相册图片以及拍照生成的图片。其对Base64的支持,为咱们显示相册图片提供了一种思路。下图代表了Weex页面中选择相册图片、拍照并进行显示的流程:
经过上图咱们能够知道,一个简单的图片显示流程,其实并不简单,其中最为关键的是在进行第5步时所选择方案。先上传图片对于程序员而言是最为便捷的方案,可是比较影响用户体验,图片本应该在须要上传的时候进行上传,而非由于技术隘口而干扰业务。
转换为Base64能够提高用户体验,可是却比较影响性能。在iOS端,将一张1M的图片转换为Base64所须要的时间≥45ms,第六、7步所消耗的时间则是30ms左右,这种时间消耗随图片大小以倍数增长。
综上所述,咱们设计了一种本地化方案,为每个添加的图片生成一个惟一性的ID,Native负责图片的存储、加载。
因为Weex所提供的<refresh>组件形式较单一,且存在交互Bug,咱们自实现了一套刷新组件。经过属性来肯定刷新的显示与否,并提供相应的接口实现Weex与Native之间的交互。
属性 |
类型 |
必填 |
描述 |
showRefresh |
Bool |
否 |
是否添加下拉刷新 |
showLoading |
Bool |
否 |
是否添加上拉刷新 |
refreshAtCreated |
Bool |
否 |
是否在初次显示的时候,自动进行下拉刷新 |
<list ref="list"
c
:show-refresh="true"
class="list"
@refresh="refreshList"
@loading="loadMoreList">
</list>复制代码
this.$refs.list.beginRefresh()
this.$refs.list.endRefresh()复制代码
若是不使用该属性,则须要在created或者mounted函数中手动调用刷新的方法以触发下拉刷新,然而在某些Android设备上面出现了白屏的状况。Weex对此作出了解释:
和浏览器不一样的是,Weex 的渲染流程是异步的,并且渲染出来的结果都是原生系统中的 View,这些数据都没法被 javascript 直接获取到。所以在 Weex 上,Vue 的 mounted 生命周期在当前组件的 virtual-dom (Vue 里的 VNode) 构建完成后就会触发,此时相应的原生视图未必已经渲染完成。
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,进行复制代码
这是一个很简单的弹框需求,视图渐渐变大最后全屏展现。然而基于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的显示动画
}复制代码
这只是最为简单的例子,更复杂的动画须要客户端支持便可。
以上归纳了Weex接入的心路历程,以及在实践中遇到的基本问题,代表了Weex在团队中的运用已经畅通并日趋规范化,可是更深刻的性能优化、热更新等须要咱们继续前行,如下是下一期文章将涉及的知识点:
加推科学院公众号