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

最近百度了一下原先写过的文章,突然发现是这样的:
WX20180904-114036@2x.png

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

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

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

D9B84F2B-9AFD-4457-894D-98AD121F2126.png

删除后的页面为这样:

8A19E376-3CDF-42F6-9C90-6BEBF9D0E167.png

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

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

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

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

IMG_0975.PNG

具体为什么会弹出这个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文件):

WX20170331-112733@2x.png

然后在代码中获取到这些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.png

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容