前言

本文介绍quick hybrid框架的核心JSBridge的实现

由于在最新版本中,已经没有考虑iOS7等低版本,因此在选用方案时没有采用url scheme方式,而是直接基于WKWebView实现

交互原理

具体H5和Native的交互原理可以参考前文的H5和Native交互原理

交互原理图如下:

预计的最终效果

如果一步一步来分析,最后再看效果,可能会很枯燥,甚至还有点化简为繁的样子。(感觉直接看代码应该是最简单的,奈何每次写成文章时都得加一大堆的描述)

因此,先来看看最终完成后应该是什么样的。

// 调用ui中alert的示例
callHandler({
// 模块名,本文中的API划分了模块
module: 'ui',
// 方法名
name: 'alert',
// 需要传递给native的请求参数
data: {
message: 'hello',
},
callback: function(res) {
/**
* 调用后的回调,接收原生传递的回调数据
* alert如果成功,可以点击后再回调
{
// 1成功/0失败
code: 1,
message: '描述',
// 数据
data: {},
}
*/
}
});

架构

从头开始实现一个JSBridge,很容易两眼一抹黑,无从下手。

因此我们需要先从大方向上把功能交互确定好,然后再开始构建细节,编码实现

功能分析与确认

根据核心架构,规划需要实现的功能:

  • H5桥接对象的设计(JSBridge)

    • 短期回调池,需自动回收

    • 长期回调池,可多次使用

    • 调用Native方法的通道,桥接对象上原生注册的接收方法

    • 接收Native调用的通道,桥接对象上H5注册的接收方法

    • H5可以注册主动给原生调用的方法

  • 原生桥接对象的设计

    • 长期方法池,每一个长期调用都会存储在回调池中,可以多次使用

    • 短期立即执行,每一个短期调用都是立即执行

    • 调用H5方法的通道,桥接对象上H5注册的接收方法

    • 接收H5调用的通道,桥接对象上原生注册的接收方法,底层自动解析,然后执行对应API

    • 回调对象,底层基于调用H5的通道,每次执行完毕后都通过回调对象回调给H5

    • 主动调用H5,不同于回调对象只能被动响应,这个可以主动调用H5中注册的方法

  • API的设计

    • H5中的API,供前端调用,底层通过调用Native方法的通道,然后将预处理后的参数发送给原生

    • Native中的API,真正的功能实现

接下来就是JSBridge的实现

全局通信对象的确认

最重要的,是先把H5和Native通信时的几个全局桥接对象确定:

  • JSBridge,H5端的桥接对象,对象中绑定了接收原生调用的方法_handleMessageFromNative,以及内部有对回调函数等进行管理

  • webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage,iOS端的桥接对象,这个方法接收H5的调用

  • prompt,Android端的桥接对象,为了方便,直接重写了WebChromeClient中的onJsPrompt

// H5端的内部逻辑处理
window.JSBridge = {...} // 接收原生的调用,有回调以及主动调用两种
JSBridge._handleMessageFromNative = function() {...}
// H5主动调用原生
if (os.ios) {
// ios采用
window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(...);
} else {
window.top.prompt(...);
}

JSBridge对象的实现

H5就依靠这个对象与Native通信,这里仅介绍核心的逻辑

JSBridge = {
// 本地注册的方法集合,原生只能主动调用本地注册的方法
messageHandlers: {},
// 短期回调函数集合,在原生调用完对应的方法后会自动删除回收
responseCallbacks: {},
// 长期存在的回调集合,可以多次调用
responseCallbacksLongTerm: {}, _handleMessageFromNative: function(messageJSON) {
// 内部的处理: /**
如果是回调函数:
如果是短期回调responseCallbacks中查询回调id,并执行,执行后自动销毁
如果是短期回调responseCallbacksLongTerm中查询回调id,并执行
*/ /**
如果是Native的主动调用:
去本地注册的方法池messageHandlers中搜索,并执行
*/
}, callHandler: function(...) {
// 底层分别调用Android或iOS的原生接收方法 // 如果是短期回调,会将回调添加到responseCallbacks中
// 如果是长期回调,会将回调添加到responseCallbacksLongTerm中 // 省略若干逻辑
... if (os.ios) {
// ios采用
window.webkit.messageHandlers.WKWebViewJavascriptBridge.postMessage(...);
} else {
window.top.prompt(...);
}
}, registerHandler: function(handlerName, handler) {
// H5在本地注册可供原生调用的方法
}, ...
};

Android中桥接对象的实现

Android中的核心就是JSBridge,其余都是围绕这个来的,以下是伪代码,列举主要的逻辑

