Web前端开发需要知道的JSBridge知识

494 阅读6分钟

一:前言

作为前端搬砖人员,我们在工作中经常会遇到这样的项目,就是写一个H5的项目,需要被不同的app镶嵌使用,即Hybrid混合开发。其中就免不了h5页面和app的通信,即JSBridge。在以往我都是只知道怎么用,照着项目中前辈的案例依葫芦画瓢,没有去了解其中的原理,故现写下此篇文章记录一下。

二:背景

现如今移动端app盛行,最开始的时候,移动端开发技术分为原生Native(即IOS,Android)和Web H5。

它们都有着各自的优缺点:

原生Native

优点:性能较高,功能覆盖率更高。

缺点:开发效率较低,开发完成需要重新打包整个App,发布依赖用户的更新,开发迭代比较麻烦比较慢。

Web H5

优点:可以更好的实现发布更新,跨平台也更加优秀。

缺点:性能较低,特性受限。

HyBrid

于是HyBrid混合开发诞生了,是一种同时使用了前端web技术(js,css,html)和原生native技术(java,kotlin,swfit,object-c)进行开发的移动应用。

它吸收了两者开发的优点:开发快,易更新,开发周期短,跨平台。

相对应的也有其缺点:性能问题,兼容性问题。

Hybrid架构的核心就是JSBridge交互

其中web和原生互相通信就要依赖JSBridge技术。

在说JSBridge之前,我们先简单的认识一下webView。

三:什么是webView?

WebView 是移动端提供的运行JavaScript的环境,是系统渲染 Web 网页的一个控件,可与页面JavaScript交互,实现混合开发。

简单来说,WebView 是手机中内置了一款高性能 Webkit 内核浏览器,在 SDK 中封装的一个组件。不过没有提供地址栏和导航栏,只是单纯的展示一个网页界面。

可以理解为一个加载呈现页面的容器。

PS:本人是H5前端开发,不会IOS和安卓,文章所有的内容都是站在H5页面开发的角度理解和书写。

四:Native与H5交互的两个方式?

1:Url Scheme

基本原理如图(盗用下文引用链接文中图):

一.png

Url Schema科普

ios应用和应用之间,消息是不互通,各自的信息只在app内部流转,为了便于两个app之间的消息交互话流转,系统提供了app之间的通讯协议Url Schema。应用通过注册Url Schema,允许其它应用通过Url Schema拉起其它程序,并将参数传入给其它参数,提高了app之间的交互性和联动性能。

2:H5直接与Native交互

参考链接

Native与H5交互原理

五:JSBridge

JSBridge是什么?

JSBridge:以 JavaScript 引擎或 Webview 容器作为媒介,通过协定协议进行通信,实现 Native 端和 Web 端双向通信的一种机制。

所谓双向通信的通道:

1,JS 向 Native 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等。

2,Native 向 JS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。

在实际开发中的运用场景有:

1,打开新的webView加载H5

2,回退

3,调用客户端的能力:地址位置、摄像头,查看本地相册,指纹支付,打开二维码扫描等

4,跳转客户端模块

5,获取用户/app信息

JSBridge的实现原理?

JavaScript 是运行在一个单独的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。由于这些 Context 与原生运行环境的天然隔离,我们可以将这种情况与 RPC(Remote Procedure Call,远程过程调用)通信进行类比,将 Native 与 JavaScript 的每次互相调用看做一次 RPC 调用。

在 JSBridge 的设计中,可以把前端看做 RPC 的客户端,把 Native 端看做 RPC 的服务器端,从而 JSBridge 要实现的主要逻辑就出现了:通信调用(Native 与 JS 通信) 和句柄解析调用。

JSBridge通信实现

实现JSBridge主要是亮点:

1,将Native端原生接口封装成JavaScript接口

2,将Web端JavaScript接口封装成原生接口

1:Native调用JS

由于是H5通过原生端的webView加载,Native端可以直接执行JS里面定义的全局方法

window.nativeCallBack = function(data){
    console.log('data', data)
}

Native可以直接调用nativeCallBack(123),将123传给JS

2:JS调用Native

1,拦截Url Schema

H5端和JS端约定一个特定的URL Schema(一般包括数据data和方法名),然后H5端使用iframe或者window.location.href发送网络请求,Native端做拦截。

var messagingIframe = document.createElement('iframe');

messagingIframe.style.display = 'none';

document.documentElement.appendChild(messagingIframe);

//触发scheme

messagingIframe.src = url;

或者

window.location.href = url

2,API注入

Native可以给webview注入全局变量并挂载在window对象上,这样前端js就可以通过window上全局对象方法来调用一些native的方法。

window.NativeApi.xxx(data)

六:案例(H5端视角)

上代码之前,说一下我负责的项目现状: 我负责的模块是一个基础公共功能,例如订单,钱包。这种功能公司各个app都需要用到,所以我们的这一个H5页面,会被镶嵌到5-10个公司app里面,而有时候不同的app可能约定的会有一丢丢不同,所以在写JSBridge的时候,写了一个文件统一管理且写了一个入口统一处理。

1,目录结构

二.png

2,具体代码

A-app.js (与A app的jsBridge交互处理)

