Hybrid应用开发初探

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客户端架构图

Zjiyyu.png

H5Container是架构设计的重点和难点,其中nativeApi,HandwareApi都是对于手机对web提供功能的封装。Data Channel负责埋点;JSBridge是处于底层的通讯接口,JSBridges为各个模块的定制和扩展。
Synchronize Service 模块表示和服务器的长连接通信模块,用于接受服务器端各种推送,包括离线包等。 Source Merge Service 模块表示对解压后的H5资源进行更新,包括增加文件、以旧换新以及删除过期文件等。

总结:

一般来说Hybrid的项目一般是用在一些快速迭代试错的地方。另外包括有一些非主流产品的页面,我们倾向于用 Hybrid 的形式做.
但是像前端购买一些交易环节,特别核心的流程的话,我们一般情况下会用 Native 的形式去写这些页面,去提升,达到一个极致的用户体验。不要为了hybrid而hybrid,一切都是根据需求的实际情况出发,同时hybrid的框架在设计时,协议方面要注意ios android两端的统一,android端自身尽量考虑扩展性和解耦,有利于后续开发迭代的稳定和迅速。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容