public class JSBridge {
// 缓存所有的API模块(注册时添加进去)
static exposedAPIModlues = new HashMap<>(); static register(String apiModelName, Class<? extends IBridgeImpl> clazz) {
// 注册时会自动寻找所有的框架API模块,然后添加到缓存exposedAPIModlues,每一个模块中可以有若干API
// 每一个模块都需要实现IBridgeImpl接口
...
} static callAPI(...) {
// 首先会解析参数(H5中传递的),解析出调用了哪一个API,传递了些什么,解析结果包括如下
// port:H5传递的回调id,是responseCallbacks或responseCallbacksLongTerm中的key
// moduleName:调用的API的模块名,用来检索exposedAPIModlues中注册的模块
// name:调用的API的方法名,在对于找到的模块中去查找API
// 其他:包括传递的参数等等 // 然后会根据H5的回调端口号,生成一个回调对象(用来回调通知H5)
Callback callback = new Callback(port); // 之后,根据解析的参数寻找API方法
// java.lang.reflect.Method;
Method method = searchMethodBy(moduleName, name); // 没有找到方法会回调对于错误信息
// 否则执行对于的method,传递解析出的参数
// 并且在method内部执行完毕后主动回调给H5对于信息
method.invoke(..., callback);
}
}

callback类伪代码如下:

public class Callback {
apply(...) {
// 先解析拼装参数,然后将参数组装成javascript代码,参数中包含Callback对于的port值(回调id)
...
String js = javascript:JSBridge._handleMessageFromNative(对于的json参数); callJS(js);
}
callHandler(...) {
// 主动调用H5,封装的参数中不再是回调id,而是handleName
...
callJS(js);
}
callJS(js) {
// 底层通过loadUrl执行
...
webviewContext.loadUrl(js);
}
}

IBridgeImpl接口是空的,只是一个抽象定义,以下以某个实现这个接口的API为例

// 为了清晰,以ui.alert为例
public class xxxApi implements IBridgeImpl {
// 定义一个注册的模块别名,方便查找,譬如ui
static RegisterName = "ui"; // 模块中的某个API,譬如alert
public static void alert(..., Callback callback) {
// 接下来就是在这个API中实现对于的逻辑
...
// 最后,通过触发callback通知H5即可
callback.apply(...);
}
}

最后可以看到,在webview中,重新了WebChromeClientonJsPrompt来接收H5的调用

并且在webview加载时就会调用JSBridgeregister

public class XXXWebChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(..., JsPromptResult result) {
// 内部触发JSBridge.callJava
result.confirm(JSBridge.callJava(...));
return true;
}
}

以上几个就是Andorid中JSBridge核心实现,其他的如长期回调,短期回调,细节实现等优化不是核心逻辑,就列举,详情可以参考最后的源码

iOS中桥接对象的实现

这里仍然是OC实现的,主要参考的marcuswestin/WebViewJavascriptBridge实现

核心仍然是WKWebViewJavascriptBridge,其余一切都是通过它来分发代理

@implementation WKWebViewJavascriptBridge {
// 内部基于一个WebViewJavascriptBridgeBase基类(基类中定义交互方法)
WebViewJavascriptBridgeBase *_base;
}
/**
* API
*/
- (void)callHandler:(NSString *)handlerName data:(id)data {
// 主动调用H5的方法
// 底层调用_base的sendData,发送数据给H5
} - (void)registerModuleFrameAPI {
// 注册模块API,模块用到了别名代理
[self registerHandlersWithClassName:@"UIApi" moduleName:@"ui"]; // 其中registerHandlersWithClassName就是将模块示例化注册到全局中的作用,不赘述
} - (void)excuteMessage:(NSString *)message {
// 内部执行API的实现,这里会解析API解析出来的数据,如
// module.name,port(callbackid)等
...
// 然后底层调用_base的excuteMsg(它内部会根据注册的API,找到相对应的,然后执行原生功能,最后通过回调通知H5)
} #pragma mark - WKScriptMessageHandler其实就是一个遵循的协议,它能让网页通过JS把消息发送给OC
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// 监听到对于API调用时,底层会调用excuteMessage
if ([message.name isEqualToString:@"WKWebViewJavascriptBridge"]) {
[self excuteMessage:message.body];
}
}

然后看看它基类WebViewJavascriptBridgeBase的实现

@implementation WebViewJavascriptBridgeBase

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
// 底层将接收到的数据组装成js代码执行
...
NSString* javascriptCommand = [NSString stringWithFormat:@"JSBridge._handleMessageFromNative('%@');", messageJSON]; [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
} - (void)excuteMsg:(NSString *)messageQueueString moduleName:(NSString *)moduleName {
// 底层根据对于的模块,API名,找到注册的handler
... // 然后创建一个回调对象
WVJBResponseCallback responseCallback = (通过sendData通知H5回调数据); // 然后执行这个handler
handler(message[@"data"], responseCallback);
}