export const ANativeCall = (opt ={}) => {
    // 传入的参数名是和app端约定好的
    const { nativeModule, method, params } = opt
    return new Promise((resolve, reject) => {
        if(!nativeModule){
            reject('nativeModule is required')
        }
        // 处理回掉函数
        const cbName = params && params.callBack
        if(cbName){
            window[cbName] = function(res){
                // 处理app返回的结果
                // void 0 的意思就是undefined。一个函数如果没有return值, 那函数的返回值就是undefined
                if(res === void 0) res = {}
                if(res.code === 0 || res.code === '0'){
                    console.log(`A app 调用方法${cbName}正确返回参数:`)
                    console.log(res.data)
                    resolve(res.data)
                }else{
                    console.log(`A app 调用方法${cbName}错误返回参数`)
                    console.log(res)
                    reject(res)
                }
            }
        }
        // 拼接约定的链接
        const AStr = `ARoute://${nativeModule}/${method}?params=${encodeURIComponent(JSON.stringify(params))}`
        try{
            // 判断有没有全局挂载方法对象
            if(window.AApp){
                // app对象中全局挂载的触发方法
                window.AApp.CallNative(AStr)
            }else if(window.webkit && window.webkit.messageHandlers.AApp){
                // ios
                window.webkit.messageHandlers.AApp.postMessage(AStr)
            }else{
                console.warn('not support')
            }
        } catch(e){
            reject(e)
        }
    })
}
// 获取登陆信息
export const getAAppLoginInfo = () => {
    const params = {
        nativeModule: 'AUserInfoRoute',
        method: 'getLoginInfo',
        params: {
            callBack: 'getAAppLoginInfo'
        }
    }
    return ANativeCall(params)
}

// 开启新web容器
export const openWebVC = (url, hideNavigationBar = 'true', needLogin = 'true') => {
    const params = {
        nativeModule: 'ARoute',
        method: 'openWebVC',
        params: {
            url,
            hideNavigationBar,
            needLogin
        }
    }
    return ANativeCall(params)
}

B-app.js (与A app的jsBridge交互处理)

export const BNativeCall = (opt = {})=>{
    return new Promise((resolve, reject) => {
        // 传入的参数名是和app端约定好的
        const {CallBack, Parameters, MethodName} = opt
        // 方法名必须存在
        if(!MethodName){
            console.error('MethodName is require')
            reject(false)
            return
        }
        // 处理回掉函数
        let cb = CallBack ? CallBack : (Parameters&&Parameters.jsCallBack ?Parameters.jsCallBack:`${MethodName}CallBack`)

        window[cb] = function(res){
            console.log(`调用Bapp方法${cb}正确返回参数`)
            console.log(res)
            resolve(res)
            // 调用成功之后,删除掉全局挂载的方法,节省内存空间
            window[cb] = null
            delete window[cb]
        }
        try {
            // BApp端约定的全局挂载的对象方法,供H5页面调用app的方法
            window.BApp.CallNative(JSON.stringify(opt))
            if(!(CallBack || Parameters || Parameters.jsCallBack)){
                resolve(true)
                window[cb] = null
                delete window[cb]
            }
        } catch(e){
            reject(e.message)
        }
    })
}

// 其中的MethodName,CallBack,Parameters都是项目中app和H5双方约定好的
// 获取登陆信息
export const getLoginInfo = () => {
    const opt = {
        MethodName: 'getLoginInfo',
        CallBack: 'getLoginInfoCb',
        Parameters: {
            jsCallBack: 'getLoginInfoCb'
        }
    }
    return BNativeCall(opt)
}

// 打开新的webview
export const router = (Parameters) => {
    const opt = {
        MethodName: 'router',
        Parameters
    }
    return BNativeCall(opt)
}

// 返回个人中心
export const backToMainVC = () => {
    const opt = {
        MethodName: 'backToMainVC'  
    }
    return BNativeCall(opt)
}

index.js (统一处理入口)

/**
 * 入口:根据不同的webview环境,运用不用的方法调用sdk
 */
import * as ANative from './A-app'
import * as BNative from './B-app'
// 根据UA中的标示来识别是什么app,这里不细讲
import {isAApp, isBApp} from '@/helper'
// 定义一个初始化的空函数
export function promiseNoop(){
    return new Promise((resolve, reject) => {reject(new Error('not support method'))})
}
const translateAAppNative = (native) => {
    return {
        nativeCall: native.ANativeCall,
        getLoginInfo: native.getAAppLoginInfo,
        // 空函数
        backToMainVC: promiseNoop,
        // 直接在转换的时候写了
        router(Parameters = {}) {
            const { defaultUrl, needLogin, hideNavigationBar} = Parameters
            return native.openWebVC(defaultUrl, hideNavigationBar, needLogin)
        }
    }
}

const translateBAppNative = (native) => {
    return {
        nativeCall: native.BNativeCall,
        getLoginInfo: native.getLoginInfo,
        backToMainVC: native.backToMainVC,
        router: native.router
    }
}

export default (() => {
    if(isAApp){
        return translateAAppNative(ANative)
    }else if(isBApp){
        return translateBAppNative(BNative)
    }else{
        return {}
    }
})

页面中使用

<template>
    <div>页面使用简单案例</div>
</template>
<script>
import appNativeCall from './index'
export default {
    methods: {
        getLoginInfo(){
            if(!inApp) return Promise.reject('非app内环境')
            return appNativeCall.getLoginInfo().then((res) => {
                console.log('getLoginInfo', res)
            })
        }
    }
}
</script>

如果需要再接入其他的app,就可以新建一个C-app.js,统一管理。

参考链接:

JSBridge

前端也要懂系列之 JSBridge 原理解析

Hybrid APP基础篇(四)->JSBridge的原理

小白必看,JSBridge 初探