JS Bridge 通信原理,50家大厂面试万字精华总结

public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

return super.onJsAlert(view, url, message, result);

}

iOS 实现

我们以 WKWebView 为例:

+ (void)webViewRunJavaScriptTextInputPanelWithPrompt:(NSString *)prompt

defaultText:(NSString *)defaultText

completionHandler:(void (^)(NSString * _Nullable))completionHandler

{

/** Triggered by JS:

var person = prompt(“Please enter your name”, “Harry Potter”);

if (person == null || person == “”) {

txt = “User cancelled the prompt.”;

} else {

txt = "Hello " + person + “! How are you today?”;

}

*/

if (xxx) {

BLOCK_EXEC(completionHandler, text);

} else {

BLOCK_EXEC(completionHandler, nil);

}

}

这种方式的缺点就是在 iOS 上面 UIWebView 不支持,虽然 WKWebView 支持,但它又有更好的 scriptMessageHandler,比较尴尬。

注入上下文

前面我们有讲过在 iOS 中内置了 JavaScriptCore 这个框架,可以实现执行 JS 以及注入 Native 对象等功能。

这种方式不依赖拦截,主要是通过 WebView 向 JS 的上下文注入对象和方法,可以让 JS 直接调用原生。

PS:iOS 中的 Block 是 OC 对于闭包的实现,它本质上是个对象,定义 JS 里面的函数。

iOS UIWebView

iOS 侧代码:

// 获取 JS 上下文

JSContext *context = [webview valueForKeyPath:@“documentView.webView.mainFrame.javaScriptContext”];

// 注入 Block

context[@“callHandler”] = ^(JSValue * data) {

// 处理调用方法和参数

// 调用 Native 功能

// 回调 JS Callback

}

JS 代码:

window.callHandler(JSON.stringify({

type: “scan”,

data: “”,

callback: function(data) {

}

}));

这种方式的牛逼之处在于,JS 调用是同步的,可以立马拿到返回值。

我们也不再需要像拦截方式一样,每次传值都要把对象做 JSON.stringify,可以直接传 JSON 过去,也支持直接传一个函数过去。

iOS WKWebView

WKWebView 里面通过 addScriptMessageHandler 来注入对象到 JS 上下文,可以在 WebView 销毁的时候调用 removeScriptMessageHandler 来销毁这个对象。

前端调用注入的原生方法之后,可以通过 didReceiveScriptMessage 来接收前端传过来的参数。

WKWebView *wkWebView = [[WKWebView alloc] init];

WKWebViewConfiguration *configuration = wkWebView.configuration;

WKUserContentController *userCC = configuration.userContentController;

// 注入对象

[userCC addScriptMessageHandler:self name:@“nativeObj”];

// 清除对象

[userCC removeScriptMessageHandler:self name:@“nativeObj”];

// 客户端处理前端调用

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message

{

// 获取前端传来的参数

NSDictionary *msgBody = message.body;

// 如果是 nativeObj 就进行相应处理

if (![message.name isEqualToString:@“nativeObj”]) {

//

return;

}

}

使用 addScriptMessageHandler 注入的对象实际上只有一个 postMessage 方法,无法调用更多自定义方法。前端的调用方式如下:

window.webkit.messageHandlers.nativeObj.postMessage(data);

需要注意的是,这种方式要求 iOS8 及以上,而且返回不是同步的。和 UIWebView 一样的是,也支持直接传 JSON 对象,不需要 stringify。

Android addJavascriptInterface

安卓4.2之前注入 JS 一般是使用 addJavascriptInterface ,和前面的 addScriptMessageHandler 有一些类似,但又没有它的限制。

public void addJavascriptInterface() {

mWebView.addJavascriptInterface(new DatePickerJSBridge(), “DatePickerBridge”);

}

private class PickerJSBridge {

public void _pick(…) {

}

}

在 JS 里面调用:

window.DatePickerBridge._pick(…)

但这种方案有一定风险,可以参考这篇文章:WebView中接口隐患与手机挂马利用

在 Android4.2 之后提供了 @JavascriptInterface 注解,暴露给 JS 的方法必须要带上这个。

所以前面的 _pick 方法需要带上这个注解。

private class PickerJSBridge {

@JavascriptInterface

public void _pick(…) {

}

}

Native 调用 JS


Native 调用 JS 一般就是直接 JS 代码字符串,有些类似我们调用 JS 中的 eval 去执行一串代码。一般有 loadUrlevaluateJavascript 等几种方法,这里逐一介绍。

但是不管哪种方式,客户端都只能拿到挂载到 window 对象上面的属性和方法。

Android