接下来是API的定义

定义API模块之前,需要先了解RegisterBaseClass,所有模块必须实现的基类,定义了如何注册

@implementation RegisterBaseClass
#pragma mark - 注册api的统一方法
- (void)registerHandlers {
// 子类重写改方法实现自定义API注册
} #pragma mark - handler存取
- (void)registerHandlerName:(NSString *)handleName
handler:(WVJBHandler)handler {
// 注册某个模块下的某个API
} - (WVJBHandler)handler:(NSString *)handlerName {
// 通过名称获取对应的API
}

要定义一个API模块,则需继承RegisterBaseClass然后重写registerHandlers(为了清晰,以ui.alert为例)

@implementation UIApi
- (void)registerHandlers {
[self registerHandlerName:@"alert" handler:^(id data, WVJBResponseCallback responseCallback) {
// 同样,在接收到数据,并处理后,通过responseCallback通知H5
...
responseCallback(...);
}
}

webview加载时就会调用WKWebViewJavascriptBridgeregisterModuleFrameAPI,对于模块名ui与别名UIApi,可以在注册时看到,它们之间是有一一对应关系的

然后在webview创建时,会进行监听,userContentController

WKWebViewConfiguration * webConfig = [[WKWebViewConfiguration alloc] init];
WKUserContentController * userContentVC = [[WKUserContentController alloc] init];
webConfig.userContentController = userContentVC;
WKWebView * wk = [[WKWebView alloc] initWithFrame: CGRectZero configuration: webConfig]; self.wv = wk;
... // 代理
self.bridge = [WKWebViewJavascriptBridge bridgeForWebView: self.wv];
[self.bridge setWebViewDelegate: self]; // 添加供js调用oc的桥梁。这里的name对应WKScriptMessage中的name,多数情况下我们认为它就是方法名。
[self.wv.configuration.userContentController addScriptMessageHandler: self.bridge name: @"WKWebViewJavascriptBridge"];

同样,iOS中的长期回调等其它一些非核心内容也暂时隐藏了

API的设计

按照上述的实现,可以构建出一个完整的JSBridge交互流程,H5和Native的交互已经通了

接下来就是设计API真正给外界调用

准确的来说,API的设计已经脱离了JSBridge交互内容,属于混合框架框架应用层次,因此后续会有单独的章节介绍quick hybrid中的API

API如何实现?可以参考上文中Android的继承IBridgeImpl法以及iOS的继承RegisterBaseClass然后重写registerHandlers

至于该规划些什么API,这与实际的需求有关,不过一般情况下,像ui.alert等等一般都是必须的

更多详情请待后续章节

结束语

最后再来一张图巩固下把

至此,整个JSBridge交互就已经完成了

其实在总结文章时,考虑过很多种形式,发现,

如果是全文字描述,十分枯燥,很难坚持读下来,

如果是各种原理都用绘图+描述,发现会化简为繁,硬生生把难度提高了几个level,

所以最终采用的是伪代码(半伪半真)展示形式(剔除一些无效信息,提取关键,而且还不和最终的代码冲突)

虽然说,这整套流程都没有特别难的地方,涉及的知识点都不是特别深。但是却包含了前端,Android,iOS三个领域。

因此如果要将整套工作做的比较好的化最好还是有分工的好,比较一个人的精力有限,真正专精多个领域的人还是比较少的,

而且后续各个优化的内容也不少(API,优化,等等...)

返回根目录

源码

github上这个框架的实现

quickhybrid/quickhybrid

附录

参考资料

【quickhybrid】JSBridge的实现的更多相关文章

  1. 【quickhybrid】iOS端的项目实现

    前言 18年元旦三天内和朋友突击了下,勉强是将雏形做出来了,后续的API慢慢完善.(当然了,主力还是那个朋友,本人只是初涉iOS,勉强能看懂,修修改改而已) 大致内容如下: JSBridge核心交互部 ...

  2. quickhybrid】如何实现一个Hybrid框架

    章节目录 [quickhybrid]如何实现一个跨平台Hybrid框架 [quick hybrid]架构一个Hybrid框架 [quick hybrid]H5和Native交互原理 [quick hy ...

  3. 【quickhybrid】H5和Native交互原理

    前言 Hybrid架构的核心就是JSBridge交互,而实现这个交互的前提是弄清楚H5和Native端的交互 本文主要介绍Native端(Android/iOS)和H5端(泛指前端)的交互原理 (之前 ...

  4. 【quickhybrid】H5和原生的职责划分

    前言 在JSBridge实现后,前端网页与原生的交互已经通了,接下来就要开始规划API,明确需要提供哪一些功能来供前端调用. 但是在这之前,还有一点重要工作需要做: 明确H5与Native的职责划分, ...

  5. 【quickhybrid】API的分类:短期API、长期API

    前言 一切就绪,开始规划API,这里在规划前对API进行了一次分类:短期API.长期API 首先申明下,这个是在实际框架演变过程中自创的一个概念,其它混合框架可能也会有这个概念,但应该是会在原生底层来 ...

  6. 【quickhybrid】架构一个Hybrid框架

    前言 虽然说本系列中架构篇是第一章,但实际过程中是在慢慢演化的第二版中才有这个概念, 经过不断的迭代,演化才逐步稳定 明确目标 首先明确需要做成一个什么样的框架? 大致就是: 一套API规范(统一An ...

  7. 【quickhybrid】如何实现一个Hybrid框架

    章节目录 [quickhybrid]如何实现一个跨平台Hybrid框架 [quick hybrid]架构一个Hybrid框架 [quick hybrid]H5和Native交互原理 [quick hy ...

  8. 【quickhybrid】Android端的项目实现

    前言 前文中就有提到,Hybrid模式的核心就是在原生,而本文就以此项目的Android部分为例介绍Android部分的实现. 提示,由于各种各样的原因,本项目中的Android容器确保核心交互以及部 ...

  9. 【quickhybrid】JS端的项目实现

    前言 API实现阶段之JS端的实现,重点描述这个项目的JS端都有些什么内容,是如何实现的. 不同于一般混合框架的只包含JSBridge部分的前端实现,本框架的前端实现包括JSBridge部分.多平台支 ...

随机推荐

  1. NHibernate Criteria中 Restriction与Expression的差别

    http://stackoverflow.com/questions/5483393/nhibernate-criteria-restriction-vs-expression 据说是Restrict ...

  2. Linux 文件系统模型

    声明:本文仅限于 cnblogs 发布,其他第三方网站均为盗版,原文地址:Linux 文件系统模型 在 Linux 环境下有过一些经历的同学可能都会遇到一个问题,这个问题就是往机器上插入 U盘 或者其 ...

  3. mapbox-gl象形文字字体glyph生成

    简介 mapbox-gl可以对文字显示各种字体(依赖ttf文件),内部采用的是读取protobuf文件 环境条件 硬件:mac.网络 软件:nodejs.npm 创建mapbox-gl可用的字体pro ...

  4. Java中Httpsession是如何实现的?

    HTTP协议(http://www.w3.org/Protocols/)是“一次性单向”协议. 服务端不能主动连接客户端,只能被动等待并答复客户端请求.客户端连接服务端,发出一个HTTP Reques ...

  5. 修改 Sublime 按快捷键 ctrl+s 自动格式化(reindent lines)的问题

    Sublime 工具自带代码格式化的功能,但在某些场景下格式化代码后并不是我们想要的代码格式,且是点击保存ctrl+s才触发的格式代码事件,so,为关闭点击ctrl+s格式代码,我们需要改命令 sav ...

  6. iOS多线程--深度解析

    多线程 你们项目中为什么多线程用GCD而不用NSOperation呢? 你有没有发现国外的大牛他们多线程都是用NSOperation? 你能告诉我他们这样做的理由吗? 关系: ①:先搞清两者的关系,N ...

  7. webpack 3.X学习之JS压缩与打包HTML文件

    js压缩 webpack自带一个插件uglifyjs-webpack-plugin来压缩js,所以不需要再次安装,当一切都准备妥当,引入uglifyjs-webpack-plugin模块: const ...

  8. CSS常见布局解决方案

    最近要准备移动端项目,大半年没好好写过CSS了,今天恶补了一下CSS的一些布局,下面做一些分享. 水平居中布局 1.margin + 定宽 <div class="parent&quo ...

  9. parquet列式文件实战

    前言 列式文件,顾名思义就是按列存储到文件,和行式存储文件对应.保证了一列在一个文件中是连续的.下面从parquet常见术语,核心schema和文件结构来深入理解.最后通过java api完成writ ...

  10. RAC环境下误操作将数据文件添加到本地存储

    今天碰到个有意思的事情,有客户在Oracle RAC环境,误操作将新增的数据文件直接创建到了其中一个节点的本地存储上. 发现网上去搜的话这种问题还真不少,对应解决方案也各式各样,客户问我选择哪种方案可 ...