浅谈Hybrid

1,978 阅读11分钟

引言

随着 Web 技术和移动设备的飞速发展,各种 APP 层出不穷,极速的业务扩展提高了团队对开发效率的要求,这个时候使用 IOS/Andriod 开发一个 APP 似乎成本有点过高了,而 H5 的低成本、高效率、跨平台等特性马上被利用起来形成了一种新的开发模式:Hybrid APP

Hybrid 技术已经成为一种最主流最常见的方案。一套好的 Hybrid 架构解决方案能让 App 既能拥有极致的体验和性能,同时也能拥有 Web 技术 灵活的开发模式、跨平台能力以及热更新机制。本文主要是结合我最近开发的一个 Hybrid 项目(https://github.com/Jack-cool/hybrid_jd),带大家全面了解一下 Hybrid。

现有混合方案

深入了解 Hybrid 前,让我们先来看一下目前市面上比较成熟的混合解决方案。

基于 WebView UI 的基础方案

这种是市面上大多数 app 采取的方案,也是混合开发最基础的方案。在 webview 的基础上,与原生客户端建立js bridge桥接,以达到 js 调用Native API和 Native 执行js方法的目的。

目前国内绝大部分的大厂都有一套自己的基于 webview ui 的 hybrid 解决方案,例如微信的JS-SDK,支付宝的JSAPI等,通过JSBridge完成 h5 与 Native 的双向通讯,从而赋予 H5 一定程度的原生能力。

基于 Native UI 的方案

可以简单理解为“跨平台”,现在比较通用的有React NativeWeexFlutter等。在赋予 H5 原生 API 能力的基础上,进一步通过 JSBridge 将 JS 解析成的虚拟节点数(Virtual DOM)传递到 Native 并使用原生渲染。我们这里来看下上面提到的这三种:

React Native

“Learn once, write anywhere”,React Native采用了 React 的设计模式,但 UI 渲染、动画效果、网络请求等均由原生端实现(由于 JS 是单线程,不大可能处理太多耗时的操作)。开发者编写的 JS 代码,通过 React Native 的中间层转化为原生控件和操作,极大的提高了用户体验。

React Native所有的标签都不是真实控件,JS 代码中所写控件的作用,类似 Map 中的 key 值。JS 端通过这个 key 组合的 Dom ,最后 Native 端会解析这个 Dom ,得到对应的 Native 控件渲染,如 Android 中 标签对应 ViewGroup 控件。

总结下来,就是:React Native 是利用 JS 来调用 Native 端的组件,从而实现相应的功能。

Weex

“Write once, run everywhere”,基于 Vue 设计模式,支持 web、android、ios 三端,原生端同样通过中间层转化,将控件和操作转化为原生逻辑来提升用户体验。

在 weex 中,主要包括三大部分:JS BridgeRenderDom,JS Bridge 主要用来和 JS 端实现进行双向通信,比如把 JS 端的 dom 结构传递给 Dom 线程。Dom 主要是用于负责 dom 的解析、映射、添加等等的操作,最后通知 UI 线程更新。而 Render 负责在 UI 线程中对 dom 实现渲染。

和 react native 一样,weex 所有的标签也都不是真实控件,JS 代码中所生成的 dom,最终都是由 Native 端解析,再得到对应的 Native 控件渲染,如 Android 中 标签对应 WXTextView 控件。

Flutter

Flutter 是谷歌 2018 年发布的跨平台移动 UI 框架。与 react native 和 weex 的通过 Javascript 开发不同,Flutter 的编程语言是Dart,所以执行时并不需要 Javascript 引擎,但实际效果最终也通过原生渲染。

看完这三种方案的简介,下面让我们简单来做个对比吧:

React Native Weex Flutter
平台实现 JavaScript JavaScript 原生编码
引擎 JS V8 JSCore Flutter engine
核心语言 React Vue Dart
框架程度 较重 较轻
特点 适合开发整体 App 适合单页面 适合开发整体 App
支持 Android、IOS Android、IOS、Web Android、IOS(可能还不止)
Apk 大小(Release) 7.6M 10.6M 8.1M

小程序

小程序开发本质上还是前端 HTML + CSS + JS 那一套逻辑,它基于 WebView 和微信(当然支付宝、百度、字节等现在都有自己的小程序,这里只是拿微信小程序做个说明)自己定义的一套 JS/WXML/WXSS/JSON 来开发和渲染页面。微信官方文档里提到,小程序运行在三端:iOS、Android 和用于调试的开发者工具,三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的。

通过更加定制化的 JSBridge,并使用双 WebView 双线程的模式隔离了 JS 逻辑与 UI 渲染,形成了特殊的开发模式,加强了 H5 与 Native 混合程度,提高了页面性能及开发体验。

PWA

Progressive Web App, 简称 PWA,是提升 Web App 体验的一种新方法,能给用户带来原生应用的体验。

PWA 能做到原生应用的体验不是靠某一项特定的技术,而是经过应用一系列新技术进行改进,在安全、性能和体验三个方面都有了很大的提升,PWA 本质上还是 Web App,并兼具了 Native App 的一些特性和优点,主要包括下面三点:

  • 可靠 - 即使在不稳定的网络环境下,也能快速加载并展现
  • 体验 - 快速响应,并且有平滑的动画响应用户的操作
  • 粘性 - 设备上的原生应用,具有沉浸式的用户体验,用户可以添加到桌面

Android 和主流的浏览器都早已支持了 PWA 标准,在 iOS 11.3 和 macOS 10.13.4 上,苹果的 Safari 上也支持了 PWA。相信在不久的将来势必会迎来 PWA 的大爆发...

看完目前主流的混合解决方案,我们回归本篇主题,讲解一下成熟解决方案背后的 Hybrid底层基础,要知道决定上层建筑的永远都是底层基础,新的技术层出不穷,只有原理是不变的~~

Hybrid 是什么,为什么要用 Hybrid?

Hybrid,字面意思“混合”。可以简单理解为是前端和客户端的混合开发。

让我们先来看一下目前主流的移动应用开发方式:

Native APP

Native App 是一种基于智能手机本地操作系统如 iOS、Android、WP 并使用原生程式编写运行的第三方应用程序,也叫本地 app。一般使用的开发语言为 Java、C++、Objective-C。。分别来看一下 Native 开发的优缺点:

  • 优点

    • 用户体验近乎完美
    • 性能稳定
    • 访问本地资源(通讯录、相册)
    • 操作流畅
    • 设计出色的动效、转场
    • 系统级的贴心通知或提醒
    • 用户留存率高
  • 缺点

    • 门槛高,原生开发人才稀缺,至少比前端和后端少,开发环境昂贵
    • 发布成本高,需要通过 store 或 market 的审核,导致更新缓慢
    • 维持多个版本、多个系统的成本比较高,而且必须做兼容
    • 无法跨平台,开发的成本比较大,各个系统独立开发

Web APP

Web App,顾名思义是指基于 Web 的应用,基本采用 Html5 语言写出,不需要下载安装。类似于现在所说的轻应用。基于浏览器运行的应用,基本上可以说是触屏版的网页应用。分别来看一下 Web 开发的优缺点:

  • 优点

    • 开发成本低
    • 临时入口,可以随意嵌入
    • 无需安装,不会占用手机内存,而且更新速度最快
    • 能够跨多个平台和终端
    • 不存在多版本问题,维护成本低
  • 缺点

    • 无法获取系统级别的通知,提醒,动效等等
    • 设计受限制较多
    • 体验较差
    • 受限于手机和浏览器性能,用户体验相较于其他模式最差
    • 用户留存率低

究其原因就是性能要求的问题。Web app 之所以能够占领开发市场,主要是因为它的开发速度快,使用简单,应用范围广,但是在性能方面因为无法调用全部硬件底层功能,就现在讲,还是比不过原生 App 的性能。

Hybrid APP

混合开发,也就是半原生半 Web 的开发模式,由原生提供统一的 API 给 JS 调用,实际的主要逻辑有 Html 和 JS 来完成,最终是放在 webview 中显示的,所以只需要写一套代码即可达到跨平台效果。

Hybrid App 兼具了 Native APP 用户体验佳、系统功能强大和 Web APP 跨平台、更新速度快的优势。本质其实是在原生的 App 中,使用 WebView 作为容器直接承载 Web 页面。因此,最核心的点就是 Native 端 与 H5 端 之间的双向通讯层,也就是我们常说的 JSBridge

下面让我们来看下 JS 与 Native(客户端)通信的方式吧。

JS 与客户端通信

JS 通知客户端(Native)

JS上下文注入

原理其实就是 Native 获取 JavaScript 环境上下文,并直接在上面挂载对象或者方法,使 JS 可以直接调用。

Android 与 IOS 分别拥有对应的挂载方式。分别对应是:苹果UIWebview JavaScriptCore注入安卓addJavascriptInterface注入苹果WKWebView scriptMessageHandler注入

上面这三种方式都可以被称为是JS上下文注入,他们都有一个共同的特点就是,不通过任何拦截的办法,而是直接将一个 native 对象(or 函数)注入到 JS 里面,可以由 Web 的 JS 代码直接调用,直接操作。

弹窗拦截

这种方式主要是通过修改浏览器 Window 对象的某些方法,然后拦截固定规则的参数,之后分发给客户端对应的处理方法,从而实现通信。

常用的四个方法:

  • alert: 可以被 webview 的 onJsAlert 监听
  • confirm: 可以被 webview 的 onJsConfirm 监听
  • prompt: 可以被 webview 的 onJsPrompt 监听

简单拿 prompt 来举例说明,Web 页面通过调用 prompt()方法,安卓客户端通过监听onJsPrompt事件,拦截传入的参数,如果参数符合一定协议规范,那么就解析参数,扔给后续的 Java 去处理。这种协议规范,最好是跟 iOS 的协议规范一样,这样跨端调起协议是一致的,但具体实现不一样而已。比如:jack://utils/${action}?a=a 这样的协议,而其他格式的 prompt 参数,是不会监听的,即除了 jack://utils/${action}?a=a 这样的规范协议,prompt 还是原来的 prompt。

但这几种方法在实际的使用中有利有弊,但由于prompt是几个里面唯一可以自定义返回值,可以做同步交互的,所以在目前的使用中,prompt是使用的最多的。

URL Schema

schema 是 URI 的一种格式,上文提到的jack://utils/${action}?a=a 就是一个 scheme 协议,这里说的 scheme(或者 schema)泛指安卓和 iOS 的 schema 协议,因为它比较通用。

安卓和 iOS 都可以通过拦截跳转页 URL 请求,然后解析这个 scheme 协议,符合约定规则的就给到对应的 Native 方法去处理。

安卓和 iOS 分别用于拦截 URL 请求的方法是:

  • android:shouldOverrideUrlLoading方法
  • iOS:UIWebView 的delegate函数

这里简单看一个之前项目中对于 schema 封装:

// 调用
window.fsInvoke.share({title: 'xxx', content: 'xxx'}, result => {
    if (result.errno === 0) {
        alert('分享成功')
    } else {
        // 分享失败
        alert(result.message)
    }
)

---------------------------下方为对fsInvoke的封装

(function(window, undefined) {
    // 分享
    invokeShare = (data, callback) => {
        _invoke('share', data, callback)
    }

    // 登录
    invokeLogin = (data, callback) => {
        _invoke('login', data, callback)
    }

    // 打开扫一扫
    invokeScan = (data, callback) => {
        _invoke('scan', data, callback)
    }

    _invoke = (action, data, callback) => {
        // 拼接schema协议
        let schema = `jack://utils/${action}?a=a`;
        Object.keys(data).forEach(key => {
            schema += `&${key}=${data[key]}`
        })

        // 处理callback
        let callbackName = '';
        if(typeof callback === 'string) {
            callbackName = callback
        } else {
            callbackName = action + Date.now();
            window[callbackName] = callback;
        }

        schema += `&callback=${callbackName}`

        // 触发
        let iframe = document.createElement('iframe');
        iframe.style.display = 'none';
        iframe.src = schema;
        let body = document.body;
        body.appendChild(iframe);
        setTimeout(function() {
            body.removeChild(iframe);
            iframe = null;
        })
    }

    // 暴露给全局
    window.fsInvoke = {
        share: invokeShare,
        login: invokeLogin,
        scan: invokeScan
    }
})(window)

说完了 JS 主动通知客户端(Native)的方式,下面让我们来看下客户端(Native)主动通知调用 JS。

客户端(Native)通知 JS

loadUrl

在安卓 4.4 以前是没有 evaluatingJavaScript API 的,只能通过 loadUrl 来调用 JS 方法,只能让某个 JS 方法执行,但是无法获取该方法的返回值。这时我们需要使用前面提到的 prompt 方法进行兼容,让 H5 端 通过 prompt 进行数据的发送,客户端进行拦截并获取数据。

// mWebView = new WebView(this); //即当前webview对象
mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')");

//ui线程中运行
 runOnUiThread(new Runnable() {
        @Override
        public void run() {
            mWebView.loadUrl("javascript: 方法名('参数,需要转为字符串')");
            Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();
        }
});

evaluatingJavaScript

在安卓 4.4 之后,evaluatingJavaScript 是一个非常普遍的调用方式。通过 evaluateJavascript 异步调用 JS 方法,并且能在 onReceiveValue 中拿到返回值。

//异步执行JS代码,并获取返回值
mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
    		//这里的value即为对应JS方法的返回值
        }
});

stringByEvaluatingJavaScriptFromString

在 iOS 中 Native 通过stringByEvaluatingJavaScriptFromString调用 Html 绑定在 window 上的函数。

// Swift
webview.stringByEvaluatingJavaScriptFromString("方法名('参数')")
// oc
[webView stringByEvaluatingJavaScriptFromString:@"方法名(参数);"];

总结

看完本篇文章,相信你对 Hybrid 有了一个初步的了解。虽然本篇比较基础,但是只有了解了最本质的底层原理后,才能对现有的解决方案有一个很好的理解,你也可以去打造适合你和团队的Hybrid方案。当然了,后面会有对于 Hybrid 更深入的探讨,敬请期待哦!!

最后

你可以关注我的同名公众号【前端森林】,这里我会定期发一些大前端相关的前沿文章和日常开发过程中的实战总结。当然,我也是开源社区的积极贡献者,github地址https://github.com/Jack-cool,欢迎star!!!