Hybrid开发定义和使用范围
为什么要采用hybrid:
现阶段的应用开发,会遇到如下问题和挑战:
1 一些页面或业务,和运营强相关,无法native固定(例如电子商务 详情展示)
2 客户端发版周期长,一些需求想要很快上线,或变化非常频繁
现有3类主流APP,分别为:Web App、Hybrid App(混合模式应用,Hybrid有“混合的”意思)、 Native App;
Native App 和 Web App不作解释了,主要解释Hybrid App。
Hybrid App按网页语言与程序语言的混合,通常分为三种类型:多View混合型,单View混合型,Web主体型。
单页混合型
即在同一个页面内,同时包括Native View和Web View。互相之间是覆盖(层叠)的关系。这种Hybrid App的开发成本较高,开发难度较大,但是体验较好。如百度搜索为代表的单页混合型移动应用,既可以实现充分的灵活性,又能实现较好的用户体验。一般如无特殊需求,不会采用此种方式。
Web主体型
这种常见于市面上第三方hybrid框架实现。例如Wex5,AppCan和Rexsee都属于Web主体型移动应用中间件。基本可以实现跨平台,主要以网页语言编写,利用框架生成native的壳子。但是一般用户体验存在缺陷。常见于一些小型或功能单一app。
多主体混合型
即Native View和Web View独立展示,交替出现。这种应用混合逻辑相对简单。这种移动应用主体通常是Native App,Web技术只是起到补充作用。开发难度和Native App基本相当。常见的Hybrid App是Native View与WebView交替的场景出现。
与App内接入H5的区别:
hybrid的开发模式与我们之前一些运营页面采用h5的根本区别在于,后者只是在一些不重要的功能上实现可运营和便于分享,并不接入到应用的主要流程中,与native的交互较少,对应用的影响小,作为开发的一个小模块独立存在。hybrid开发则是将web页面作为native的重要补充,应用功能的重要组成部分,需要考虑上线节奏,web与native的通讯,优化web体验等问题,对于应用来讲,web与native的地位,被大大拉平了。
如何区分Hybrid APP中的原生页面和H5页面
很多人从页面的设计上来区分的。如:(1)顶部显示网页链接;(2)有加载的进度条;(3)没有底部tab导航栏;(4)顶部显示两个导航条;
但是现在app的h5页面做的可以以假乱真了,这些统统不管用。
以淘宝为例:
设置-开发者选项-显示布局边界
H5中使用了webview控件,其作为一个控件,只有一个边界框,所以通过这一点,就比较容易区分出一个界面是webview实现的还是原生布局控件实现的
这次再来看看:
几个主流HybridApp:淘宝、京东、大众点评等
Hybrid中Native和H5的使用范围:
Native
1 应用核心逻辑:例如 下单、支付等
2 对手机native功能(如照相、定位)重度依赖的页面
3 用户体验要求强,运营要求弱的页面
H5:
1.功能开发不完善,试运营阶段(试错成本低)
2.强运营需求,在功能调整或内容的运营上很灵活
3.阶段性的营销活动,希望被分享出去
Hybrid开发中要解决的几个问题
一、H5 和 Native 上线时间不一致,如何衔接?
二、H5 和 Native 之间如何进行通信?
三、H5 页面如何接近 Native 的体验?
针对几个问题,参考了美团团队技术分享的解决方案,同时根据自己的理解做了适当的扩展:
1. H5 和 Native 上线时间不一致,如何衔接?
比如一个功能以H5形式作出,但H5的发布滞后于native,当H5上线之后,客户端需要给H5提供一些跳转的入口,这个跳转的入口提供的应该是在不发版的情况下去给出的。
这就需要对路由的跳转做到后台的可配置。
现阶段的跳转:(Native 到 Native)
这种组件化的全局统跳协议,利用ARouter、天猫统跳协议等其他路由机制,都可以实现。
对这个跳转去做一些扩展:对路由协议扩展后,让他能支持跳转到H5里。如下图:
通过后台动态决定一个页面,究竟是native还是h5的展现形式。
举个例子:
在APP里一个购物下单的流程,用户需要访问列表页,商家的详情页,创建订单,最后购买成功。对一些新的产品,有新的产品详情和创建订单样式。可以通过h5上线的方式:
可以看到流程的两端都是native,中间环节从native到h5可以动态切换
备注:这些路由配置,是实际需求的少数,作为主体方案的有效补充存在。
2. H5 和 Native 如何进行通信?
传统的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>
对应的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联系起来,实现了js向native的调用。反过来,webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")"),loadUrl调用到js中定义的log方法,实现了native到js的回调。
但是,,,
4.2版本之前的addjavascriptInterface接口引起的漏洞,可能导致恶意网页通过Js方法遍历刚刚通过addjavascriptInterface注入进来的类的所有方法从中获取到getClass方法,然后通过反射获取到Runtime对象,进而调用Runtime对象的exec方法执行一些操作,恶意的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调用的方法添加@JavascriptInterface注解来解决,但是4.2之前的版本兼容性存在问题。而且这种类似于函数式的调用方式,扩展性和两端的兼容性都受限,所以他也就没法广泛采用了。
UrlRouter
严格的说,UrlRouter不算是js和java的通信,它只是一个通过url来让前端唤起native页面的框架。不过千万不要小看它的作用,如果协议定义的合理,它可以让前端,Android和iOS三端有一个高度的统一,十分方便。
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中的解析处理,可以根据业务特点,根据scheme host url地址解析出跳转路径和携带的参数。
关于携带参数,再多说两句:h5与native要约定传参的格式,比如json格式,那么在json字串里约定好字段的含义,就可以传参,比如要实现跳转到指定页面,并携带参数:
{"p": "orderlist","pa": {"tp": "per"}}
字段p代码代码页面,字段pa代表参数,pa字段后面的json表示此页面需要的具体传参。要注意传参部分要进行加密处理。
JSBridge
这种方式不算新,一些大公司都有自己的jsBridge封装方式,这里简要说明一下基本原理。
WebView中有一个WebChromeClient类,有三个监听函数:
@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通信。通过在回调函数中message参数传递通讯协议,native根据协议解析决定自己的操作。
onJsPrompt方法中message参数:hybrid://JSBridge:1538351/method?{“message”:”msg”}
sheme是hybrid://,host是JSBridge,方法名字是toast,传递的参数是以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脚本中把对应的方法名,参数等写成一个符合协议的uri,并且通过window.prompt方法发送给java层。
(2) 在java层的onJsPrompt方法中接受到对应的message之后,通过JsCallJava类进行具体的解析。
(3) 在JsCallJava类中,我们解析得到对应的方法名,参数等信息,并且在map中查找出对应的类的方法。
思考:为什么不对message中的字段进行switch case的逻辑判断,而是要经过mInjectNameMethods的遍历呢?
在业务复杂,应用已经组件化的情况下,JSBridge一定是作为整体架构的一部分存在的,那么其定义和使用可能是分离的,通过mInjectNameMethods遍历的方法,JSBridge中定义方法的权利交给了业务部门,有效实现了解耦。
可以这么说UrlRouter在页面跳转方面,JSBridge在方法调用方面,都具备各自的特点和优势,可以有效的结合起来。
3 . H5 页面如何接近 Native 的体验?
资源加载缓慢
1.模块化你的 H5 页面/应用,引入模块加载器
2.资源预加载
第一种方式是说使用 WebView 自身的缓存机制
这种缓存,系统会自动把它清掉,我们没法进行控制
第二种方案是说,我们自己去构建,自己管理缓存
把这些需要预加载的资源放在 APP 里面,他可能是预制放进去的,也可能是后续下载的。
每当这个 WebView 发起资源请求的时候,我们会拦截到这些资源的请求,去本地检查一下我们的这些静态资源本地离线包有没有。针对本地的缓存文件我们有些策略能够及时的去更新它
资源预加载效果:
每个页面在预加载后都有明显提升(4G下明显),同时横向比较,也可看出,在一系列的web加载过程中,平均时间再降低。也说明了webview自身的缓存机制。
腾讯开源的hybrid框架(实际只是webview首屏优化),实践了webview的优化,具体原理可以去github:
https://github.com/Tencent/VasSonic
VasSonic有如下特点(缺点):
1.VasSonic的技术实现上,需要服务端、客户端 同时修改配合;
2.目前sonic后台仅支持node.js和php版本,暂时还不支持其他后台;
3.iOS 只支持UIWebView,不支持WKWebView,主要是因为在WKWebView目前不支持NSURLProtocol拦截;
vassonic这套方案,对于现有项目还是有一定侵入性的,而且需要服务端配合。可以参考其思路,完全照搬对于大项目有风险。
最后放一张hybrid客户端架构图
H5Container是架构设计的重点和难点,其中nativeApi,HandwareApi都是对于手机对web提供功能的封装。Data Channel负责埋点;JSBridge是处于底层的通讯接口,JSBridges为各个模块的定制和扩展。
Synchronize Service 模块表示和服务器的长连接通信模块,用于接受服务器端各种推送,包括离线包等。 Source Merge Service 模块表示对解压后的H5资源进行更新,包括增加文件、以旧换新以及删除过期文件等。
总结:
一般来说Hybrid的项目一般是用在一些快速迭代试错的地方。另外包括有一些非主流产品的页面,我们倾向于用 Hybrid 的形式做.
但是像前端购买一些交易环节,特别核心的流程的话,我们一般情况下会用 Native 的形式去写这些页面,去提升,达到一个极致的用户体验。不要为了hybrid而hybrid,一切都是根据需求的实际情况出发,同时hybrid的框架在设计时,协议方面要注意ios android两端的统一,android端自身尽量考虑扩展性和解耦,有利于后续开发迭代的稳定和迅速。