在 Android 里面需要区分版本,在安卓4.4之前的版本支持 loadUrl,使用方式类似我们在 a 标签的 href 里面写 JS 脚本一样,都是javascript:xxx 的形式。

这种方式无法直接获取返回值。

webView.loadUrl(“javascript:foo()”)

在安卓4.4以上的版本一般使用 evaluateJavascript 这个 API 来调用。这里需要判断一下版本。

if (Build.VERSION.SDK_INT > 19) //see what wrapper we have

{

webView.evaluateJavascript(“javascript:foo()”, null);

} else {

webView.loadUrl(“javascript:foo()”);

}

UIWebView

在 iOS 的 UIWebView 里面使用 stringByEvaluatingJavaScriptFromString 来调用 JS 代码。这种方式是同步的,会阻塞线程。

results = [self.webView stringByEvaluatingJavaScriptFromString:“foo()”];

WKWebView

WKWebView 可以使用 evaluateJavaScript 方法来调用 JS 代码。

[self.webView evaluateJavaScript:@“document.body.offsetHeight;” completionHandler:^(id _Nullable response, NSError * _Nullable error) {

// 获取返回值 response

}];

JS Bridge 设计


前面讲完了 JS 和 Native 互调的所有方法,这里来介绍一下我们这边 JS Bridge 的设计吧。

我们这边的 JS Bridge 通信是基于 WebViewJavascriptBridge 这个库来实现的。

主要是结合 Scheme 协议+上下文注入来做。考虑到 Android 和 iOS 不一样的通信方式,这里进行了封装,保证提供给外部的 API 一致。

具体功能的调用我们封装成了 npm 包,下面的是几个基础 API:

  1. callHandler(name, params, callback):这个是调用 Native 功能的方法,传模块名、参数、回调函数给 Native。

  2. hasHandler(name):这个是检查客户端是否支持某个功能的调用。

  3. registerHandler(name):这个是提前注册一个函数,等待 Native 回调,比如 pageDidBack 这种场景。

那么这几个 API 又是如何实现的呢?这里 Android 和 iOS 封装不一致,应当分开来说。

Android Bridge

前面我们有说过安卓可以通过 @JavascriptInterface 注解来将对象和方法暴露给 JS。

所以这里的几个方法都是通过注解暴露给 JS 来调用的,在 JS 层面做了一些兼容处理。

hasHandler

首先最简单的是这个 hasHandler,就是在客户端里面维护一张表(其实我们是写死的),里面有支持的 Bridge 模块信息,只需要用 switch...case 判断一下就行了。

@JavascriptInterface

public boolean hasHandler(String cmd) {

switch (cmd) {

case xxx:

case yyy:

case zzz:

return true;

}

return false;

}

callHandler

然后我们来看 callHandler 这个方法,它是提供 JS 调用 Native 功能的方法。在调用这个方法之前,我们一般需要先判断一下 Native 是否支持这个功能。

function callHandler(name, params, callback) {

if (!window.WebViewJavascriptBridge.hasHandler(name)) {

}

}

如果 Native 没有支持这个 Bridge,我们就需要对回调进行兼容性处理。这个兼容性处理包括两个方面,一个是功能方面,一个是 callback 的默认回参。

比如我们调用 Native 的弹窗功能,如果客户端没支持这个 Bridge,或者我们是在浏览器里面打开的这个页面,此时应该降级到使用 Web 的 alert 弹窗。

对于 callback,我们可以默认给传个 0,表示当前不支持这个功能。

假设这个 alert 的 bridge 接收两个参数,分别是 titlecontent,那么此时就应该使用浏览器自带的 alert 展示出来。

function fallback(params, callback) {

let content = ${params.title}\n{params.content}

window.alert(content);

callback && callback(0)

}

这个 fallback 函数我们希望能够更加通用,每个调用方法都应该有自己的 fallback 函数,所以前面的 callHandler 应该设计成这样:

function callHandler(name, params, fallback) {

return function(…rest, callback) {

const paramsList = {};

for (let i = 0; i < params.length; i++) {

paramsList[params] = rest[i];

}

if (!callback) {

callback = function(result) {};

}

if (fallback && !window.WebViewJavascriptBridge.hasHandler(name))) {

fallback(paramsList, callback);

} else {

window.WebViewJavascriptBridge.callHandler(name, params, callback);

}

}

}

我们可以基于这个函数封装一些功能方法,比如前面的 alert:

function fallback(params, callback) {

let content = ${params.title}\n{params.content}

window.alert(content);

callback && callback(0)

}

function alert(

title,

content,

cb: any

) {

return callHandler(

‘alert’,

[‘title’, ‘content’],

fallback

)(title, content, cb);

}

