Hybrid開(kāi)發(fā)定義和使用范圍
為什么要采用hybrid:
現(xiàn)階段的應(yīng)用開(kāi)發(fā),會(huì)遇到如下問(wèn)題和挑戰(zhàn):
1 一些頁(yè)面或業(yè)務(wù)恃泪,和運(yùn)營(yíng)強(qiáng)相關(guān),無(wú)法native固定(例如電子商務(wù) 詳情展示)
2 客戶端發(fā)版周期長(zhǎng),一些需求想要很快上線,或變化非常頻繁
現(xiàn)有3類主流APP任斋,分別為:Web App、Hybrid App(混合模式應(yīng)用耻涛,Hybrid有“混合的”意思)废酷、 Native App;
Native App 和 Web App不作解釋了抹缕,主要解釋Hybrid App澈蟆。
Hybrid App按網(wǎng)頁(yè)語(yǔ)言與程序語(yǔ)言的混合,通常分為三種類型:多View混合型卓研,單View混合型趴俘,Web主體型。
單頁(yè)混合型
即在同一個(gè)頁(yè)面內(nèi)奏赘,同時(shí)包括Native View和Web View寥闪。互相之間是覆蓋(層疊)的關(guān)系磨淌。這種Hybrid App的開(kāi)發(fā)成本較高疲憋,開(kāi)發(fā)難度較大,但是體驗(yàn)較好梁只。如百度搜索為代表的單頁(yè)混合型移動(dòng)應(yīng)用缚柳,既可以實(shí)現(xiàn)充分的靈活性埃脏,又能實(shí)現(xiàn)較好的用戶體驗(yàn)。一般如無(wú)特殊需求喂击,不會(huì)采用此種方式剂癌。
Web主體型
這種常見(jiàn)于市面上第三方hybrid框架實(shí)現(xiàn)。例如Wex5翰绊,AppCan和Rexsee都屬于Web主體型移動(dòng)應(yīng)用中間件佩谷。基本可以實(shí)現(xiàn)跨平臺(tái)监嗜,主要以網(wǎng)頁(yè)語(yǔ)言編寫谐檀,利用框架生成native的殼子。但是一般用戶體驗(yàn)存在缺陷裁奇。常見(jiàn)于一些小型或功能單一app桐猬。
多主體混合型
即Native View和Web View獨(dú)立展示,交替出現(xiàn)刽肠。這種應(yīng)用混合邏輯相對(duì)簡(jiǎn)單溃肪。這種移動(dòng)應(yīng)用主體通常是Native App,Web技術(shù)只是起到補(bǔ)充作用音五。開(kāi)發(fā)難度和Native App基本相當(dāng)惫撰。常見(jiàn)的Hybrid App是Native View與WebView交替的場(chǎng)景出現(xiàn)。
與App內(nèi)接入H5的區(qū)別:
hybrid的開(kāi)發(fā)模式與我們之前一些運(yùn)營(yíng)頁(yè)面采用h5的根本區(qū)別在于躺涝,后者只是在一些不重要的功能上實(shí)現(xiàn)可運(yùn)營(yíng)和便于分享厨钻,并不接入到應(yīng)用的主要流程中,與native的交互較少坚嗜,對(duì)應(yīng)用的影響小夯膀,作為開(kāi)發(fā)的一個(gè)小模塊獨(dú)立存在。hybrid開(kāi)發(fā)則是將web頁(yè)面作為native的重要補(bǔ)充苍蔬,應(yīng)用功能的重要組成部分诱建,需要考慮上線節(jié)奏,web與native的通訊碟绑,優(yōu)化web體驗(yàn)等問(wèn)題俺猿,對(duì)于應(yīng)用來(lái)講,web與native的地位蜈敢,被大大拉平了辜荠。
如何區(qū)分Hybrid APP中的原生頁(yè)面和H5頁(yè)面
很多人從頁(yè)面的設(shè)計(jì)上來(lái)區(qū)分的。如:(1)頂部顯示網(wǎng)頁(yè)鏈接抓狭;(2)有加載的進(jìn)度條伯病;(3)沒(méi)有底部tab導(dǎo)航欄;(4)頂部顯示兩個(gè)導(dǎo)航條;
但是現(xiàn)在app的h5頁(yè)面做的可以以假亂真了午笛,這些統(tǒng)統(tǒng)不管用惭蟋。
以淘寶為例:
設(shè)置-開(kāi)發(fā)者選項(xiàng)-顯示布局邊界
H5中使用了webview控件,其作為一個(gè)控件药磺,只有一個(gè)邊界框告组,所以通過(guò)這一點(diǎn),就比較容易區(qū)分出一個(gè)界面是webview實(shí)現(xiàn)的還是原生布局控件實(shí)現(xiàn)的
這次再來(lái)看看:
幾個(gè)主流HybridApp:淘寶癌佩、京東木缝、大眾點(diǎn)評(píng)等
Hybrid中Native和H5的使用范圍:
Native
1 應(yīng)用核心邏輯:例如 下單、支付等
2 對(duì)手機(jī)native功能(如照相围辙、定位)重度依賴的頁(yè)面
3 用戶體驗(yàn)要求強(qiáng)我碟,運(yùn)營(yíng)要求弱的頁(yè)面
H5:
1.功能開(kāi)發(fā)不完善,試運(yùn)營(yíng)階段(試錯(cuò)成本低)
2.強(qiáng)運(yùn)營(yíng)需求姚建,在功能調(diào)整或內(nèi)容的運(yùn)營(yíng)上很靈活
3.階段性的營(yíng)銷活動(dòng)矫俺,希望被分享出去
Hybrid開(kāi)發(fā)中要解決的幾個(gè)問(wèn)題
一、H5 和 Native 上線時(shí)間不一致掸冤,如何銜接厘托?
二、H5 和 Native 之間如何進(jìn)行通信稿湿?
三铅匹、H5 頁(yè)面如何接近 Native 的體驗(yàn)?
針對(duì)幾個(gè)問(wèn)題缎罢,參考了美團(tuán)團(tuán)隊(duì)技術(shù)分享的解決方案伊群,同時(shí)根據(jù)自己的理解做了適當(dāng)?shù)臄U(kuò)展:
1. H5 和 Native 上線時(shí)間不一致考杉,如何銜接策精?
比如一個(gè)功能以H5形式作出,但H5的發(fā)布滯后于native崇棠,當(dāng)H5上線之后咽袜,客戶端需要給H5提供一些跳轉(zhuǎn)的入口,這個(gè)跳轉(zhuǎn)的入口提供的應(yīng)該是在不發(fā)版的情況下去給出的枕稀。
這就需要對(duì)路由的跳轉(zhuǎn)做到后臺(tái)的可配置询刹。
現(xiàn)階段的跳轉(zhuǎn):(Native 到 Native)
這種組件化的全局統(tǒng)跳協(xié)議,利用ARouter萎坷、天貓統(tǒng)跳協(xié)議等其他路由機(jī)制凹联,都可以實(shí)現(xiàn)。
對(duì)這個(gè)跳轉(zhuǎn)去做一些擴(kuò)展:對(duì)路由協(xié)議擴(kuò)展后哆档,讓他能支持跳轉(zhuǎn)到H5里蔽挠。如下圖:
通過(guò)后臺(tái)動(dòng)態(tài)決定一個(gè)頁(yè)面,究竟是native還是h5的展現(xiàn)形式瓜浸。
舉個(gè)例子:
在APP里一個(gè)購(gòu)物下單的流程澳淑,用戶需要訪問(wèn)列表頁(yè)比原,商家的詳情頁(yè),創(chuàng)建訂單杠巡,最后購(gòu)買成功量窘。對(duì)一些新的產(chǎn)品,有新的產(chǎn)品詳情和創(chuàng)建訂單樣式氢拥“鐾可以通過(guò)h5上線的方式:
可以看到流程的兩端都是native,中間環(huán)節(jié)從native到h5可以動(dòng)態(tài)切換
備注:這些路由配置嫩海,是實(shí)際需求的少數(shù)厘线,作為主體方案的有效補(bǔ)充存在。
2. H5 和 Native 如何進(jìn)行通信出革?
傳統(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>
對(duì)應(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 + "'" + ")");
}
});
}
}
}
通過(guò)webView.addJavascriptInterface(new JsInterface(), "control")造壮,將js的control與native的JsInterface聯(lián)系起來(lái),實(shí)現(xiàn)了js向native的調(diào)用骂束。反過(guò)來(lái)耳璧,webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")"),loadUrl調(diào)用到j(luò)s中定義的log方法展箱,實(shí)現(xiàn)了native到j(luò)s的回調(diào)旨枯。
但是,混驰,攀隔,
4.2版本之前的addjavascriptInterface接口引起的漏洞,可能導(dǎo)致惡意網(wǎng)頁(yè)通過(guò)Js方法遍歷剛剛通過(guò)addjavascriptInterface注入進(jìn)來(lái)的類的所有方法從中獲取到getClass方法栖榨,然后通過(guò)反射獲取到Runtime對(duì)象昆汹,進(jìn)而調(diào)用Runtime對(duì)象的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以后通過(guò)為可以被Js調(diào)用的方法添加@JavascriptInterface注解來(lái)解決婴栽,但是4.2之前的版本兼容性存在問(wèn)題满粗。而且這種類似于函數(shù)式的調(diào)用方式,擴(kuò)展性和兩端的兼容性都受限愚争,所以他也就沒(méi)法廣泛采用了映皆。
UrlRouter
嚴(yán)格的說(shuō),UrlRouter不算是js和java的通信轰枝,它只是一個(gè)通過(guò)url來(lái)讓前端喚起native頁(yè)面的框架捅彻。不過(guò)千萬(wàn)不要小看它的作用,如果協(xié)議定義的合理鞍陨,它可以讓前端步淹,Android和iOS三端有一個(gè)高度的統(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ù)特點(diǎn),根據(jù)scheme host url地址解析出跳轉(zhuǎn)路徑和攜帶的參數(shù)盅藻。
關(guān)于攜帶參數(shù)购桑,再多說(shuō)兩句:h5與native要約定傳參的格式,比如json格式氏淑,那么在json字串里約定好字段的含義勃蜘,就可以傳參,比如要實(shí)現(xiàn)跳轉(zhuǎn)到指定頁(yè)面假残,并攜帶參數(shù):
{"p": "orderlist","pa": {"tp": "per"}}
字段p代碼代碼頁(yè)面缭贡,字段pa代表參數(shù),pa字段后面的json表示此頁(yè)面需要的具體傳參辉懒。要注意傳參部分要進(jìn)行加密處理阳惹。
JSBridge
這種方式不算新,一些大公司都有自己的jsBridge封裝方式眶俩,這里簡(jiǎn)要說(shuō)明一下基本原理莹汤。
WebView中有一個(gè)WebChromeClient類,有三個(gè)監(jiān)聽(tīng)函數(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本身的使用概率還是很高的纲岭,不建議使用這兩個(gè)通道,onJsPrompt方法則可以用來(lái)js與java通信线罕。通過(guò)在回調(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腳本中把對(duì)應(yīng)的方法名,參數(shù)等寫成一個(gè)符合協(xié)議的uri跨蟹,并且通過(guò)window.prompt方法發(fā)送給java層雳殊。
(2) 在java層的onJsPrompt方法中接受到對(duì)應(yīng)的message之后,通過(guò)JsCallJava類進(jìn)行具體的解析窗轩。
(3) 在JsCallJava類中夯秃,我們解析得到對(duì)應(yīng)的方法名,參數(shù)等信息,并且在map中查找出對(duì)應(yīng)的類的方法仓洼。
思考:為什么不對(duì)message中的字段進(jìn)行switch case的邏輯判斷介陶,而是要經(jīng)過(guò)mInjectNameMethods的遍歷呢?
在業(yè)務(wù)復(fù)雜,應(yīng)用已經(jīng)組件化的情況下色建,JSBridge一定是作為整體架構(gòu)的一部分存在的哺呜,那么其定義和使用可能是分離的,通過(guò)mInjectNameMethods遍歷的方法箕戳,JSBridge中定義方法的權(quán)利交給了業(yè)務(wù)部門某残,有效實(shí)現(xiàn)了解耦。
可以這么說(shuō)UrlRouter在頁(yè)面跳轉(zhuǎn)方面陵吸,JSBridge在方法調(diào)用方面玻墅,都具備各自的特點(diǎn)和優(yōu)勢(shì),可以有效的結(jié)合起來(lái)壮虫。
3 . H5 頁(yè)面如何接近 Native 的體驗(yàn)澳厢?
資源加載緩慢
1.模塊化你的 H5 頁(yè)面/應(yīng)用,引入模塊加載器
2.資源預(yù)加載
第一種方式是說(shuō)使用 WebView 自身的緩存機(jī)制
這種緩存囚似,系統(tǒng)會(huì)自動(dòng)把它清掉赏酥,我們沒(méi)法進(jìn)行控制
第二種方案是說(shuō),我們自己去構(gòu)建谆构,自己管理緩存
把這些需要預(yù)加載的資源放在 APP 里面裸扶,他可能是預(yù)制放進(jìn)去的,也可能是后續(xù)下載的搬素。
每當(dāng)這個(gè) WebView 發(fā)起資源請(qǐng)求的時(shí)候呵晨,我們會(huì)攔截到這些資源的請(qǐng)求,去本地檢查一下我們的這些靜態(tài)資源本地離線包有沒(méi)有熬尺。針對(duì)本地的緩存文件我們有些策略能夠及時(shí)的去更新它
資源預(yù)加載效果:
每個(gè)頁(yè)面在預(yù)加載后都有明顯提升(4G下明顯)摸屠,同時(shí)橫向比較,也可看出粱哼,在一系列的web加載過(guò)程中季二,平均時(shí)間再降低。也說(shuō)明了webview自身的緩存機(jī)制揭措。
騰訊開(kāi)源的hybrid框架(實(shí)際只是webview首屏優(yōu)化)胯舷,實(shí)踐了webview的優(yōu)化,具體原理可以去github:
https://github.com/Tencent/VasSonic
VasSonic有如下特點(diǎn)(缺點(diǎn)):
1.VasSonic的技術(shù)實(shí)現(xiàn)上绊含,需要服務(wù)端桑嘶、客戶端 同時(shí)修改配合;
2.目前sonic后臺(tái)僅支持node.js和php版本躬充,暫時(shí)還不支持其他后臺(tái)逃顶;
3.iOS 只支持UIWebView讨便,不支持WKWebView,主要是因?yàn)樵赪KWebView目前不支持NSURLProtocol攔截以政;
vassonic這套方案霸褒,對(duì)于現(xiàn)有項(xiàng)目還是有一定侵入性的,而且需要服務(wù)端配合盈蛮“涟裕可以參考其思路,完全照搬對(duì)于大項(xiàng)目有風(fēng)險(xiǎn)眉反。
最后放一張hybrid客戶端架構(gòu)圖
H5Container是架構(gòu)設(shè)計(jì)的重點(diǎn)和難點(diǎn)昙啄,其中nativeApi,HandwareApi都是對(duì)于手機(jī)對(duì)web提供功能的封裝寸五。Data Channel負(fù)責(zé)埋點(diǎn);JSBridge是處于底層的通訊接口梳凛,JSBridges為各個(gè)模塊的定制和擴(kuò)展。
Synchronize Service 模塊表示和服務(wù)器的長(zhǎng)連接通信模塊梳杏,用于接受服務(wù)器端各種推送韧拒,包括離線包等。 Source Merge Service 模塊表示對(duì)解壓后的H5資源進(jìn)行更新十性,包括增加文件叛溢、以舊換新以及刪除過(guò)期文件等。
總結(jié):
一般來(lái)說(shuō)Hybrid的項(xiàng)目一般是用在一些快速迭代試錯(cuò)的地方劲适。另外包括有一些非主流產(chǎn)品的頁(yè)面楷掉,我們傾向于用 Hybrid 的形式做.
但是像前端購(gòu)買一些交易環(huán)節(jié),特別核心的流程的話霞势,我們一般情況下會(huì)用 Native 的形式去寫這些頁(yè)面烹植,去提升,達(dá)到一個(gè)極致的用戶體驗(yàn)愕贡。不要為了hybrid而hybrid草雕,一切都是根據(jù)需求的實(shí)際情況出發(fā),同時(shí)hybrid的框架在設(shè)計(jì)時(shí)固以,協(xié)議方面要注意ios android兩端的統(tǒng)一墩虹,android端自身盡量考慮擴(kuò)展性和解耦,有利于后續(xù)開(kāi)發(fā)迭代的穩(wěn)定和迅速憨琳。