H5 VS Native 一直是前端技術界爭執不下的話題。react、vue等技術棧引領着純H5開發,rn、week則倡導原生體驗。但在項目實戰中,常常會選擇一箇中立的方案:混合開發。大衆稱呼:Hybrid。javascript
本人目前從事新聞類產品研發,對於你們來說,就是熟知的現在日頭條、百度新聞、網易新聞等。在產品設計初期,考慮到一些實現難易程度問題(如新聞詳情頁,圖文混排,NA實現起來不如H5這樣自如),一些部分選擇了Hybrid方式開發,本篇就把開發過程當中的一些想法分享一下,以供你們參考。html
混合開發,最重要的問題是:H5和Native的雙向通訊。 但現實中JS和NA的交互方法很是有限,下面會詳細說明。開發中如只是單純的方法調用,既沒法確保調用成功率,也沒法確保代碼足夠簡潔。因而就有了JSBridge。JSBridge,是一種JS實現的Bridge,是一種思路,能夠有不一樣理解,不一樣的代碼實現。主旨思想是在H5和NA之間搭建一個橋樑(Bridge),給兩端留好更友好、更合理的接口。前端
H5通訊方式和兼容性以下表所示。指的是藉助Native的webview加載H5頁面,H5和NA之間經過API、URL攔截、全局調用等形式,實現消息通訊。站在大廠的角度考慮,在實戰的時候,會選擇更兼容的方式。vue
平臺 | 方法 | 備註 |
---|---|---|
Android | shouldOverrideUrlLoading | scheme攔截方法 |
Android | addJavascriptInterface | API |
Android | onJsAlert()、onJsConfirm()、onJsPrompt() | |
IOS | 攔截URL | |
IOS(UIwebview) | JavaScriptCore | API方法,IOS7+ 支持 |
IOS(WKwebview) | window.webkit.messageHandlers | APi方法,IOS8+支持 |
平臺 | 方法 | 備註 |
---|---|---|
Android | loadurl() | |
Android | evaluateJavascript() | Android 4.4 + |
IOS(UIwebview) | stringByEvaluatingJavaScriptFromString | |
IOS(UIwebview) | JavaScriptCore | IOS7.0+ |
IOS(Wkwebview) | evaluateJavaScript:javaScriptString | iOS8.0+ |
經過上面兩端調用方法梳理表,不難分析出,URL攔截 & 執行JS是 安卓和IOS比較通用且兼容性較好的方案。咱們混合開發的基礎正是基於這種方法來實現的。java
H5和NA通訊方面,最簡單直接的思路是:NA攔截H5的URL獲取消息(通常是經過修改iframe的src來實現 ①),通過業務處理,NA執行JS(在H5側提早註冊好的全局方法③)回調通知H5(以下圖)。node
H5代碼實現以下:react
<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原理大體以下:git
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大概邏輯以下:github
// 初始化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];
}
複製代碼
但這樣開發存在一些痛點:web
1)回調函數不明確。能夠說目前沒有回調函數的機制,這致使一些依賴於回調函數的分析及判斷沒法正常使用,如:功能調用方、調用是否成功、調用失敗異常處理等這些CASE;
2)對應關係不明確。有一些調用看起來像是回調,但沒有把他們放到一塊兒,致使代碼散亂,難以維護。如上面demo:sendschema('load_finish') 和 setAllContent 原本含義是 告訴NA頁面準備好了,NA收到後,向頁面塞數據。原本緊密相關的一對功能,拆分開看不出有什麼聯繫;
3)全局函數冗雜。理想中若是調用和回調成對出現,DEMO中註冊及維護全局函數的工做就會減小不少。提高頁面可讀性和維護成本。如 load_finish 和 setAllContent,只保留 load_finish 便可;
4)端內代碼冗雜。端內註冊了與H5約定的調用方法,很顯然也須要維護一套代碼標識何時調用。
以上開發中遇到的問題,也許剛開始功能很少的時候還察覺不出問題,可是隨着功能增長,後期維護成本很大。
在H5和NA之間增長一箇中間層,這層封裝了H5和NA通訊的交互方式。H5和NA互不關心對方的樣子,經過中間層暴露的方法進行功能調用便可。
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層處於JSBridge底層和業務,有些人也把它當作JSBridge的一部分,爲了更好理解,我將它單獨抽離出來。此處主要封裝業務層調用,以下面代碼。
此處多說一句:平日開發要有封裝和抽離的思想,一方面減小重複代碼,一方面不斷抽離將代碼分層,沒一層能夠作一些封裝和擴展,能夠提升代碼複用性。
咱們確定是指望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也能夠在頁面一開始就注入。好比在head裏直接應用抽離出來的Jsbridge代碼,本次8.0咱們採用了這種降級方案,短期內完成了架構搭建。
優勢:
這樣減少了維護成本,功能完整,提升了調用成功的概率。
缺點:
增長了頁面加載解析時間會影響白屏時間。
Hybrid是一種鏈接H5跟NA的思路,便可以快速迭代H5功能,又能夠有NA的體驗,是混合開發的典型開發模式。實踐過程當中須要根據業務形態模型來定製代碼實現,注入時機也不是一成不變的能夠根據業務形態來選擇。