在公司快速發展的大環境下,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在團隊中的運用已經暢通並日趨規範化,可是更深刻的性能優化、熱更新等須要咱們繼續前行,如下是下一期文章將涉及的知識點:
加推科學院公衆號