引言
隨着 Web 技術和移動設備的飛速發展,各種 APP 層出不窮,極速的業務擴展提高了團隊對開發效率的要求,這個時候使用 IOS/Andriod 開發一個 APP 似乎成本有點過高了,而 H5 的低成本、高效率、跨平台等特性馬上被利用起來形成了一種新的開發模式:Hybrid APP。
Hybrid 技術已經成為一種最主流最常見的方案。一套好的 Hybrid 架構解決方案能讓 App 既能擁有極致的體驗和性能,同時也能擁有 Web 技術 靈活的開發模式、跨平台能力以及熱更新機制。本文主要是結合我最近開發的一個 Hybrid 項目(https://github.com/Jack-cool/hybrid_jd),帶大家全面瞭解一下 Hybrid。
現有混合方案
深入瞭解 Hybrid 前,讓我們先來看一下目前市面上比較成熟的混合解決方案。
基於 WebView UI 的基礎方案
這種是市面上大多數 app 採取的方案,也是混合開發最基礎的方案。在 webview 的基礎上,與原生客户端建立js bridge橋接,以達到 js 調用Native API和 Native 執行js方法的目的。
目前國內絕大部分的大廠都有一套自己的基於 webview ui 的 hybrid 解決方案,例如微信的JS-SDK,支付寶的JSAPI等,通過JSBridge完成 h5 與 Native 的雙向通訊,從而賦予 H5 一定程度的原生能力。
基於 Native UI 的方案
可以簡單理解為“跨平台”,現在比較通用的有React Native,Weex,Flutter等。在賦予 H5 原生 API 能力的基礎上,進一步通過 JSBridge 將 JS 解析成的虛擬節點數(Virtual DOM)傳遞到 Native 並使用原生渲染。我們這裏來看下上面提到的這三種:
React Native
“Learn once, write anywhere”,React Native採用了 React 的設計模式,但 UI 渲染、動畫效果、網絡請求等均由原生端實現(由於 JS 是單線程,不大可能處理太多耗時的操作)。開發者編寫的 JS 代碼,通過 React Native 的中間層轉化為原生控件和操作,極大的提高了用户體驗。
React Native所有的標籤都不是真實控件,JS 代碼中所寫控件的作用,類似 Map 中的 key 值。JS 端通過這個 key 組合的 Dom ,最後 Native 端會解析這個 Dom ,得到對應的 Native 控件渲染,如 Android 中 標籤對應 ViewGroup 控件。
總結下來,就是:React Native 是利用 JS 來調用 Native 端的組件,從而實現相應的功能。
Weex
“Write once, run everywhere”,基於 Vue 設計模式,支持 web、android、ios 三端,原生端同樣通過中間層轉化,將控件和操作轉化為原生邏輯來提升用户體驗。
在 weex 中,主要包括三大部分:JS Bridge、Render、Dom,JS Bridge 主要用來和 JS 端實現進行雙向通信,比如把 JS 端的 dom 結構傳遞給 Dom 線程。Dom 主要是用於負責 dom 的解析、映射、添加等等的操作,最後通知 UI 線程更新。而 Render 負責在 UI 線程中對 dom 實現渲染。
和 react native 一樣,weex 所有的標籤也都不是真實控件,JS 代碼中所生成的 dom,最終都是由 Native 端解析,再得到對應的 Native 控件渲染,如 Android 中 標籤對應 WXTextView 控件。
Flutter
Flutter 是谷歌 2018 年發佈的跨平台移動 UI 框架。與 react native 和 weex 的通過 Javascript 開發不同,Flutter 的編程語言是Dart,所以執行時並不需要 Javascript 引擎,但實際效果最終也通過原生渲染。
看完這三種方案的簡介,下面讓我們簡單來做個對比吧:
| React Native | Weex | Flutter | |
|---|---|---|---|
| 平台實現 | JavaScript | JavaScript | 原生編碼 |
| 引擎 | JS V8 | JSCore | Flutter engine |
| 核心語言 | React | Vue | Dart |
| 框架程度 | 較重 | 較輕 | 重 |
| 特點 | 適合開發整體 App | 適合單頁面 | 適合開發整體 App |
| 支持 | Android、IOS | Android、IOS、Web | Android、IOS(可能還不止) |
| Apk 大小(Release) | 7.6M | 10.6M | 8.1M |
小程序
小程序開發本質上還是前端 HTML + CSS + JS 那一套邏輯,它基於 WebView 和微信(當然支付寶、百度、字節等現在都有自己的小程序,這裏只是拿微信小程序做個説明)自己定義的一套 JS/WXML/WXSS/JSON 來開發和渲染頁面。微信官方文檔裏提到,小程序運行在三端:iOS、Android 和用於調試的開發者工具,三端的腳本執行環境以及用於渲染非原生組件的環境是各不相同的。
通過更加定製化的 JSBridge,並使用雙 WebView 雙線程的模式隔離了 JS 邏輯與 UI 渲染,形成了特殊的開發模式,加強了 H5 與 Native 混合程度,提高了頁面性能及開發體驗。
PWA
Progressive Web App, 簡稱 PWA,是提升 Web App 體驗的一種新方法,能給用户帶來原生應用的體驗。
PWA 能做到原生應用的體驗不是靠某一項特定的技術,而是經過應用一系列新技術進行改進,在安全、性能和體驗三個方面都有了很大的提升,PWA 本質上還是 Web App,併兼具了 Native App 的一些特性和優點,主要包括下面三點:
- 可靠 - 即使在不穩定的網絡環境下,也能快速加載並展現
- 體驗 - 快速響應,並且有平滑的動畫響應用户的操作
- 粘性 - 設備上的原生應用,具有沉浸式的用户體驗,用户可以添加到桌面
Android 和主流的瀏覽器都早已支持了 PWA 標準,在 iOS 11.3 和 macOS 10.13.4 上,蘋果的 Safari 上也支持了 PWA。相信在不久的將來勢必會迎來 PWA 的大爆發...
看完目前主流的混合解決方案,我們迴歸本篇主題,講解一下成熟解決方案背後的 Hybrid底層基礎,要知道決定上層建築的永遠都是底層基礎,新的技術層出不窮,只有原理是不變的~~
Hybrid 是什麼,為什麼要用 Hybrid?
Hybrid,字面意思“混合”。可以簡單理解為是前端和客户端的混合開發。
讓我們先來看一下目前主流的移動應用開發方式:
Native APP
Native App 是一種基於智能手機本地操作系統如 iOS、Android、WP 並使用原生程式編寫運行的第三方應用程序,也叫本地 app。一般使用的開發語言為 Java、C++、Objective-C。。分別來看一下 Native 開發的優缺點:
-
優點
- 用户體驗近乎完美
- 性能穩定
- 訪問本地資源(通訊錄、相冊)
- 操作流暢
- 設計出色的動效、轉場
- 系統級的貼心通知或提醒
- 用户留存率高
-
缺點
- 門檻高,原生開發人才稀缺,至少比前端和後端少,開發環境昂貴
- 發佈成本高,需要通過 store 或 market 的審核,導致更新緩慢
- 維持多個版本、多個系統的成本比較高,而且必須做兼容
- 無法跨平台,開發的成本比較大,各個系統獨立開發
Web APP
Web App,顧名思義是指基於 Web 的應用,基本採用 Html5 語言寫出,不需要下載安裝。類似於現在所説的輕應用。基於瀏覽器運行的應用,基本上可以説是觸屏版的網頁應用。分別來看一下 Web 開發的優缺點:
-
優點
- 開發成本低
- 臨時入口,可以隨意嵌入
- 無需安裝,不會佔用手機內存,而且更新速度最快
- 能夠跨多個平台和終端
- 不存在多版本問題,維護成本低
-
缺點
- 無法獲取系統級別的通知,提醒,動效等等
- 設計受限制較多
- 體驗較差
- 受限於手機和瀏覽器性能,用户體驗相較於其他模式最差
- 用户留存率低
究其原因就是性能要求的問題。Web app 之所以能夠佔領開發市場,主要是因為它的開發速度快,使用簡單,應用範圍廣,但是在性能方面因為無法調用全部硬件底層功能,就現在講,還是比不過原生 App 的性能。
Hybrid APP
混合開發,也就是半原生半 Web 的開發模式,由原生提供統一的 API 給 JS 調用,實際的主要邏輯有 Html 和 JS 來完成,最終是放在 webview 中顯示的,所以只需要寫一套代碼即可達到跨平台效果。
Hybrid App 兼具了 Native APP 用户體驗佳、系統功能強大和 Web APP 跨平台、更新速度快的優勢。本質其實是在原生的 App 中,使用 WebView 作為容器直接承載 Web 頁面。因此,最核心的點就是 Native 端 與 H5 端 之間的雙向通訊層,也就是我們常説的 JSBridge。
下面讓我們來看下 JS 與 Native(客户端)通信的方式吧。
JS 與客户端通信
JS 通知客户端(Native)
JS上下文注入
原理其實就是 Native 獲取 JavaScript 環境上下文,並直接在上面掛載對象或者方法,使 JS 可以直接調用。
Android 與 IOS 分別擁有對應的掛載方式。分別對應是:蘋果UIWebview JavaScriptCore注入、安卓addJavascriptInterface注入、蘋果WKWebView scriptMessageHandler注入。
上面這三種方式都可以被稱為是JS上下文注入,他們都有一個共同的特點就是,不通過任何攔截的辦法,而是直接將一個 native 對象(or 函數)注入到 JS 裏面,可以由 Web 的 JS 代碼直接調用,直接操作。
彈窗攔截
這種方式主要是通過修改瀏覽器 Window 對象的某些方法,然後攔截固定規則的參數,之後分發給客户端對應的處理方法,從而實現通信。
常用的四個方法:
- alert: 可以被 webview 的 onJsAlert 監聽
- confirm: 可以被 webview 的 onJsConfirm 監聽
- prompt: 可以被 webview 的 onJsPrompt 監聽
簡單拿 prompt 來舉例説明,Web 頁面通過調用 prompt()方法,安卓客户端通過監聽onJsPrompt事件,攔截傳入的參數,如果參數符合一定協議規範,那麼就解析參數,扔給後續的 Java 去處理。這種協議規範,最好是跟 iOS 的協議規範一樣,這樣跨端調起協議是一致的,但具體實現不一樣而已。比如:jack://utils/${action}?a=a 這樣的協議,而其他格式的 prompt 參數,是不會監聽的,即除了 jack://utils/${action}?a=a 這樣的規範協議,prompt 還是原來的 prompt。
但這幾種方法在實際的使用中有利有弊,但由於prompt是幾個裏面唯一可以自定義返回值,可以做同步交互的,所以在目前的使用中,prompt是使用的最多的。
URL Schema
schema 是 URI 的一種格式,上文提到的jack://utils/${action}?a=a 就是一個 scheme 協議,這裏説的 scheme(或者 schema)泛指安卓和 iOS 的 schema 協議,因為它比較通用。
安卓和 iOS 都可以通過攔截跳轉頁 URL 請求,然後解析這個 scheme 協議,符合約定規則的就給到對應的 Native 方法去處理。
安卓和 iOS 分別用於攔截 URL 請求的方法是:
- android:
shouldOverrideUrlLoading方法 - iOS:UIWebView 的
delegate函數
這裏簡單看一個之前項目中對於 schema 封裝:
// 調用
window.fsInvoke.share({title: 'xxx', content: 'xxx'}, result => {
if (result.errno === 0) {
alert('分享成功')
} else {
// 分享失敗
alert(result.message)
}
)
---------------------------下方為對fsInvoke的封裝
(function(window, undefined) {
// 分享
invokeShare = (data, callback) => {
_invoke('share', data, callback)
}
// 登錄
invokeLogin = (data, callback) => {
_invoke('login', data, callback)
}
// 打開掃一掃
invokeScan = (data, callback) => {
_invoke('scan', data, callback)
}
_invoke = (action, data, callback) => {
// 拼接schema協議
let schema = `jack://utils/${action}?a=a`;
Object.keys(data).forEach(key => {
schema += `&${key}=${data[key]}`
})
// 處理callback
let callbackName = '';
if(typeof callback === 'string) {
callbackName = callback
} else {
callbackName = action + Date.now();
window[callbackName] = callback;
}
schema += `&callback=${callbackName}`
// 觸發
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = schema;
let body = document.body;
body.appendChild(iframe);
setTimeout(function() {
body.removeChild(iframe);
iframe = null;
})
}
// 暴露給全局
window.fsInvoke = {
share: invokeShare,
login: invokeLogin,
scan: invokeScan
}
})(window)
説完了 JS 主動通知客户端(Native)的方式,下面讓我們來看下客户端(Native)主動通知調用 JS。
客户端(Native)通知 JS
loadUrl
在安卓 4.4 以前是沒有 evaluatingJavaScript API 的,只能通過 loadUrl 來調用 JS 方法,只能讓某個 JS 方法執行,但是無法獲取該方法的返回值。這時我們需要使用前面提到的 prompt 方法進行兼容,讓 H5 端 通過 prompt 進行數據的發送,客户端進行攔截並獲取數據。
// mWebView = new WebView(this); //即當前webview對象
mWebView.loadUrl("javascript: 方法名('參數,需要轉為字符串')");
//ui線程中運行
runOnUiThread(new Runnable() {
@Override
public void run() {
mWebView.loadUrl("javascript: 方法名('參數,需要轉為字符串')");
Toast.makeText(Activity名.this, "調用方法...", Toast.LENGTH_SHORT).show();
}
});
evaluatingJavaScript
在安卓 4.4 之後,evaluatingJavaScript 是一個非常普遍的調用方式。通過 evaluateJavascript 異步調用 JS 方法,並且能在 onReceiveValue 中拿到返回值。
//異步執行JS代碼,並獲取返回值
mWebView.evaluateJavascript("javascript: 方法名('參數,需要轉為字符串')", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//這裏的value即為對應JS方法的返回值
}
});
stringByEvaluatingJavaScriptFromString
在 iOS 中 Native 通過stringByEvaluatingJavaScriptFromString調用 Html 綁定在 window 上的函數。
// Swift
webview.stringByEvaluatingJavaScriptFromString("方法名('參數')")
// oc
[webView stringByEvaluatingJavaScriptFromString:@"方法名(參數);"];
總結
看完本篇文章,相信你對 Hybrid 有了一個初步的瞭解。雖然本篇比較基礎,但是隻有了解了最本質的底層原理後,才能對現有的解決方案有一個很好的理解,你也可以去打造適合你和團隊的Hybrid方案。當然了,後面會有對於 Hybrid 更深入的探討,敬請期待哦!!
最後
你可以關注我的同名公眾號【前端森林】,這裏我會定期發一些大前端相關的前沿文章和日常開發過程中的實戰總結。當然,我也是開源社區的積極貢獻者,github地址https://github.com/Jack-cool,歡迎star!!!