一:前言
作为前端搬砖人员,我们在工作中经常会遇到这样的项目,就是写一个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
基本原理如图(盗用下文引用链接文中图):
ios应用和应用之间,消息是不互通,各自的信息只在app内部流转,为了便于两个app之间的消息交互话流转,系统提供了app之间的通讯协议Url Schema。应用通过注册Url Schema,允许其它应用通过Url Schema拉起其它程序,并将参数传入给其它参数,提高了app之间的交互性和联动性能。
2:H5直接与Native交互
参考链接
五: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,目录结构
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,统一管理。
参考链接: