WKWebView干货--与JS交互实现可选删广告功能。

最近百度了一下原先写过的文章,突然发现是这样的:

也是无语了,下面是正文,虽然写的不好,但是是原创。

公司项目是一款类似浏览器的APP,前两天需要做一个需求,就是给网页去除广告,百度goolge之后,发现只能通过与JS交互来实现,故在此记录一下方法,由于我不懂JS,所以还需要一段时间来实现,不过目前已经可以实现删除图片了。

需求大概是这个样子:打开某个网页,长按网页中某个控件,弹出一个iOS原生的alertController,里边若干个选项中有删除广告的选项,点击后可隐藏网页中对应的标签,类似于UC浏览器的长按事件:

WKWebView干货--与JS交互实现可选删广告功能。_第2张图片
D9B84F2B-9AFD-4457-894D-98AD121F2126.png

删除后的页面为这样:

WKWebView干货--与JS交互实现可选删广告功能。_第3张图片
8A19E376-3CDF-42F6-9C90-6BEBF9D0E167.png

红框之中就是删掉的图片。

好了,不多说了,下面是具体的实现方法:

(一)、 WKWebView的使用和代理方法啥的网上有很多,在这里也就不再赘述了。

(二)、 由于需要有个长按事件,故我现在我的浏览器中长按试了一下,结果有一个系统自带的alertController弹了出来:

WKWebView干货--与JS交互实现可选删广告功能。_第4张图片
IMG_0975.PNG

具体为什么会弹出这个alert我也不清楚,我在LLDB中给系统的 [UIAlertController addAction:]方法下了个断点:
WKWebView干货--与JS交互实现可选删广告功能。_第5张图片
E866B5F2EEFCFCB07A221B5F7C44C4EB.jpg

再次执行长按方法后,得到方法调用的堆栈:
WKWebView干货--与JS交互实现可选删广告功能。_第6张图片
0ECACD6B0E4F76BB2FABEC33CBE9C5F6.jpg

图中蓝色区域是这个alert弹出所调用的方法,我又再次查看wkwebview的代理方法后得知,该方法并没有在代理中实现,也看不到这个方法的实现,经过请教别人得知,可以用 method swizzling去更改这个方法的实现,但这又涉及到了私有API的问题,故放弃。

(三)、 因为用不到这个系统的alert,所以我先把它隐藏掉:


WKWebView干货--与JS交互实现可选删广告功能。_第7张图片
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代码,形成一个字符串:


这里是注入方法,我是写了一个写了一个帮助类来存放这些东西,具体如何视情况而定:


WKWebView干货--与JS交互实现可选删广告功能。_第9张图片
CBF7304C-6A58-4C72-B0B2-8350C9E12A63.png

注意WKUserScript类的初始化方法中的后两个参数:WKUserScriptInjectionTimeAtDocumentEnd这个表示会在网页加载完之后再执行注入,后边那个BOOL值表示的是是否只是在主窗口才注入,所以一定要写对。

(六)、 第五步完成之后,就可以在网页中实现长按,不知道各位还记不记得给iOS发消息的JS代码:
webkit.messageHandlers.contextMenuMessageHandler.postMessage(data);

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

通过另一种方法来注入:

WKWebView干货--与JS交互实现可选删广告功能。_第10张图片
1DFC24CC-24FE-4F7E-A8B1-421811693C46.png

这里很容易看懂,通过一个block当注入成功后,会打印"注入成功"。

注入成功后,就可以在删除广告的代码中调用这个刚刚注入成功的方法了:

WKWebView干货--与JS交互实现可选删广告功能。_第11张图片
7942FF51-4D30-4D63-A42D-D59C154E176F.png

removeImgByUrl('%@')调用这个JS方法,把img的URL传进去从而删除掉这个img。

至此通过WKWebView与JS交互来删除img的方法已经实现,剩下还有点WKWebView点击和长按手势的判别需要做,后续更新吧。

如果有不同想法可以给我留言,望各位大大不吝言辞。

你可能感兴趣的:(WKWebView干货--与JS交互实现可选删广告功能。)