WKWebView干货--与JS交互实现可选删广告功能。
公司项目是一款类似浏览器的APP,前两天需要做一个需求,就是给网页去除广告,百度goolge之后,发现只能通过与JS交互来实现,故在此记录一下方法,由于我不懂JS,所以还需要一段时间来实现,不过目前已经可以实现删除图片了。
需求大概是这个样子:打开某个网页,长按网页中某个控件,弹出一个iOS原生的alertController
,里边若干个选项中有删除广告的选项,点击后可隐藏网页中对应的标签,类似于UC浏览器的长按事件:
删除后的页面为这样:
8A19E376-3CDF-42F6-9C90-6BEBF9D0E167.png红框之中就是删掉的图片。
好了,不多说了,下面是具体的实现方法:
(一)、 WKWebView的使用和代理方法啥的网上有很多,在这里也就不再赘述了。
(二)、 由于需要有个长按事件,故我现在我的浏览器中长按试了一下,结果有一个系统自带的alertController
弹了出来:
具体为什么会弹出这个alert我也不清楚,我在LLDB中给系统的
[UIAlertController addAction:]
方法下了个断点:E866B5F2EEFCFCB07A221B5F7C44C4EB.jpg
再次执行长按方法后,得到方法调用的堆栈:
0ECACD6B0E4F76BB2FABEC33CBE9C5F6.jpg
图中蓝色区域是这个alert弹出所调用的方法,我又再次查看wkwebview的代理方法后得知,该方法并没有在代理中实现,也看不到这个方法的实现,经过请教别人得知,可以用
method swizzling
去更改这个方法的实现,但这又涉及到了私有API的问题,故放弃。
(三)、 因为用不到这个系统的alert,所以我先把它隐藏掉:
E374EAB8-F8A0-4864-8496-02E27B5D322C.png
这里是执行一段JS代码来屏蔽掉弹出框。
(四)、 屏蔽之后,再次长按网页,果然不会出现这个alert了,那么接下来,自然是要自己加一个长按事件来自定义弹出alertController,但我在给wkwebview加了长按事件之后,确实可以自定义弹出框,但是网页的长按事件和单击事件是共存的,也就是说我长按之后,如果长按的是一个按钮,那么他会跳转到下一页面,而且网页中没有长按状态(也就是长按某个控件置灰),故这种方法貌似也行不通。
(五)、 原生方法行不通后,通过JS来操作网页貌似是最正确的方法,由于我不会JS,故去GitHub找到了火狐浏览器的源码,期望能从中获取到什么,果不其然,从中找到了实现网页长按获取控件信息并发送给iOS的方法代码:
(function() {
"use strict";
var MAX_RADIUS = 9;
var longPressTimeout = null;
var touchDownX = 0;
var touchDownY = 0;
var highlightDiv = null;
var touchHandled = false;
function cancel() {
if (longPressTimeout) {
clearTimeout(longPressTimeout);
longPressTimeout = null;
if (highlightDiv) {
document.body.removeChild(highlightDiv);
highlightDiv = null;
}
}
}
function createHighlightOverlay(element) {
// Create a parent element to hold each highlight rect.
// This allows us to set the opacity for the entire highlight
// without worrying about overlapping opacities for each child.
highlightDiv = document.createElement("div");
highlightDiv.style.pointerEvents = "none";
highlightDiv.style.top = "0px";
highlightDiv.style.left = "0px";
highlightDiv.style.position = "absolute";
highlightDiv.style.opacity = 0.1;
highlightDiv.style.zIndex = 99999;
document.body.appendChild(highlightDiv);
var rects = element.getClientRects();
for (var i = 0; i != rects.length; i++) {
var rect = rects[i];
var rectDiv = document.createElement("div");
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
var top = rect.top + scrollTop - 2.5;
var left = rect.left + scrollLeft - 2.5;
// These styles are as close as possible to the default highlight style used
// by the web view.
rectDiv.style.top = top + "px";
rectDiv.style.left = left + "px";
rectDiv.style.width = rect.width + "px";
rectDiv.style.height = rect.height + "px";
rectDiv.style.position = "absolute";
rectDiv.style.backgroundColor = "#000";
rectDiv.style.borderRadius = "2px";
rectDiv.style.padding = "2.5px";
rectDiv.style.pointerEvents = "none";
highlightDiv.appendChild(rectDiv);
}
}
function handleTouchMove(event) {
if (longPressTimeout) {
var { screenX, screenY } = event.touches[0];
// Cancel the context menu if finger has moved beyond the maximum allowed distance.
if (Math.abs(touchDownX - screenX) > MAX_RADIUS || Math.abs(touchDownY - screenY) > MAX_RADIUS) {
cancel();
}
}
}
function handleTouchEnd(event) {
cancel();
removeEventListener("touchend", handleTouchEnd);
removeEventListener("touchmove", handleTouchMove);
// If we're showing the context menu, prevent the page from handling the click event.
if (touchHandled) {
touchHandled = false;
event.preventDefault();
}
}
addEventListener("touchstart", function (event) {
// Don't show the context menu for multi-touch events.
if (event.touches.length !== 1) {
cancel();
return;
}
var data = {};
var element = event.target;
// Listen for touchend or move events to cancel the context menu timeout.
element.addEventListener("touchend", handleTouchEnd);
element.addEventListener("touchmove", handleTouchMove);
do {
if (!data.link && element.localName === "a") {
data.link = element.href;
// The web view still shows the tap highlight after clicking an element,
// so add a delay before showing the long press highlight to avoid
// the highlight flashing twice.
var linkElement = element;
setTimeout(function () {
if (longPressTimeout) {
createHighlightOverlay(linkElement);
}
}, 100);
}
if (!data.image && element.localName === "img") {
data.image = element.src;
}
element = element.parentElement;
} while (element);
if (data.link || data.image) {
var touch = event.touches[0];
touchDownX = touch.screenX;
touchDownY = touch.screenY;
longPressTimeout = setTimeout(function () {
touchHandled = true;
cancel();
webkit.messageHandlers.contextMenuMessageHandler.postMessage(data);
}, 500);
webkit.messageHandlers.contextMenuMessageHandler.postMessage({ handled: true });
}
}, true);
// If the user touches down and moves enough to make the page scroll, cancel the
// context menu handlers.
addEventListener("scroll", cancel);
}) ();
从上面代码可看出,这个JS方法写的大概是一个:当点击一个控件,给他一个100毫秒的高亮状态,然后如果是长按的话(按住的时间超过500毫秒),通过webkit.messageHandlers.contextMenuMessageHandler.postMessage(data);
给iOS发送信息,其中的data
是一个字典,其中的参数从代码中也可看出,参数为link
(按钮的超链接)和image
(图片的URL)。
下面 简单说下这个JS代码如何来注入:
先将这个JS代码存到项目中(创建一个JS文件):
然后在代码中获取到这些JS代码,形成一个字符串:
WX20170331-112657@2x.png
这里是注入方法,我是写了一个写了一个帮助类来存放这些东西,具体如何视情况而定:
CBF7304C-6A58-4C72-B0B2-8350C9E12A63.png
注意WKUserScript
类的初始化方法中的后两个参数:WKUserScriptInjectionTimeAtDocumentEnd
这个表示会在网页加载完之后再执行注入,后边那个BOOL
值表示的是是否只是在主窗口才注入,所以一定要写对。
(六)、 第五步完成之后,就可以在网页中实现长按,不知道各位还记不记得给iOS发消息的JS代码:
webkit.messageHandlers.contextMenuMessageHandler.postMessage(data);
,<p>WKWebView
有专门的协议方法来接收这一信息:
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
在这个方法的实现中可以拿到JS给iOS的字典data:NSMutableDictionary *data = (NSMutableDictionary *)message.body;
,然后通过分析这两个参数来创建alertController。
删除JS控件的代码(只找到了删除img的代码,后续再更新别的方法):
function removeImgByUrl(url) {
var x = document.getElementsByTagName("img");
for (var i = 0; i < x.length; i++){
if (x[i].src==url){
x[i].parentNode.remove(x[i]);
}
}
}
同样把它写入到JS文件中:
B6424109-08AA-4C72-84D5-AD89E2CD6105.png通过另一种方法来注入:
1DFC24CC-24FE-4F7E-A8B1-421811693C46.png这里很容易看懂,通过一个block当注入成功后,会打印"注入成功"。
注入成功后,就可以在删除广告的代码中调用这个刚刚注入成功的方法了:
7942FF51-4D30-4D63-A42D-D59C154E176F.pngremoveImgByUrl('%@')
调用这个JS方法,把img的URL传进去从而删除掉这个img。
至此通过WKWebView与JS交互来删除img的方法已经实现,剩下还有点WKWebView点击和长按手势的判别需要做,后续更新吧。
如果有不同想法可以给我留言,望各位大大不吝言辞。