向在这次肺炎疫情中逝世的同胞默哀 |
本文首发于政采云前端团队博客:小白必看,JSBridge 初探javascript
近些年,移动端普及化愈来愈高,开发过程当中选用 Native 仍是 H5 一直是热门话题。Native 和 H5 都有着各自的优缺点,为了知足业务的须要,公司实际项目的开发过程当中每每会融合二者进行 Hybrid 开发。Native 和 H5 分处两地,看起来没法联系,那么如何才能让双方协同实现功能呢?前端
这时咱们想到了 Codova ,Codova 提供了一组与设备相关的 API ,是早期 JS 调用原生代码来实现原生功能的经常使用方案。不过 JSBridge 真正在国内普遍应用是因为移动互联网的盛行。java
JSBridge 是一种 JS 实现的 Bridge,链接着桥两端的 Native 和 H5。它在 APP 内方便地让 Native 调用 JS,JS 调用 Native ,是双向通讯的通道。JSBridge 主要提供了 JS 调用 Native 代码的能力,实现原生功能如查看本地相册、打开摄像头、指纹支付等。web
H5 与 Native 对比objective-c
name | H5 | Native |
---|---|---|
稳定性 | 调用系统浏览器内核,稳定性较差 | 使用原生内核,更加稳定 |
灵活性 | 版本迭代快,上线灵活 | 迭代慢,须要应用商店审核,上线速度受限制 |
受网速 影响 | 较大 | 较小 |
流畅度 | 有时加载慢,给用户“卡顿”的感受 | 加载速度快,更加流畅 |
用户体验 | 功能受浏览器限制,体验有时较差 | 原生系统 api 丰富,能实现的功能较多,体验较好 |
可移植性 | 兼容跨平台跨系统,如 PC 与 移动端,iOS 与 Android | 可移植性较低,对于 iOS 和 Android 须要维护两套代码 |
JS 调用 Native 的实现方式较多,主要有拦截 URL Scheme
、重写 prompt 、注入 API 等方法。npm
Android 和 iOS 均可以经过拦截 URL Scheme 并解析 Scheme 来决定是否进行对应的 Native 代码逻辑处理。json
Android 的话,Webview
提供了 shouldOverrideUrlLoading
方法来提供给 Native 拦截 H5 发送的 URL Scheme
请求。代码以下:swift
public class CustomWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
......
// 场景一: 拦截请求、接收 scheme
if (url.equals("xxx")) {
// handle
...
// callback
view.loadUrl("javascript:setAllContent(" + json + ");")
return true;
}
return super.shouldOverrideUrlLoading(url);
}
}
复制代码
iOS 的 WKWebview
能够根据拦截到的 URL Scheme
和对应的参数执行相关的操做。代码以下:api
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
if ([navigationAction.request.URL.absoluteString hasPrefix:@"xxx"]) {
[[UIApplication sharedApplication] openURL:navigationAction.request.URL];
}
decisionHandler(WKNavigationActionPolicyAllow);
}
复制代码
这种方法的优势是不存在漏洞问题、使用灵活,能够实现 H5 和 Native 页面的无缝切换。例如在某一页面须要快速上线的状况下,先开发出 H5 页面。某一连接填写的是 H5 连接,在对应的 Native 页面开发完成前先跳转至 H5 页面,待 Native 页面开发完后再进行拦截,跳转至 Native 页面,此时 H5 的连接无需进行修改。可是使用 iframe.src 来发送 URL Scheme
须要对 URL 的长度做控制,使用复杂,速度较慢。浏览器
Android 4.2 以前注入对象的接口是 addJavascriptInterface ,可是因为安全缘由慢慢不被使用。通常会经过修改浏览器的部分 Window 对象的方法来完成操做。主要是拦截 alert、confirm、prompt、console.log 四个方法,分别被 Webview
的 onJsAlert、onJsConfirm、onConsoleMessage、onJsPrompt 监听。其中 onJsPrompt 监听的代码以下:
public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {
String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue);
xxx;
return true;
}
复制代码
iOS 因为安全机制,WKWebView
对 alert、confirm、prompt 等方法作了拦截,若是经过此方式进行 Native 与 JS 交互,须要实现 WKWebView
的三个 WKUIDelegate
代理方法。代码示例以下:
-(void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:([UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}])];
[self presentViewController:alertController animated:YES completion:nil];
}
复制代码
使用该方式时,能够与 Android 和 iOS 约定好使用传参的格式,这样 H5 能够无需识别客户端,传入不一样参数直接调用 Native 便可。剩下的交给客户端本身去拦截相同的方法,识别相同的参数,进行本身的处理逻辑便可实现多端表现一致。如:
alert("肯定xxx?", "取消", "肯定", callback());
复制代码
另外,若是能与 Native 肯定好方法名、传参等调用的协议规范,这样其它格式的 prompt 等方法是不会被识别的,能起到隔离的做用。
基于 Webview
提供的能力,咱们能够向 Window 上注入对象或方法。JS 经过这个对象或方法进行调用时,执行对应的逻辑操做,能够直接调用 Native 的方法。使用该方式时,JS 须要等到 Native 执行完对应的逻辑后才能进行回调里面的操做。
Android 的 Webview
提供了 addJavascriptInterface 方法,支持 Android 4.2 及以上系统。
gpcWebView.addJavascriptInterface(new JavaScriptInterface(), 'nativeApiBridge');
public class JavaScriptInterface {
Context mContext;
JavaScriptInterface(Context c) {
mContext = c;
}
public void share(String webMessage){
// Native 逻辑
}
}
复制代码
JS 调用示例:
window.NativeApi.share(xxx);
复制代码
iOS 的 UIWebview
提供了 JavaScriptScore 方法,支持 iOS 7.0 及以上系统。WKWebview
提供了 window.webkit.messageHandlers 方法,支持 iOS 8.0 及以上系统。UIWebview
在几年前经常使用,目前已不常见。如下为建立 WKWebViewConfiguration
和 建立 WKWebView 示例:
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 40.0;
configuration.preferences = preferences;
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"share"];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"pickImage"];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"share"];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"pickImage"];
}
复制代码
JS 调用示例:
window.webkit.messageHandlers.share.postMessage(xxx);
复制代码
Native 调用 JS 比较简单,只要 H5 将 JS 方法暴露在 Window 上给 Native 调用便可。
Android 中主要有两种方式实现。在 4.4 之前,经过 loadUrl 方法,执行一段 JS 代码来实现。在 4.4 之后,可使用 evaluateJavascript 方法实现。loadUrl 方法使用起来方便简洁,可是效率低没法得到返回结果且调用的时候会刷新 WebView。evaluateJavascript 方法效率高获取返回值方便,调用时候不刷新WebView,可是只支持 Android 4.4+。相关代码以下:
webView.loadUrl("javascript:" + javaScriptString);
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value){
xxx
}
});
复制代码
iOS 在 WKWebview
中能够经过 evaluateJavaScript:javaScriptString 来实现,支持 iOS 8.0 及以上系统。
// swift
func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil)
// javaScriptString 须要调用的 JS 代码
// completionHandler 执行后的回调
复制代码
// objective-c
[jsContext evaluateJavaScript:@"ZcyJsBridge(ev, data)"]
复制代码
如何引用
由 H5 引用
在我司移动端初期版本时采用的是该方式,采用本地引入 npm 包的方式进行调用。这种方式能够肯定 JSBridge 是存在的,可直接调用 Native 方法。可是若是后期 Bridge 的实现方式改变,双方须要作更多的兼容,维护成本高
由 Native 注入
这是当前我司移动端选用的方式。在考虑到后期业务须要的状况下,进行了从新设计,选用 Native 注入的方式来引用 JSBridge。这样有利于保持 API 与 Native 的一致性,可是缺点是在 Native 注入的方法和时机都受限,JS 调用 Native 以前须要先判断 JSBridge 是否注入成功
使用规范
H5 调用 Native 方法的伪代码实例,如:
params = {
api_version: "xxx", // API 版本
title: "xxx", // 标题
filename: "xxx", // 文件名称
image: "xxx", // 图片连接
url: "xxx", // 网址连接
success: function (res) {
xxx; // 调用成功后执行
},
fail: function (err) {
if (err.code == '-2') {
fail && fail(err); // 调用了当前客户端中不存在的 API 版本
} else {
const msg = err.msg; //异常信息
Toast.fail(msg);
}
}
};
window.NativeApi.share(params);
复制代码
如下简要列出通用方法的抽象,目前基本遵循如下规范进行双端通讯。
window.NativeApi.xxx({
api_version:'',
name: "xxx",
path: "xxx",
id: "xxx",
success: function (res) {
console.log(res);
},
fail: function (err) {
console.log(err);
}
});
复制代码
因为初期版本选择了由 H5 本地引用 JSBridge,后期采用 Native 注入的方式。现有的 H5 须要对各类状况作兼容,逻辑抽象以下:
reqNativeBridge(vm, fn) {
if (!isApp()) {
// 若是不在 APP 内进行调用
vm.$dialog.alert({
message: "此功能须要访问 APP 才能使用",
});
} else {
if (!window.NativeApi) {
// 针对初期版本
vm.$dialog.alert({
message: "请更新到最新 APP 使用该功能",
});
} else {
// 此处只针对“调用了当前客户端中不存在的 API 版本”的报错进行处理
// 其他种类的错误信息交由具体的业务去处理
fn && fn((err) => {
vm.$dialog.alert({
message: "请更新到最新 APP 使用该功能",
});
});
}
}
}
复制代码
上述内容简要介绍了 JSBridge 的部分原理,但愿对从未了解过 JSBridge 的同窗能有所帮助。若是须要更深刻的了解 JSBridge 的原理和实现,如 JSBridge 接口调用的封装实现,JS 调用 Native 时的回调的惟一性等。你们能够去查阅更多资料,参考更详细的相关文档或他人的整理成文的沉淀。
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“ 5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com