抖音 JSBridge(旧版,现在统一使用via)
一、JSBridge是什么
- 主流App开发模式
- Native App:传统原生APP开发模式。Android基于Java语言,底层调用Google的API,iOS基于OC或者Swift语言,底层调用ios官方提供的API。
- Web App:网站开发模式。将页面部署在服务器上,用户使用浏览器访问,一般泛指 SPA(Single Page Application)模式开发出的网站。
- Hybrid App:半Native半web混合开发模式。介于Web App、Native App两者之间,兼具Native良好交互体验和Web页跨平台开发优势。
- React Native App:用JS写出的原生应用。
2.Native vs Web
Native
|
Web
|
|
优点
|
可以调用系统底层API 交互顺畅,转场平滑 数据安全,稳定
|
跨多平台,表现力强 迭代快 即时上线,无需发版
|
缺点
|
任何改动需要发版(安卓热更新除外) 两端各一套实现方式,无法跨平台开发
|
无法调用系统底层API 性能取决于容器 安全性稳定性相对较弱
|
Native App
|
Web App
|
Hybrid App
|
React Native App
|
|
原生功能体验
|
优秀
|
差
|
良好
|
接近优秀
|
渲染性能
|
非常快
|
慢
|
接近快
|
快
|
是否支持设备底层访问
|
支持
|
不支持
|
支持
|
支持
|
网络要求
|
支持离线
|
依赖网络
|
支持离线(资源存本地情况)
|
支持离线
|
更新复杂度
|
高(几乎总是通过应用商店更新)
|
低(服务器端直接更新)
|
较低(可以进行资源包更新)
|
较低(可以进行资源包更新)
|
编程语言
|
Android(Java) iOS(OC/Swift)
|
js+html+css3
|
js+html+css3
|
主要使用JS编写 语法规则JSX
|
社区资源
|
丰富
|
丰富(大量前端资源)
|
有局限(不同的Hybrid相互独立)
|
丰富(统一的活跃社区)
|
上手难度
|
难(不同平台需要单独学习)
|
简单(写一次,支持不同平台访问)
|
简单(写一次,运行任何平台)
|
中等(学习一次,写任何平台)
|
开发周期
|
长
|
短
|
较短
|
中等
|
开发成本
|
昂贵
|
便宜
|
较为便宜
|
中等
|
跨平台
|
不跨平台
|
所有H5浏览器
|
Android,iOS,h5浏览器
|
Android,iOS
|
APP发布
|
App Store
|
Web服务器
|
App Store
|
App Store
|
3.Hybrid App
- 定义:Hybrid App(混合模式开发的移动应用)底层功能API均由原生容器通过某种方式提供,业务逻辑由H5页面完成,最后原生容器加载H5,从而完成整个业务流程,只需要写一套代码即可,即可达到跨平台效果。
- 原生容器 :Native中的webview组件,用于加载HTML文件。Android中是webview,iOS7以下有UIWebview,iOS7以上有了WKWebview。
- hybrid架构:上层为web层,底层为native层,通信靠jsbridge
4.hybrid核心--JSBridge技术:构建了 Native 和非 Native 间消息双向通信的通道
- JS 向 Native 发送消息 : 调用相关功能、通知 Native JS当前状态。
- Native 向 JS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态。
二、JSBridge实现原理
JSBridge是一种通用的交互理念,多种设计方式都可以实现,思路也不尽相同。
- JS 调用 Native 的几种通信方案
- 特殊url scheme假跳转的请求拦截:h5发出一条跳转请求,其中跳转目的地是一个非法的不存在的地址,客户端拦截并分析请求从而调用native方法。
- 优点: 兼容性好。这是所有JS调用Native的通信方式里,唯一同时支持安卓webview/苹果UIWebView/WKWebView的通信方式。
- 缺点:webview会把调用封装为请求,时延达到200ms~400ms;跳转的URL存在长度限制。
- JS上下文注入API:通过 WebView 提供的接口,向JS运行的Context(window)中直接注入对象或者方法,JavaScript调用时能直接执行相应的Native代码逻辑。
- 优点: 由于同步返回调用速度非常快,参照alert。
- 缺点: 低版本iOS系统不支持此方式;安卓 4.2 之前,注入JavaScript的接口是 addJavascriptInterface,存在安全漏洞,4.2 后引入JavascriptInterface新接口做替代,所以存在兼容性问题。
2.请求拦截假跳转通信方式详解
- url地址分为几部分:
协议://域名/路径?参数
aweme://profile/?douyin_id=88 (假跳转)
- JS发起跳转3种方式:
1)在HTML中用a标签直接填写假请求地址
<a href="aweme://profile/?douyin_id=88">A标签</a>
2) 原地跳转:在JS中用location.href
location.href = 'aweme://profile/?douyin_id=88'
3) iframe跳转:在JS中创建一个iframe,插入dom之中进行跳转
$('body').append('<iframe src="' + 'aweme://profile/?douyin_id=88' + '" style="display:none"></iframe>');
- 拦截规则:因为客户端内打开H5页面的webview容器,会无差别拦截h5页面发送所有请求,真正的url地址会正常跳转,而协议域名符合一定规则的url地址则会被客户端拦截,拦截下来的url不会导致webview继续跳转,因此用户完全没有感知。我们可以利用这个条件,定义一些scheme规则,客户端读取伪协议域名的部分作为通信识别,如bytedance:// , snssdk1128://, aweme:// 可与正常协议做区分,读取路径作为指令识别,读取参数作为数据,并根据约定调用对应的native原生代码。
- 客户端请求拦截:不同种类webview通过不同方法,都实现了这样的流程:首先根据协议/域名判断是否需要拦截此调用,若需要则取出路径,并匹配出指令,传入js携带参数数据调用相应native方法,停止webview的继续请求。
- 1)安卓 shouldOverrideUrlLoading
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//1 根据url,判断是否是所需要的拦截的调用 判断协议/域名
if (是){
//2 取出路径,确认要发起的native调用的指令是什么
//3 取出参数,拿到JS传过来的数据
//4 根据指令调用对应的native方法,传递数据
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
2)UIWebView webView:shouldStartLoadWithRequest:navigationType:
(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
//1 根据url,判断是否是所需要的拦截的调用 判断协议/域名
if (是){
//2 取出路径,确认要发起的native调用的指令是什么
//3 取出参数,拿到JS传过来的数据
//4 根据指令调用对应的native方法,传递数据
return NO;
//确认拦截,拒绝WebView继续发起请求
}
return YES;
}
3)WKWebView webView:decidePolicyForNavigationAction:decisionHandler:
(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
//1 根据url,判断是否是所需要的拦截的调用 判断协议/域名
if (是){
//2 取出路径,确认要发起的native调用的指令是什么
//3 取出参数,拿到JS传过来的数据
//4 根据指令调用对应的native方法,传递数据
//确认拦截,拒绝WebView继续发起请求
decisionHandler(WKNavigationActionPolicyCancel);
}else{
decisionHandler(WKNavigationActionPolicyAllow);
}
return YES;
}
3.Native调用 Js
- 使用evaluatingJavaScript 执行JS代码:
相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单,毕竟不管是 iOS 的 UIWebView 还是 WKWebView,还是 Android 的 WebView 组件,都以子组件的形式存在于 View/Activity 中,Native想要调用JS的时候,可以把数据与调用的JS函数,通过字符串拼接成JS代码,交给WebView进行执行,直接调用相应的 API 即可执行拼接 JavaScript 字符串。所以设计成为暴露一个如JSBridge的对象供native调用。
- 举个例子
1)JS中存在函数jsfunction()
function jsfunction(data){
console.log(JSON.parse(data))
//1 识别客户端传来的数据
//2 对数据进行分析,从而调用或执行其他逻辑
}
2)客户端用OC拼接字符串,拼出js代码,并用json传递数据
//data是一个字典,把字典序列化
NSString *paramsString = [self _serializeMessageData:data];
NSString* javascriptCommand = [NSString stringWithFormat:@"jsfunction('%@');", paramsString];
//要求必须在主线程执行JS
if ([[NSThread currentThread] isMainThread]) {
[self.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
} else {
__strong typeof(self)strongSelf = self;
dispatch_sync(dispatch_get_main_queue(), ^{
[strongSelf.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
});
}
这段代码只是用来拼接出这个字符串:jsfunction('{data:xxx,data2:xxx}');
再用evaluatingJavaScript或loadUrl对js代码进行执行,即完成了native对js方法的调用
4.通信过程整理
- S1. 客户端内H5里发送请求scheme,aweme://profile?douyin_id=233;
- S2. 客户端内webview容器拦截所有请求,挨个请求做字符串匹配,判断是否以 aweme:// 开头;
- S3. scheme命中匹配,客户端拆解出后面的参数得到操作名和对应的操作参数;
- S4. 客户端执行对应的操作,打开个人profile页;
- S5. Native功能调用完毕。
5.回调设计
- 场景问题:例如分享成功后增加积分等场景,h5网页如何知道客户端执行完毕,并执行相应回调呢?
- 参照JSONP信息传递的执行过程:
JSONP利用<script>标签没有跨域限制的特点来达到与第三方通讯的目的
调用方需要提供一个回调函数 比如cb20180725 来接收数据:
- <script src='https://www.douyin.com/get_data?callback= cb20180725'></script>
- 第三方包装callback参数作为函数名来包裹住响应的JSON数据如cb20180725({"param": “1111”})
- 1. 在window上生成挂载一个随机名的全局函数,函数里执行success的回调函数;
- 2.创建script标签,载入请求地址;
- 3.服务端返回一段执行window上cb20180725函数的js代码: cb20180725({"param": “1111”})
- 4.浏览器中执行完成jsonp回调。
- JSBridge回调过程:
- 1,H5发起scheme请求之前,随机生成一个callback_id,挂到window上;
- 2,在callback_id指向的唯一函数里,执行想要的回调函数
- 3,H5发送请求scheme,bytedance://profile?douyin_id=233&callback_id=dy20180724;
- 4,客户端内webview容器拦截所有请求,挨个请求做字符串匹配,匹配是否以 bytedance:// 开头;
- 5, scheme命中匹配,客户端拆解出后面的参数得到操作名和对应的操作参数;
- 6,客户端执行对应的操作,打开个人profile页;
- 7,执行完对应操作之后,客户端调用webview接口执行回调 javascript:dy20180724(json_data)
关键点:生成回调函数callback_id,回调函数存储(挂载在window)