JSBridge实战
栏目: JavaScript · 发布时间: 7年前
内容简介:H5 VS Native 一直是前端技术界争执不下的话题。react、vue等技术栈引领着纯H5开发,rn、week则倡导原生体验。但在项目实战中,经常会选择一个中立的方案:混合开发。大众称呼:Hybird。本人目前从事新闻类产品研发,对于大家来讲,就是熟知的如今日头条、百度新闻、网易新闻等。在产品设计初期,考虑到一些实现难易程度问题(如新闻详情页,图文混排,NA实现起来不如H5这样自如),一些部分选择了Hybird方式开发,本篇就把开发过程中的一些想法分享一下,以供大家参考。混合开发,最重要的问题是:
H5 VS Native 一直是前端技术界争执不下的话题。react、vue等技术栈引领着纯H5开发,rn、week则倡导原生体验。但在项目实战中,经常会选择一个中立的方案:混合开发。大众称呼:Hybird。
本人目前从事新闻类产品研发,对于大家来讲,就是熟知的如今日头条、百度新闻、网易新闻等。在产品设计初期,考虑到一些实现难易程度问题(如新闻详情页,图文混排,NA实现起来不如H5这样自如),一些部分选择了Hybird方式开发,本篇就把开发过程中的一些想法分享一下,以供大家参考。
JSBridge解决的问题
混合开发,最重要的问题是: H5和Native的双向通信。 但现实中JS和NA的交互方法非常有限,下面会详细说明。开发中如只是单纯的方法调用,既无法确保调用成功率,也无法确保代码足够简洁。于是就有了JSBridge。JSBridge,是一种JS实现的Bridge,是一种思路,可以有不同理解,不同的代码实现。主旨思想是在H5和NA之间搭建一个桥梁(Bridge),给两端留好更友好、更合理的接口。
H5和NA的双向通信通用方法
H5通信方式和兼容性如下表所示。指的是借助Native的webview加载H5页面,H5和NA之间通过API、URL拦截、全局调用等形式,实现消息通信。站在大厂的角度考虑,在实战的时候,会选择更兼容的方式。
H5调用NA方法梳理
| 平台 | 方法 | 备注 |
|---|---|---|
| Android | shouldOverrideUrlLoading | scheme拦截方法 |
| Android | addJavascriptInterface | API |
| Android | onJsAlert()、onJsConfirm()、onJsPrompt() | |
| IOS | 拦截URL | |
| IOS(UIwebview) | JavaScriptCore | API方法,IOS7+ 支持 |
| IOS(WKwebview) | window.webkit.messageHandlers | APi方法,IOS8+支持 |
NA调用H5方法梳理
| 平台 | 方法 | 备注 |
|---|---|---|
| Android | loadurl() | |
| Android | evaluateJavascript() | Android 4.4 + |
| IOS(UIwebview) | stringByEvaluatingJavaScriptFromString | |
| IOS(UIwebview) | JavaScriptCore | IOS7.0+ |
| IOS(Wkwebview) | evaluateJavaScript:javaScriptString | iOS8.0+ |
通过上面两端调用方法梳理表,不难分析出,URL拦截 & 执行JS是 安卓和IOS比较通用且兼容性较好的方案。我们混合开发的基础正是基于这种方法来实现的。
常规混合开发思路
H5和NA通信方面,最简单直接的思路是:NA拦截H5的URL获取消息(一般是通过修改iframe的src来实现 ①),经过业务处理,NA执行JS(在H5侧提前注册好的全局方法③)回调通知H5(如下图)。
H5代码实现如下:
<html>
...
<body>
<div class="content">XXXXX</div>
</body>
<script>
// ① 注册全局函数,以便端调用
window.setAllContent = function(){
}
// ② 通用方法函数
var sendschema = function(action,param){
let tempnode = document.createElement('iframe');
tempnode.src = "bdnews://"+action+param;
}
// ③ H5逻辑开始 运行函数
document.addEventListener("DOMContentLoaded",function(){
sendschema('load_finish');
},false);
</script>
...
</html>
复制代码
Android原理大致如下:
webView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 场景一: 拦截请求、接收schema
if (url.equals("load_url")) {
// 处理逻辑
dosomething
// 回掉
view.loadUrl("javascript:setAllContent(" + json + ");")
}
// 场景二:端自己调用H5,没有请求发起
clickbutton(){
view.loadUrl("javascript:setAllContent(" + json + ");")
}
}
});
复制代码
IOS大概逻辑如下:
// 初始化webview
UIWebView * view = [[UIWebView alloc]initWithFrame:self.view.frame];
[view loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.xx.com"]]];
[self.view addSubview:view];
/*
webView协议中的方法
shouldStartLoadWithRequest //准备加载内容时调用的方法,通过返回值来进行是否加载的设置
webViewDidStartLoad //开始加载时调用的方法
webViewDidFinishLoad //结束加载时调用的方法
didFailLoadWithError //加载失败时调用的方法
*/
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if ([urlString hasPrefix:@"scheme://hybrid?info="]) {
if([name isEqualToString:@"load_finish"]){
// [self.webView setContent];
[self.webView stringByEvaluatingJavaScriptFromString:strFormat];
}
}
}
- clickbutton(){
[self.webView setContent];
}
复制代码
但这样开发存在一些痛点:
1) 回调函数不明确 。可以说目前没有回调函数的机制,这导致一些依赖于回调函数的分析及判断无法正常使用,如:功能调用方、调用是否成功、调用失败异常处理等这些CASE;
2) 对应关系不明确 。有一些调用看起来像是回调,但没有把他们放到一起,导致代码散乱,难以维护。如上面demo:sendschema('load_finish') 和 setAllContent 本来含义是 告诉NA页面准备好了,NA收到后,向页面塞数据。本来紧密相关的一对功能,拆分开看不出有什么联系;
3) 全局函数冗杂 。理想中如果调用和回调成对出现,DEMO中注册及维护全局函数的工作就会减少很多。提升页面可读性和维护成本。如 load_finish 和 setAllContent,只保留 load_finish 即可;
4) 端内代码冗杂 。端内注册了与H5约定的调用方法,很显然也需要维护一套代码标识什么时候调用。
以上开发中遇到的问题,也许刚开始功能不多的时候还察觉不出问题,但是随着功能增加,后期维护成本很大。
JSB方案设计
在H5和NA之间增加一个中间层,这层封装了H5和NA通信的交互方式。H5和NA互不关心对方的样子,通过中间层暴露的方法进行功能调用即可。
JSB交互模型
H5跟NA交互,从H5角度来看大致可分为两大类:有去无回&有去有回、无去有回。
第一类交互模型
请求逻辑:有去无回、有去有回。这里有两种实现方案(初步思路稿如下):
① 函数名关联
let BDAPPnode = {
callbacks: {},
// 调用函数注册
invoke(action, params, successfnname, successfn) {
this.callbacks[successfnname] = {
success: successfn
};
sendschema(action, params);
},
// NA调用
callbackSuccess(callbackname, params) {
try {
BDAPPnode.callbackFromNative(callbackname, params, true);
} catch (e) {
console.log('Error in error callback: ' + callbackname + ' = ' + e);
}
},
callbackFromNative(callbackname, params, isSuccess) {
let callback = this.callbacks[callbackname];
if (callback) {
if (isSuccess) {
callback.success && callback.success(params);
}
};
}
};
复制代码
② ID 关联
let BDAPPnode = {
callbackId: Math.floor(Math.random() * 2000000000),
callbacks: {},
invoke(action, params, onSuccess, onFail) {
this.callbackId++;
this.callbacks[self.callbackId] = {
success: onSuccess,
fail: onFail
};
sendschema(action, params, this.callbackId);
},
callbackSuccess(callbackId, params) {
try {
BDAPPnode.callbackFromNative(callbackId, params, true);
} catch (e) {
console.log('Error in error callback: ' + callbackId + ' = ' + e);
}
},
callbackError(callbackId, params) {
try {
BDAPPnode.callbackFromNative(callbackId, params, false);
} catch (e) {
console.log('Error in error callback: ' + callbackId + ' = ' + e);
}
},
callbackFromNative(callbackId, params, isSuccess) {
let callback = this.callbacks[callbackId];
if (callback) {
if (isSuccess) {
callback.success && callback.success(callbackId, params);
} else {
callback.fail && callback.fail(callbackId, params);
}
delete BDAPPnode.callbacks[callbackId];
};
}
};
复制代码
在发出请求的时候,注册回调方法。这么做有两个目的:
-
无需提前注册所有全局回掉函数,减少不必要的初始化,进而减少白屏时间;
-
不用额外起回掉函数的名称,发起请求的时候传入一个随机ID,同时注册此ID的回掉函数。NA通过统一封装好的回掉函数调用,回调ID和参数,进而达到执行回调逻辑。
具体选用那个,还得根据具体情况具体分析看。
第二类交互模型看
请求逻辑:无去有回,没有发出请求,NA主动调用。此类还需注册全局变量,等待NA调用。跟非JSBridge的实现是一个道理
window.fn1 = () =>{
// do fn1
}
window.fn2 = () =>{
// do fn2
}
复制代码
方案选择
实战过程中深刻体会到,混合开发可以分为两大类: NA服务H5,H5服务NA 。
前者H5为主,大多数交互是H5发起NA请求,等待NA回调,可称之为:『一对一请求』,如:H5请求获取地理位置,NA做完后返回N\S坐标;
后者主要是为了解决NA成本实现高的问题,多为NA主动调用H5提前注册好的方法,可称之为:『单独请求』,确保功能顺利实现。
在项目实战过程中,经常会有这种情况:回调函数既是一对一请求,也是单独调用,如:评论功能,可以页面点击弹出NA输入框发送,也可以点击底BAR上NA实现的按钮弹框发送。对于页面来讲都需要更新。站在H5角度希望NA区分,H5页面调用的评论成功和NA调用的评论成功进行区分,这样就可以把模型一和模型二区分开独立实现(同时也可以区分页面刷新的来源)。但站在NA角度来讲,不关心谁吊起的,只要评论成功,就应该去调用更新页面的H5方法。不然NA需要从调用开始就携带参数,一路到底。跟端沟通后,双方都妥协了一步,简单功能的进行了来源区分模型一实现,较为复杂的模型二实现。
API封装
API层处于JSBridge底层和业务,有些人也把它当做JSBridge的一部分,为了更好理解,我将它单独抽离出来。此处主要封装业务层调用,如下面代码。
此处多说一句: 平日开发要有封装和抽离的思想,一方面减少重复代码,一方面不断抽离将代码分层,没一层可以做一些封装和扩展,可以提高代码复用性。
JSB注入时机
NA注入
我们肯定是期望JSB注入越早越好,这样不论在前端页面中任何位置都可以随时调用,NA注入JS的方法和时机都比较局限。如下表:
| 平台 | 方法 | 时机 |
|---|---|---|
| IOS[UI] | [self.webView stringByEvaluatingJavaScriptFromString:injectjs] | webViewDidFinishLoad(会有时机问题) |
| IOS[wk] | evaluateJavaScript:xxxx | didCreateJavaScriptContext |
| Android | webView.loadUrl("javascript:" + injectjs);) | OnPageFinished |
网页描述页面状态的值有以下方法,根据兼容性及实现完整性,一般用DOMContentLoaded,IE9以下用readystatechange来判断页面是否加载成功。
| 名称 | 父对象 | 描述 | 兼容性 |
|---|---|---|---|
| DOMContentLoaded | doc | 页面内容OK | IE9+ |
| onload | win | 页面所有只要加载完成 | |
| readystatechange | doc | 页面加载状态:uninitialized(为初始化):对象存在但尚未初始化。loading(正在加载):对象正在加载数据。loaded(加载完毕):对象加载数据完成。interactive(交互):可以操作对象了,但还没有完全加载。complete(完成):对象已经加载完毕 | IE9&IE10有实现bug |
IOS的uiwebview提供了代理WebViewDidFinishLoad,WebViewDidFinishLoad 被调用时,readyState 可能处在 interactive 和 complete 两种状态,所以初始化页面直接调用会有问题。对于这个问题从NA角度可以实现一个NSObject的扩展,并实现webView:didCreateJavaScriptContext:forFrame。从H5角度可以检测页面状态,在complete之后再调用native。
IOS的didCreateJavaScriptContext和Android的OnPageFinished(the page has finished loading)均是在网页onload之前完成,所以这两个时机没有调用顺序的问题。
优点:
1)注册早,即使在页面初始化就调用端能力,也可以满足
缺点:
由于我们选择的是uiwebview如果按照上面的考虑,这样做有几点不足之处 1)监听实现成本高 2)需要NA注入,NA对于JS不熟悉,JS往往也不清楚NA逻辑,后面维护成本不可控制。
如果时间不充裕的情况下,除了NA注入,还有别的办法嘛?
JS注入
其实JS也可以在页面一开始就注入。比如在head里直接应用抽离出来的Jsbridge代码,本次8.0我们采用了这种降级方案,短时间内完成了架构搭建。
优点:
这样减小了维护成本,功能完整,提高了调用成功的几率。
缺点:
增加了页面加载解析时间会影响白屏时间。
以上所述就是小编给大家介绍的《JSBridge实战》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Alone Together
Sherry Turkle / Basic Books / 2011-1-11 / USD 28.95
Consider Facebookit’s human contact, only easier to engage with and easier to avoid. Developing technology promises closeness. Sometimes it delivers, but much of our modern life leaves us less connect......一起来看看 《Alone Together》 这本书的介绍吧!