前言
H5 VS Native 一直是前端技術界爭執不下的話題。react、vue等技術棧引領著純H5開發,rn、week則倡導原生體驗。但在專案實戰中,經常會選擇一箇中立的方案:混合開發。大眾稱呼:Hybrid。
本人目前從事新聞類產品研發,對於大家來講,就是熟知的如今日頭條、百度新聞、網易新聞等。在產品設計初期,考慮到一些實現難易程度問題(如新聞詳情頁,圖文混排,NA實現起來不如H5這樣自如),一些部分選擇了Hybrid方式開發,本篇就把開發過程中的一些想法分享一下,以供大家參考。
JSBridge解決的問題
混合開發,最重要的問題是:H5和Native的雙向通訊。 但現實中JS和NA的互動方法非常有限,下面會詳細說明。開發中如只是單純的方法呼叫,既無法確保呼叫成功率,也無法確保程式碼足夠簡潔。於是就有了JSBridge。JSBridge,是一種JS實現的Bridge,是一種思路,可以有不同理解,不同的程式碼實現。主旨思想是在H5和NA之間搭建一個橋樑(Bridge),給兩端留好更友好、更合理的介面。
H5和NA的雙向通訊通用方法
H5通訊方式和相容性如下表所示。指的是藉助Native的webview載入H5頁面,H5和NA之間通過API、URL攔截、全域性呼叫等形式,實現訊息通訊。站在大廠的角度考慮,在實戰的時候,會選擇更相容的方式。
H5呼叫NA方法梳理
平臺 | 方法 | 備註 |
---|---|---|
Android | shouldOverrideUrlLoading | scheme攔截方法 |
Android | addJavascriptInterface | API |
Android | onJsAlert()、onJsConfirm()、onJsPrompt() | |
IOS | 攔截URL | |
IOS(UIwebview) | JavaScriptCore | API方法,IOS7+ 支援 |
IOS(WKwebview) | window.webkit.messageHandlers | APi方法,IOS8+支援 |
NA呼叫H5方法梳理
平臺 | 方法 | 備註 |
---|---|---|
Android | loadurl() | |
Android | evaluateJavascript() | Android 4.4 + |
IOS(UIwebview) | stringByEvaluatingJavaScriptFromString | |
IOS(UIwebview) | JavaScriptCore | IOS7.0+ |
IOS(Wkwebview) | evaluateJavaScript:javaScriptString | iOS8.0+ |
通過上面兩端呼叫方法梳理表,不難分析出,URL攔截 & 執行JS是 安卓和IOS比較通用且相容性較好的方案。我們混合開發的基礎正是基於這種方法來實現的。
常規混合開發思路
H5和NA通訊方面,最簡單直接的思路是:NA攔截H5的URL獲取訊息(一般是通過修改iframe的src來實現 ①),經過業務處理,NA執行JS(在H5側提前註冊好的全域性方法③)回撥通知H5(如下圖)。
H5程式碼實現如下:
<html>
...
<body>
<div class="content">XXXXX</div>
</body>
<script>
// ① 註冊全域性函式,以便端呼叫
window.setAllContent = function(){
}
// ② 通用方法函式
var sendschema = function(action,param){
let tempnode = document.createElement('iframe');
tempnode.src = "bdnews://"+action+param;
}
// ③ H5邏輯開始 執行函式
document.addEventListener("DOMContentLoaded",function(){
sendschema('load_finish');
},false);
</script>
...
</html>
複製程式碼
Android原理大致如下:
webView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 場景一: 攔截請求、接收schema
if (url.equals("load_url")) {
// 處理邏輯
dosomething
// 回掉
view.loadUrl("javascript:setAllContent(" + json + ");")
}
// 場景二:端自己呼叫H5,沒有請求發起
clickbutton(){
view.loadUrl("javascript:setAllContent(" + json + ");")
}
}
});
複製程式碼
IOS大概邏輯如下:
// 初始化webview
UIWebView * view = [[UIWebView alloc]initWithFrame:self.view.frame];
[view loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.xx.com"]]];
[self.view addSubview:view];
/*
webView協議中的方法
shouldStartLoadWithRequest //準備載入內容時呼叫的方法,通過返回值來進行是否載入的設定
webViewDidStartLoad //開始載入時呼叫的方法
webViewDidFinishLoad //結束載入時呼叫的方法
didFailLoadWithError //載入失敗時呼叫的方法
*/
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if ([urlString hasPrefix:@"scheme://hybrid?info="]) {
if([name isEqualToString:@"load_finish"]){
// [self.webView setContent];
[self.webView stringByEvaluatingJavaScriptFromString:strFormat];
}
}
}
- clickbutton(){
[self.webView setContent];
}
複製程式碼
但這樣開發存在一些痛點:
1)回撥函式不明確。可以說目前沒有回撥函式的機制,這導致一些依賴於回撥函式的分析及判斷無法正常使用,如:功能呼叫方、呼叫是否成功、呼叫失敗異常處理等這些CASE;
2)對應關係不明確。有一些呼叫看起來像是回撥,但沒有把他們放到一起,導致程式碼散亂,難以維護。如上面demo:sendschema('load_finish') 和 setAllContent 本來含義是 告訴NA頁面準備好了,NA收到後,向頁面塞資料。本來緊密相關的一對功能,拆分開看不出有什麼聯絡;
3)全域性函式冗雜。理想中如果呼叫和回撥成對出現,DEMO中註冊及維護全域性函式的工作就會減少很多。提升頁面可讀性和維護成本。如 load_finish 和 setAllContent,只保留 load_finish 即可;
4)端內程式碼冗雜。端內註冊了與H5約定的呼叫方法,很顯然也需要維護一套程式碼標識什麼時候呼叫。
以上開發中遇到的問題,也許剛開始功能不多的時候還察覺不出問題,但是隨著功能增加,後期維護成本很大。
JSB方案設計
在H5和NA之間增加一箇中間層,這層封裝了H5和NA通訊的互動方式。H5和NA互不關心對方的樣子,通過中間層暴露的方法進行功能呼叫即可。
JSB互動模型
H5跟NA互動,從H5角度來看大致可分為兩大類:有去無回&有去有回、無去有回。
第一類互動模型
請求邏輯:有去無回、有去有回。這裡有兩種實現方案(初步思路稿如下):
① 函式名關聯
let BDAPPnode = {
callbacks: {},
// 呼叫函式註冊
invoke(action, params, successfnname, successfn) {
this.callbacks[successfnname] = {
success: successfn
};
sendschema(action, params);
},
// NA呼叫
callbackSuccess(callbackname, params) {
try {
BDAPPnode.callbackFromNative(callbackname, params, true);
} catch (e) {
console.log('Error in error callback: ' + callbackname + ' = ' + e);
}
},
callbackFromNative(callbackname, params, isSuccess) {
let callback = this.callbacks[callbackname];
if (callback) {
if (isSuccess) {
callback.success && callback.success(params);
}
};
}
};
複製程式碼
② ID 關聯
let BDAPPnode = {
callbackId: Math.floor(Math.random() * 2000000000),
callbacks: {},
invoke(action, params, onSuccess, onFail) {
this.callbackId++;
this.callbacks[self.callbackId] = {
success: onSuccess,
fail: onFail
};
sendschema(action, params, this.callbackId);
},
callbackSuccess(callbackId, params) {
try {
BDAPPnode.callbackFromNative(callbackId, params, true);
} catch (e) {
console.log('Error in error callback: ' + callbackId + ' = ' + e);
}
},
callbackError(callbackId, params) {
try {
BDAPPnode.callbackFromNative(callbackId, params, false);
} catch (e) {
console.log('Error in error callback: ' + callbackId + ' = ' + e);
}
},
callbackFromNative(callbackId, params, isSuccess) {
let callback = this.callbacks[callbackId];
if (callback) {
if (isSuccess) {
callback.success && callback.success(callbackId, params);
} else {
callback.fail && callback.fail(callbackId, params);
}
delete BDAPPnode.callbacks[callbackId];
};
}
};
複製程式碼
在發出請求的時候,註冊回撥方法。這麼做有兩個目的:
-
無需提前註冊所有全域性回掉函式,減少不必要的初始化,進而減少白屏時間;
-
不用額外起回掉函式的名稱,發起請求的時候傳入一個隨機ID,同時註冊此ID的回掉函式。NA通過統一封裝好的回掉函式呼叫,回撥ID和引數,進而達到執行回撥邏輯。
具體選用那個,還得根據具體情況具體分析看。
第二類互動模型看
請求邏輯:無去有回,沒有發出請求,NA主動呼叫。此類還需註冊全域性變數,等待NA呼叫。跟非JSBridge的實現是一個道理
window.fn1 = () =>{
// do fn1
}
window.fn2 = () =>{
// do fn2
}
複製程式碼
方案選擇
實戰過程中深刻體會到,混合開發可以分為兩大類:NA服務H5,H5服務NA。
前者H5為主,大多數互動是H5發起NA請求,等待NA回撥,可稱之為:『一對一請求』,如:H5請求獲取地理位置,NA做完後返回N\S座標;
後者主要是為了解決NA成本實現高的問題,多為NA主動呼叫H5提前註冊好的方法,可稱之為:『單獨請求』,確保功能順利實現。
在專案實戰過程中,經常會有這種情況:回撥函式既是一對一請求,也是單獨呼叫,如:評論功能,可以頁面點選彈出NA輸入框傳送,也可以點選底BAR上NA實現的按鈕彈框傳送。對於頁面來講都需要更新。站在H5角度希望NA區分,H5頁面呼叫的評論成功和NA呼叫的評論成功進行區分,這樣就可以把模型一和模型二區分開獨立實現(同時也可以區分頁面重新整理的來源)。但站在NA角度來講,不關心誰吊起的,只要評論成功,就應該去呼叫更新頁面的H5方法。不然NA需要從呼叫開始就攜帶引數,一路到底。跟端溝通後,雙方都妥協了一步,簡單功能的進行了來源區分模型一實現,較為複雜的模型二實現。
API封裝
API層處於JSBridge底層和業務,有些人也把它當做JSBridge的一部分,為了更好理解,我將它單獨抽離出來。此處主要封裝業務層呼叫,如下面程式碼。
此處多說一句:平日開發要有封裝和抽離的思想,一方面減少重複程式碼,一方面不斷抽離將程式碼分層,沒一層可以做一些封裝和擴充套件,可以提高程式碼複用性。
JSB注入時機
NA注入
我們肯定是期望JSB注入越早越好,這樣不論在前端頁面中任何位置都可以隨時呼叫,NA注入JS的方法和時機都比較侷限。如下表:
平臺 | 方法 | 時機 |
---|---|---|
IOS[UI] | [self.webView stringByEvaluatingJavaScriptFromString:injectjs] | webViewDidFinishLoad(會有時機問題) |
IOS[wk] | evaluateJavaScript:xxxx | didCreateJavaScriptContext |
Android | webView.loadUrl("javascript:" + injectjs);) | OnPageFinished |
網頁描述頁面狀態的值有以下方法,根據相容性及實現完整性,一般用DOMContentLoaded,IE9以下用readystatechange來判斷頁面是否載入成功。
名稱 | 父物件 | 描述 | 相容性 |
---|---|---|---|
DOMContentLoaded | doc | 頁面內容OK | IE9+ |
onload | win | 頁面所有隻要載入完成 | |
readystatechange | doc | 頁面載入狀態:uninitialized(為初始化):物件存在但尚未初始化。loading(正在載入):物件正在載入資料。loaded(載入完畢):物件載入資料完成。interactive(互動):可以操作物件了,但還沒有完全載入。complete(完成):物件已經載入完畢 | IE9&IE10有實現bug |
IOS的uiwebview提供了代理WebViewDidFinishLoad,WebViewDidFinishLoad 被呼叫時,readyState 可能處在 interactive 和 complete 兩種狀態,所以初始化頁面直接呼叫會有問題。對於這個問題從NA角度可以實現一個NSObject的擴充套件,並實現webView:didCreateJavaScriptContext:forFrame。從H5角度可以檢測頁面狀態,在complete之後再呼叫native。
IOS的didCreateJavaScriptContext和Android的OnPageFinished(the page has finished loading)均是在網頁onload之前完成,所以這兩個時機沒有呼叫順序的問題。
優點:
1)註冊早,即使在頁面初始化就呼叫端能力,也可以滿足
缺點:
由於我們選擇的是uiwebview如果按照上面的考慮,這樣做有幾點不足之處 1)監聽實現成本高 2)需要NA注入,NA對於JS不熟悉,JS往往也不清楚NA邏輯,後面維護成本不可控制。
如果時間不充裕的情況下,除了NA注入,還有別的辦法嘛?
JS注入
其實JS也可以在頁面一開始就注入。比如在head裡直接應用抽離出來的Jsbridge程式碼,本次8.0我們採用了這種降級方案,短時間內完成了架構搭建。
優點:
這樣減小了維護成本,功能完整,提高了呼叫成功的機率。
缺點:
增加了頁面載入解析時間會影響白屏時間。
總結
Hybrid是一種連線H5跟NA的思路,即可以快速迭代H5功能,又可以有NA的體驗,是混合開發的典型開發模式。實踐過程中需要根據業務形態模型來定製程式碼實現,注入時機也不是一成不變的可以根據業務形態來選擇。