JSBridge实战
前言
H5 VS Native 一直是前端技术界争执不下的话题。react、vue等技术栈引领着纯H5开发,rn、week则倡导原生体验。但在项目实战中,经常会选择一个中立的方案:混合开发。大众称呼:Hybrid。
本人目前从事新闻类产品研发,对于大家来讲,就是熟知的如今日头条、百度新闻、网易新闻等。在产品设计初期,考虑到一些实现难易程度问题(如新闻详情页,图文混排,NA实现起来不如H5这样自如),一些部分选择了Hybrid方式开发,本篇就把开发过程中的一些想法分享一下,以供大家参考。
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) {// 场景一: 拦截请求、接收schemaif (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我们采用了这种降级方案,短时间内完成了架构搭建。
优点:
这样减小了维护成本,功能完整,提高了调用成功的几率。
缺点:
增加了页面加载解析时间会影响白屏时间。
总结
Hybrid是一种连接H5跟NA的思路,即可以快速迭代H5功能,又可以有NA的体验,是混合开发的典型开发模式。实践过程中需要根据业务形态模型来定制代码实现,注入时机也不是一成不变的可以根据业务形态来选择。
参考文献
移动混合开发中的 JSBridge
远程过程调用
你要的WebView与 JS 交互方式 都在这里了
UIWebView与WKWebView、JavaScript与OC交互
iOS中UIWebView的使用详解
UIWebView代码注入时机与姿势
Hybrid 开发
JavaScriptCore在实际项目中的使用的坑
JSBridge实战相关推荐
- android 加载条封装,Android基于JsBridge封装的高效带加载进度的WebView
图片发自简书App 概述 从去年4月项目就一直用起了JsBridge,前面也针对jsBridge使用姿势介绍过一篇入门篇,<Android JsBridge实战 打造专属你的Hybrid APP ...
- JSBridge通信原理
JSBridge是个啥 直接来重点,记住:JSBridge 是一个很简单的东西,更多的是一种形式.一种思想,为了解决 H5 和 Native 的双向通信. 就像我们刚接触 ajax 时,也很懵逼.其实 ...
- Android(2017-2018)BAT面试题整理(Android篇,含答案)
1.四大组件 - Activity Activity详细总结 - 简书 - Service Carson带你学Android:一份全面 & 简洁的 Service 知识讲解攻略 - 简 ...
- WebView与APP交互实战记录
WebView与APP交互 WebView与APP交互,即网页通过JSBrige调用APP的功能,APP也可以通过JSBrige调用网页提供的方法.最近刚好接触到这一块,记录一下前端侧的实际操作过程, ...
- Android性能优化面试题集锦,实战篇
接触这一行也有很久了,从开始的实习到带团队,中间接触过很多人,前不久身边刚好有人去面试了阿里,抖音等这些公司还成功的面试上了,现在来分享一下面试前需要准备的知识点 很多人去面试之前,不知道会问到那些知 ...
- 支付宝 App 架构的原理与实战
点击上方"开发者技术前线",选择"星标" 13:21 在看 真爱 根据公开的 2018 年移动互联网行业分析报告,目前支付宝的月活跃用户已经超过 QQ ,成为国 ...
- DingTalk「开发者说」第8期 钉钉微应用开发实战
分享人:悦铭,钉钉H5微应用前端开发链路负责人 视频地址:一键回看 目录: 一.初识钉钉H5微应用 二.H5微应用原理解析 三.钉钉JSAPI使用与鉴权 四.H5微应用开发 五.H5的性能优化建议 六 ...
- 支付宝 App架构的原理与实战
根据公开的 2018 年移动互联网行业分析报告,目前支付宝的月活跃用户已经超过 QQ ,成为国内第二大 App. 支付宝一开始仅仅只是一个单体应用的工具型 App,让用户可以在手机完成支付宝相关的业务 ...
- 直播平台源码搭建教程直播原理与web直播实战
文章结构 直播构成 直播流程 web中直播技术 HLS协议 RTMP协议 HLS与RTMP对比 直播实战 安装nginx.nginx-rtmp-module.FFmpeg(以下操作均在mac下进行) ...
最新文章
- __cdecl __stdcall区别-转
- Android ExecutorService 的理解与使用
- 第一天 Requests库入门
- 【性能优化】 之 10053 事件
- 【Android开发】文本框、按钮、文本编辑框、提交登录、单选框
- 有关链表的小技巧,我都给你总结好了
- dfa算法c语言,DFA跟trie字典树实现敏感词过滤(python和c语言)
- 云服务器运行gpu程序很卡,请问怎样才使程序在GPU上运行?
- 高通发布两款耳机芯片:支持主动降噪和语音助手功能
- Ghost8.0分区备份与恢复详细图解
- ubuntu下python3及idle3的安装
- TensorFlow发布语音识别入门教程,附1GB数据集代码
- 深度学习1-tensorflow2.0自定义操作与建模方式
- 计算机随机储存器是什么,随机存储器是什么
- 【python】牛客竞赛语法入门班选择结构习题 python解法
- 《畅玩NAS》第8章 ZeroTier组建局域网
- 《Cracking the Coding Interview程序员面试金典》----猫狗收容所
- 考研/嵌入式/我的所思所想及其他
- sklearn之train_test_split()函数各参数含义
- MacBook 安装7zip