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
去执行一串代码。一般有 loadUrl
、evaluateJavascript
等几种方法,这里逐一介绍。
但是不管哪种方式,客户端都只能拿到挂载到 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:
-
callHandler(name, params, callback):这个是调用 Native 功能的方法,传模块名、参数、回调函数给 Native。
-
hasHandler(name):这个是检查客户端是否支持某个功能的调用。
-
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 接收两个参数,分别是 title
和 content
,那么此时就应该使用浏览器自带的 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 title
, hahaha
, 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前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
文末
技术是没有终点的,也是学不完的,最重要的是活着、不秃。
零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。
最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。
自学最怕的就是缺乏自驱力,一定要自律,杜绝“三天打鱼两天晒网”,到最后白忙活一场。
高度自律的同时,要保持耐心,不抛弃不放弃,切勿自怨自艾,每天给自己一点点鼓励,学习的劲头就会很足,不容易犯困。
技术学到手后,找工作的时候一定要好好准备一份简历,不要无头苍蝇一样去海投简历,容易“竹篮打水一场空”。好好的准备一下简历,毕竟是找工作的敲门砖。
拿到面试邀请后,在面试的过程中一定要大大方方,尽力把自己学到的知识舒适地表达出来,不要因为是自学就不够自信,给面试官一个好的印象,面试成功的几率就会大很多,加油吧,骚年!
01013896)]
[外链图片转存中…(img-qret9a2S-1711701013896)]
[外链图片转存中…(img-uT1b10H3-1711701013896)]
[外链图片转存中…(img-yiFNrnsp-1711701013896)]
[外链图片转存中…(img-nP15GqjM-1711701013897)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-3l1ltqve-1711701013897)]
文末
技术是没有终点的,也是学不完的,最重要的是活着、不秃。
零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。
最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。
自学最怕的就是缺乏自驱力,一定要自律,杜绝“三天打鱼两天晒网”,到最后白忙活一场。
高度自律的同时,要保持耐心,不抛弃不放弃,切勿自怨自艾,每天给自己一点点鼓励,学习的劲头就会很足,不容易犯困。
技术学到手后,找工作的时候一定要好好准备一份简历,不要无头苍蝇一样去海投简历,容易“竹篮打水一场空”。好好的准备一下简历,毕竟是找工作的敲门砖。
拿到面试邀请后,在面试的过程中一定要大大方方,尽力把自己学到的知识舒适地表达出来,不要因为是自学就不够自信,给面试官一个好的印象,面试成功的几率就会大很多,加油吧,骚年!