alert(this is titlehahaha, function() {

console.log(‘success’)

})

具体效果类似下面这种,这是从 Google 上随便找的一张图(侵删):

那么客户端又如何实现回调 callback 函数的呢?前面说过,客户端想调用 JS 方法,只能调用挂载到 window 对象上面的。

因此,这里使用了一种很巧妙的方法,实际上 callback 函数依然是 JS 执行的。

在调用 Native 之前,我们可以先将 callback 函数和一个 uniqueId 映射起来,然后存在 JS 本地。我们只需要将 callbackId 传给 Native 就行了。

function callHandler(name, data, callback) {

const id = cb_${uniqueId++}_${new Date().getTime()};

callbacks[id] = callback;

window.bridge.send(name, JSON.stringify(data), callbackId)

}

在客户端这里,当 send 方法接收到参数之后,会执行相应功能,然后使用 webView.loadUrl 主动调用前端的一个接收函数。

@JavascriptInterface

public void send(final String cmd, String data, final String callbackId) {

// 获取数据,根据 cmd 来调用对应功能

// 调用结束后,回调前端 callback

String js = String.format(“javascript: window.bridge.onReceive(‘%1KaTeX parse error: Expected group as argument to '\'' at end of input: s\', \'%2s’);”, callbackId, result.toDataString());

webView.loadUrl(js);

}

所以 JS 需要事前定义好这个 onReceive 方法,它接收一个 callbackId 和一个 result。

window.bridge.onReceive = function(callbackId, result) {

let params = {};

try {

params = JSON.parse(result)

} catch (err) {

//

}

if (callbackId) {

const callback = callbacks[callbackId];

callback(params)

delete callbacks[callbackId];

}

}

大致流程如下:

registerHandler

注册的流程比较简单,也是我们把 callback 函数事先存到一个 messageHandler 对象里面,不过这次的 key 不再是一个随机的 id,而是 name

function registerHandler(handlerName, callback) {

if (!messageHandlers[handlerName]) {

messageHandlers[handlerName] = [handler];

} else {

// 支持注册多个 handler

messageHandlers[handlerName].push(handler);

}

}

// 检查是否有这个注册可以直接检查 messageHandlers 里面是否有

function hasRegisteredHandler(handlerName) {

let has = false;

try {

has = !!messageHandlers[handlerName];

} catch (exception) {}

return has;

}

这里不像 callHandler 需要主动调用 window.bridge.send 去通知客户端,只需要等客户端到了相应的时机来调用 window.bridge.onReceive 就行了。

所以这里还需要改造一下 onReceive 方法。由于不再会有 callbackId 了,所以客户端可以传个空值,然后将 handlerName 放到 result 里面。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

文末

技术是没有终点的,也是学不完的,最重要的是活着、不秃。

零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。

最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。

自学最怕的就是缺乏自驱力,一定要自律,杜绝“三天打鱼两天晒网”,到最后白忙活一场。

高度自律的同时,要保持耐心,不抛弃不放弃,切勿自怨自艾,每天给自己一点点鼓励,学习的劲头就会很足,不容易犯困。

技术学到手后,找工作的时候一定要好好准备一份简历,不要无头苍蝇一样去海投简历,容易“竹篮打水一场空”。好好的准备一下简历,毕竟是找工作的敲门砖。

拿到面试邀请后,在面试的过程中一定要大大方方,尽力把自己学到的知识舒适地表达出来,不要因为是自学就不够自信,给面试官一个好的印象,面试成功的几率就会大很多,加油吧,骚年!

01013896)]
[外链图片转存中…(img-qret9a2S-1711701013896)]
[外链图片转存中…(img-uT1b10H3-1711701013896)]
[外链图片转存中…(img-yiFNrnsp-1711701013896)]
[外链图片转存中…(img-nP15GqjM-1711701013897)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-3l1ltqve-1711701013897)]

文末

技术是没有终点的,也是学不完的,最重要的是活着、不秃。

零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。

最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。

自学最怕的就是缺乏自驱力,一定要自律,杜绝“三天打鱼两天晒网”,到最后白忙活一场。

高度自律的同时,要保持耐心,不抛弃不放弃,切勿自怨自艾,每天给自己一点点鼓励,学习的劲头就会很足,不容易犯困。

技术学到手后,找工作的时候一定要好好准备一份简历,不要无头苍蝇一样去海投简历,容易“竹篮打水一场空”。好好的准备一下简历,毕竟是找工作的敲门砖。

拿到面试邀请后,在面试的过程中一定要大大方方,尽力把自己学到的知识舒适地表达出来,不要因为是自学就不够自信,给面试官一个好的印象,面试成功的几率就会大很多,加油吧,骚年!

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值