由于Webview内嵌H5的性能/功能各种受限,于是有了各种的混合开发解决方案,例如Hybrid、RN、WEEX、Flutter、小程序、快应用等等。
React Native 至今没有推出1.0版本,由于各种可能的坑,一些hold不住的团队可能会放弃。
Flutter 是否可替代RN,真正实现两端统一,拭目以待,他从头到尾重写一套跨平台的UI框架,包括UI控件、渲染逻辑甚至开发语言。我本人之后会关注学习一下。
小程序 不用说太多了,大家都很熟悉了;微信、支付宝、百度都在用。除了第一次需要花点时间下载,体验上可以说是很不错了,但是封闭性是他很大的一个缺点。
快应用 目标是很好的,统一API,但是还是要看各厂家的执行力度。
现在来总结一下我们团队目前使用的Hybrid方案。算是回顾一下,巩固基础,好记性不如烂笔头。
一、Hybrid简介
Hybrid可以说是上面提到的几种里最古老,最成熟的解决方案了。
缺点是明显的:H5有的缺点他几乎都有,比如性能差、JS执行效率低等等。
但是优点也很显著:随时发版,不受应用市场审核限制(当然这个前提是Hybrid对应Native的功能都已准备就绪);拥有几乎和Native一样的能力,eg:拍照、存储、加日历等等...
基本原理
Hybrid利用JSBridge进行通信的基本原理网上一搜一大把,简单记录一下。
Native => JS
两端都有现成方法。谁让都在别人的地盘下面玩呢,Native当然有办法来执行JS方法。
iOS
// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];
Android
mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//这里的value即为对应JS方法的返回值
}
});
JS => Native
对于Webview中发起的网络请求,Native都有能力去捕获/截取/干预。所以JSBridge的核心就是设计一套url方案,让Native可以识别,从而做出响应,执行对应的操作就完事。
例如,正常的网络请求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png
我们可以自定义协议,改成jsbridge://methodName?param1=value1¶m2=value2
。
Native拦截jsbridge
开头的网络请求,做出对应的动作。
最常见的做法就是创建一个隐藏的iframe来实现通信。
二、现成的解决方案
iOS WebViewJavascriptBridge
Android JsBridge
基本原理都相同,项目的设计就决定了一个它的可扩展性&可维护性。良好的可扩展性&可维护性对于JSBridge尤为重要,他是后面一切业务的基石。
基础库简析
(下面都以Android为例)
1、 初始化
类似写普通H5页面需要监听DOMContentLoaded
或者onLoad
来决定开始执行脚本一样,JSBridge需要一个契机去告诉JS,我准备好了,你可以来调用我的方法了。
[前端] 执行监听 && 检测
if (window.WebViewJavascriptBridge) {
//do your work here
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
//do your work here
},
false
);
}
[Native (埋在端里的JS)] dispatchEvent
触发
var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
init: init,
send: send,
registerHandler: registerHandler,
callHandler: callHandler,
_fetchQueue: _fetchQueue,
_handleMessageFromNative: _handleMessageFromNative
};
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('WebViewJavascriptBridgeReady');
readyEvent.bridge = WebViewJavascriptBridge;
doc.dispatchEvent(readyEvent);
2、JS调Native方法
先上代码,下面是埋在端内的,JSBridge.callHandler,用来实现JS调用Native。
// 调用线程
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
//sendMessage add message, 触发native处理 sendMessage
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
jsbridge.callHandler
是JS调Native方法的核心。
handlerName
是前端与Native协商好的方法名称
data
参数
responseCallback
回调
回调函数绑在了一个内部对象中var responseCallbacks = {}
,发送给Native的消息message中只包含了这个回调函数对应的id,端上处理完成之后触发&销毁。
这个方法并不直接把消息全部推送走,而是存在一个队列中sendMessageQueue
。同时通知Native,有新数据(message)需要处理。即上面代码的最后一行,他利用iframe的src通知端上的信息如下:
var CUSTOM_PROTOCOL_SCHEME = 'sn'
var QUEUE_HAS_MESSAGE = '__sn__queue_message__'
上面提到的,JS只是通知了端上有新消息,Native调用获取时机暂时不考虑,就假设他收到一条就处理一次,极端高频情况下,两三条处理一次。Native通过_fetchQueue
统一处理存储在sendMessageQueue
中的数据:
// 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//android can't read directly the return data, so we can reload iframe src to communicate with java
if (messageQueueString !== '[]') {
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
}
这些基本就是JS主动调用Native的流程,关于回调方法,下面统一说。
3、Native调JS方法
虽说Native可以随意执行JS,但是总是需要知道哪些JS方法是可执行的吧。registerHandler
就是用来执行注册。
registerHandler
在Native端定义(是JSBridge对象的一个方法),由前端来注册。
// 注册线程 往数组里面添加值
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
Native主动调用。
Native主动调用分两种情况,1是Native主动触发前端事件,例如通知前端页面可视状态变化。2是前端调用Native的回调。JSBridge是天生异步的,所以回调和主动调用归结到一类里面了。
如果是前端主动调用的方法,有responseId,即有回调,直接调用执行即可。
否则就去注册的messageHandlers中寻找方法,调用。
//提供给native使用,
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
//java call finished, now need to call js callback function
// 前端主动调用的Callback
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
// Native主动调用
//直接发送
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({
responseId: callbackResponseId,
responseData: responseData
});
};
}
var handler = WebViewJavascriptBridge._messageHandler;
if (message.handlerName) {
handler = messageHandlers[message.handlerName];
}
//查找指定handler
try {
handler(message.data, responseCallback);
} catch (exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
}
}
}
});
}
代码分析基本就到这里,盗一张图(地址放在最后了),把流程都画了出来,个人感觉没啥问题
三、业务封装
直接使用前面的库可以完成功能,但是不够优雅,代码不经过良好的设计可能会变得牵一发动全身,可维护性差。下面说说我们的设计,可能不是最好的,但是是很符合我们业务场景的。
- 事件基础类 EventClass
处理事件广播、订阅。 - 连接基础类 ConnectClass
/**
* 创建和获取 jsbridge 基础类
* @class ConnectClass
* @extends EventsClass
*/
class ConnectClass extends EventsClass {
/**
* 获取jsbridge实例,注入到sncClass上的bridge属性 `this.bridge`
*/
connect() {
// 事件广播,通知开始建立连接,统计使用
// 建立JSBridge
// 建立JSBridge.then 1.注册Native主动调用的事件,对应上面的bridge.registerHandler;2.广播 建立完成,统计使用
}
// ... 其他的一些方法
// eg: 分平台初始化JSBridge,处理差异性
// eg: bridge.registerHandler 回调的封装一层的统一处理函数
}
关于注册Native主动调用的事件(和下面会提到的JS主动调用事件),实现插件化,并同一封装。好处是可以明确代码执行步骤、方便业务同学调试(这不是我的锅,我已经执行调用了...)、方便性能统计。
- 业务类
class SncClass extends ConnectClass {
constructor(option){
// 监听connect,监听首屏数据
// 建立连接 this.connect
// 挂载必备API
}
// 初始化,根据参数决定挂载哪些api
init(apis){
this.mountApi(apis);
}
/**
* 挂载 api
* @param {Object} apis api 对象集合
*/
mountApi(apis) {
// 1. 错误处理
// 2. 检测是否已经jsb建立连接 已连接则 直接执行真正挂载函数 return
// 3. bridge 未初始化时,定义方法预声明。执行的方法将会被储存在缓存队列里在 bridge 初始化后调用
// 4. 监听连接事件,执行真正挂载 loadMethods
}
}
/**
* 加载 API 到实例属性,标志着 api 的真正挂载
*/
loadMethods(apis) {
// 1. 防止重复挂载 api,
// 2. 给插件初始化方法注入ctx,让插件得以调用库内真正的初始化函数,即封装一层的上面提到的 callHandler
}
// ... 其他实例方法,比如 extend,得以在业务中和Native互相约定新的非通用JSB,方便扩展
- 初始化
导出单例appSNC,拥有的方法都在appApis中定义,如果有新的业务需求直接扩展此文件夹中内容即可。
import * as apis from '../appApis'; // 方法集合
import SNC from './sdk'; // 上面的 SncClass
const option = {} ; // 一些配置
const appSNC = new SNC(option);
export default appSNC.init(apis);
以上就是我们正在使用的方案,总结一下,不断积累。