Hybrid開發(fā)定義和使用范圍
為什么要采用hybrid:
現(xiàn)階段的應(yīng)用開發(fā),會遇到如下問題和挑戰(zhàn):
1 一些頁面或業(yè)務(wù),和運營強相關(guān),無法native固定(例如電子商務(wù) 詳情展示)
2 客戶端發(fā)版周期長,一些需求想要很快上線,或變化非常頻繁
現(xiàn)有3類主流APP,分別為:Web App、Hybrid App(混合模式應(yīng)用,Hybrid有“混合的”意思)、 Native App;
Native App 和 Web App不作解釋了,主要解釋Hybrid App。
Hybrid App按網(wǎng)頁語言與程序語言的混合,通常分為三種類型:多View混合型,單View混合型,Web主體型。
單頁混合型
即在同一個頁面內(nèi),同時包括Native View和Web View?;ハ嘀g是覆蓋(層疊)的關(guān)系。這種Hybrid App的開發(fā)成本較高,開發(fā)難度較大,但是體驗較好。如百度搜索為代表的單頁混合型移動應(yīng)用,既可以實現(xiàn)充分的靈活性,又能實現(xiàn)較好的用戶體驗。一般如無特殊需求,不會采用此種方式。
Web主體型
這種常見于市面上第三方hybrid框架實現(xiàn)。例如Wex5,AppCan和Rexsee都屬于Web主體型移動應(yīng)用中間件。基本可以實現(xiàn)跨平臺,主要以網(wǎng)頁語言編寫,利用框架生成native的殼子。但是一般用戶體驗存在缺陷。常見于一些小型或功能單一app。
多主體混合型
即Native View和Web View獨立展示,交替出現(xiàn)。這種應(yīng)用混合邏輯相對簡單。這種移動應(yīng)用主體通常是Native App,Web技術(shù)只是起到補充作用。開發(fā)難度和Native App基本相當(dāng)。常見的Hybrid App是Native View與WebView交替的場景出現(xiàn)。
與App內(nèi)接入H5的區(qū)別:
hybrid的開發(fā)模式與我們之前一些運營頁面采用h5的根本區(qū)別在于,后者只是在一些不重要的功能上實現(xiàn)可運營和便于分享,并不接入到應(yīng)用的主要流程中,與native的交互較少,對應(yīng)用的影響小,作為開發(fā)的一個小模塊獨立存在。hybrid開發(fā)則是將web頁面作為native的重要補充,應(yīng)用功能的重要組成部分,需要考慮上線節(jié)奏,web與native的通訊,優(yōu)化web體驗等問題,對于應(yīng)用來講,web與native的地位,被大大拉平了。
如何區(qū)分Hybrid APP中的原生頁面和H5頁面
很多人從頁面的設(shè)計上來區(qū)分的。如:(1)頂部顯示網(wǎng)頁鏈接;(2)有加載的進度條;(3)沒有底部tab導(dǎo)航欄;(4)頂部顯示兩個導(dǎo)航條;
但是現(xiàn)在app的h5頁面做的可以以假亂真了,這些統(tǒng)統(tǒng)不管用。
以淘寶為例:
設(shè)置-開發(fā)者選項-顯示布局邊界
H5中使用了webview控件,其作為一個控件,只有一個邊界框,所以通過這一點,就比較容易區(qū)分出一個界面是webview實現(xiàn)的還是原生布局控件實現(xiàn)的
這次再來看看:
幾個主流HybridApp:淘寶、京東、大眾點評等
Hybrid中Native和H5的使用范圍:
Native
1 應(yīng)用核心邏輯:例如 下單、支付等
2 對手機native功能(如照相、定位)重度依賴的頁面
3 用戶體驗要求強,運營要求弱的頁面
H5:
1.功能開發(fā)不完善,試運營階段(試錯成本低)
2.強運營需求,在功能調(diào)整或內(nèi)容的運營上很靈活
3.階段性的營銷活動,希望被分享出去
Hybrid開發(fā)中要解決的幾個問題
一、H5 和 Native 上線時間不一致,如何銜接?
二、H5 和 Native 之間如何進行通信?
三、H5 頁面如何接近 Native 的體驗?
針對幾個問題,參考了美團團隊技術(shù)分享的解決方案,同時根據(jù)自己的理解做了適當(dāng)?shù)臄U展:
1. H5 和 Native 上線時間不一致,如何銜接?
比如一個功能以H5形式作出,但H5的發(fā)布滯后于native,當(dāng)H5上線之后,客戶端需要給H5提供一些跳轉(zhuǎn)的入口,這個跳轉(zhuǎn)的入口提供的應(yīng)該是在不發(fā)版的情況下去給出的。
這就需要對路由的跳轉(zhuǎn)做到后臺的可配置。
現(xiàn)階段的跳轉(zhuǎn):(Native 到 Native)
這種組件化的全局統(tǒng)跳協(xié)議,利用ARouter、天貓統(tǒng)跳協(xié)議等其他路由機制,都可以實現(xiàn)。
對這個跳轉(zhuǎn)去做一些擴展:對路由協(xié)議擴展后,讓他能支持跳轉(zhuǎn)到H5里。如下圖:
通過后臺動態(tài)決定一個頁面,究竟是native還是h5的展現(xiàn)形式。
舉個例子:
在APP里一個購物下單的流程,用戶需要訪問列表頁,商家的詳情頁,創(chuàng)建訂單,最后購買成功。對一些新的產(chǎn)品,有新的產(chǎn)品詳情和創(chuàng)建訂單樣式。可以通過h5上線的方式:
可以看到流程的兩端都是native,中間環(huán)節(jié)從native到h5可以動態(tài)切換
備注:這些路由配置,是實際需求的少數(shù),作為主體方案的有效補充存在。
2. H5 和 Native 如何進行通信?
傳統(tǒng)的JSInterface(兼容性)
看一段html代碼
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN" dir="ltr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript">
function showToast(toast) {
javascript:control.showToast(toast);
}
function log(msg){
console.log(msg);
}
</script>
</head>
<body>
<input type="button" value="toast"
onClick="showToast('Hello world')" />
</body>
</html>
對應(yīng)的java代碼:
public class MainActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = (WebView)findViewById(R.id.webView);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JsInterface(), "control");
webView.loadUrl("file:///android_asset/interact.html");
}
public class JsInterface {
@JavascriptInterface
public void showToast(String toast) {
Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
log("show toast success");
}
public void log(final String msg){
webView.post(new Runnable() {
@Override
public void run() {
webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")");
}
});
}
}
}
通過webView.addJavascriptInterface(new JsInterface(), "control"),將js的control與native的JsInterface聯(lián)系起來,實現(xiàn)了js向native的調(diào)用。反過來,webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")"),loadUrl調(diào)用到j(luò)s中定義的log方法,實現(xiàn)了native到j(luò)s的回調(diào)。
但是,,,
4.2版本之前的addjavascriptInterface接口引起的漏洞,可能導(dǎo)致惡意網(wǎng)頁通過Js方法遍歷剛剛通過addjavascriptInterface注入進來的類的所有方法從中獲取到getClass方法,然后通過反射獲取到Runtime對象,進而調(diào)用Runtime對象的exec方法執(zhí)行一些操作,惡意的Js代碼如下:
function execute(cmdArgs) {
for (var obj in window) {
if ("getClass" in window[obj]) {
alert(obj);
return window[obj].getClass().forName("java.lang.Runtime")
.getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
}
}
}
4.2以后通過為可以被Js調(diào)用的方法添加@JavascriptInterface注解來解決,但是4.2之前的版本兼容性存在問題。而且這種類似于函數(shù)式的調(diào)用方式,擴展性和兩端的兼容性都受限,所以他也就沒法廣泛采用了。
UrlRouter
嚴(yán)格的說,UrlRouter不算是js和java的通信,它只是一個通過url來讓前端喚起native頁面的框架。不過千萬不要小看它的作用,如果協(xié)議定義的合理,它可以讓前端,Android和iOS三端有一個高度的統(tǒng)一,十分方便。
public class NavWebViewClient extends WebViewClient {
private Context context;
public NavWebViewClient(Context context){
this.context = context;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if( Nav.from(context).toUri(url)){
return true;
}
view.loadUrl(url);
return true;
}
}
在方法shouldOverrideUrlLoading中,攔截后交給Nav處理,如果返回true則成功攔截,返回false則交給webview去load url。Nav中的解析處理,可以根據(jù)業(yè)務(wù)特點,根據(jù)scheme host url地址解析出跳轉(zhuǎn)路徑和攜帶的參數(shù)。
關(guān)于攜帶參數(shù),再多說兩句:h5與native要約定傳參的格式,比如json格式,那么在json字串里約定好字段的含義,就可以傳參,比如要實現(xiàn)跳轉(zhuǎn)到指定頁面,并攜帶參數(shù):
{"p": "orderlist","pa": {"tp": "per"}}
字段p代碼代碼頁面,字段pa代表參數(shù),pa字段后面的json表示此頁面需要的具體傳參。要注意傳參部分要進行加密處理。
JSBridge
這種方式不算新,一些大公司都有自己的jsBridge封裝方式,這里簡要說明一下基本原理。
WebView中有一個WebChromeClient類,有三個監(jiān)聽函數(shù):
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return super.onJsPrompt(view, url, message, defaultValue, result);
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
在js中,alert和confirm本身的使用概率還是很高的,不建議使用這兩個通道,onJsPrompt方法則可以用來js與java通信。通過在回調(diào)函數(shù)中message參數(shù)傳遞通訊協(xié)議,native根據(jù)協(xié)議解析決定自己的操作。
onJsPrompt方法中message參數(shù):hybrid://JSBridge:1538351/method?{“message”:”msg”}
sheme是hybrid://,host是JSBridge,方法名字是toast,傳遞的參數(shù)是以json格式傳遞的
java層的處理:
public class InjectedChromeClient extends WebChromeClient {
private final String TAG = "InjectedChromeClient";
private JsCallJava mJsCallJava;
public InjectedChromeClient() {
mJsCallJava = new JsCallJava();
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(mJsCallJava.call(view, message));
return true;
}
}
核心的call方法做了哪些?
public String call(WebView webView, String jsonStr) {
String methodName = "";
String name = BRIDGE_NAME;
String param = "{}";
String result = "";
String sid="";
if (!TextUtils.isEmpty(jsonStr) && jsonStr.startsWith(SCHEME)) {
Uri uri = Uri.parse(jsonStr);
name = uri.getHost();
param = uri.getQuery();
sid = getPort(jsonStr);
String path = uri.getPath();
if (!TextUtils.isEmpty(path)) {
methodName = path.replace("/", "");
}
}
if (!TextUtils.isEmpty(jsonStr)) {
try {
ArrayMap<String, Method> methodMap = mInjectNameMethods.get(name);
Object[] values = new Object[3];
values[0] = webView;
values[1] = new JSONObject(param);
values[2]=new JsCallback(webView,sid);
Method currMethod = null;
if (null != methodMap && !TextUtils.isEmpty(methodName)) {
currMethod = methodMap.get(methodName);
}
// 方法匹配失敗
if (currMethod == null) {
result = getReturn(jsonStr, RESULT_FAIL, "not found method(" + methodName + ") with valid parameters");
}else{
result = getReturn(jsonStr, RESULT_SUCCESS, currMethod.invoke(null, values));
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
result = getReturn(jsonStr, RESULT_FAIL, "call data empty");
}
return result;
}
代碼的思路如下:
(1) 在js腳本中把對應(yīng)的方法名,參數(shù)等寫成一個符合協(xié)議的uri,并且通過window.prompt方法發(fā)送給java層。
(2) 在java層的onJsPrompt方法中接受到對應(yīng)的message之后,通過JsCallJava類進行具體的解析。
(3) 在JsCallJava類中,我們解析得到對應(yīng)的方法名,參數(shù)等信息,并且在map中查找出對應(yīng)的類的方法。
思考:為什么不對message中的字段進行switch case的邏輯判斷,而是要經(jīng)過mInjectNameMethods的遍歷呢?
在業(yè)務(wù)復(fù)雜,應(yīng)用已經(jīng)組件化的情況下,JSBridge一定是作為整體架構(gòu)的一部分存在的,那么其定義和使用可能是分離的,通過mInjectNameMethods遍歷的方法,JSBridge中定義方法的權(quán)利交給了業(yè)務(wù)部門,有效實現(xiàn)了解耦。
可以這么說UrlRouter在頁面跳轉(zhuǎn)方面,JSBridge在方法調(diào)用方面,都具備各自的特點和優(yōu)勢,可以有效的結(jié)合起來。
3 . H5 頁面如何接近 Native 的體驗?
資源加載緩慢
1.模塊化你的 H5 頁面/應(yīng)用,引入模塊加載器
2.資源預(yù)加載
第一種方式是說使用 WebView 自身的緩存機制
這種緩存,系統(tǒng)會自動把它清掉,我們沒法進行控制
第二種方案是說,我們自己去構(gòu)建,自己管理緩存
把這些需要預(yù)加載的資源放在 APP 里面,他可能是預(yù)制放進去的,也可能是后續(xù)下載的。
每當(dāng)這個 WebView 發(fā)起資源請求的時候,我們會攔截到這些資源的請求,去本地檢查一下我們的這些靜態(tài)資源本地離線包有沒有。針對本地的緩存文件我們有些策略能夠及時的去更新它
資源預(yù)加載效果:
每個頁面在預(yù)加載后都有明顯提升(4G下明顯),同時橫向比較,也可看出,在一系列的web加載過程中,平均時間再降低。也說明了webview自身的緩存機制。
騰訊開源的hybrid框架(實際只是webview首屏優(yōu)化),實踐了webview的優(yōu)化,具體原理可以去github:
https://github.com/Tencent/VasSonic
VasSonic有如下特點(缺點):
1.VasSonic的技術(shù)實現(xiàn)上,需要服務(wù)端、客戶端 同時修改配合;
2.目前sonic后臺僅支持node.js和php版本,暫時還不支持其他后臺;
3.iOS 只支持UIWebView,不支持WKWebView,主要是因為在WKWebView目前不支持NSURLProtocol攔截;
vassonic這套方案,對于現(xiàn)有項目還是有一定侵入性的,而且需要服務(wù)端配合??梢詤⒖计渌悸?,完全照搬對于大項目有風(fēng)險。
最后放一張hybrid客戶端架構(gòu)圖
H5Container是架構(gòu)設(shè)計的重點和難點,其中nativeApi,HandwareApi都是對于手機對web提供功能的封裝。Data Channel負(fù)責(zé)埋點;JSBridge是處于底層的通訊接口,JSBridges為各個模塊的定制和擴展。
Synchronize Service 模塊表示和服務(wù)器的長連接通信模塊,用于接受服務(wù)器端各種推送,包括離線包等。 Source Merge Service 模塊表示對解壓后的H5資源進行更新,包括增加文件、以舊換新以及刪除過期文件等。
總結(jié):
一般來說Hybrid的項目一般是用在一些快速迭代試錯的地方。另外包括有一些非主流產(chǎn)品的頁面,我們傾向于用 Hybrid 的形式做.
但是像前端購買一些交易環(huán)節(jié),特別核心的流程的話,我們一般情況下會用 Native 的形式去寫這些頁面,去提升,達到一個極致的用戶體驗。不要為了hybrid而hybrid,一切都是根據(jù)需求的實際情況出發(fā),同時hybrid的框架在設(shè)計時,協(xié)議方面要注意ios android兩端的統(tǒng)一,android端自身盡量考慮擴展性和解耦,有利于后續(xù)開發(fā)迭代的穩(wěn)定和迅速。