一个奇怪的业务场景,引起的胡乱思考javascript
问题其实不难解决,只是顺着这个问题,发散出了一些有意思的东西css
本文旨在讨论UIWebView,WKWebView有本身的机制,不用这么费劲html
咱们的业务最大的最重要的流量仍是在PC与WAP,也就是说主要业务仍是以Web的形式进行开发的,WAP上不少活动/页面/功能,他们不是由APP的H5团队主导开发的,也不在APP总体的规划功能内,但常常会以所谓低成本的形式接入APP尝试,快速的也在APP里进行传播。(后续验证可行和有效后,也会归入APP的功能规划里,以最流畅的体验进行呈现)前端
但这样会有一些问题,纯为WAP开发的页面,直接扔到APP的WebView里表现并很差java
WAP的团队开发出来的界面通常长这样
ios
若是这样的页面不作任何处理直接在APP中低成本接入
会变成这样git
问题就在于APP是有本身的NavigationBar的,而WAP的页面通常都为浏览器而生,浏览器没有本身的导航条因而WAP的团队很天然都会在WAP页面里开发出一个导航条,若是这个页面不作任何针对APP的处理,直接放入APP的WebView中,就会出现这样丑陋的双导航条,一个是native App本身的,一个是WAP网页本身画的github
这是一个很是常见的场景web
想要实现也很是简单后端
WAP识别APP的UA,进行定制化的开发就行了
为何说他奇怪?
团队不一样,业务场景不一样,也面临不同的问题,对于咱们来讲,这个问题不在于如何实现,而在于如何作到让WAP开发最省事。由于背景交代过了,WAP的前端团队和APP彻底不是一拨人,若是能有什么办法让WAP前端团队在开发工做中尽可能的无感知,尽可能的少操做,不须要WAP团队在开发的时候人工的判断UA,选择性渲染,因而蛋疼的问题来了
现有老的开发模式就是这样,每次都是人工适配,纯体力活,有时候项目紧急WAP团队就会忘了,上线的时候一发现,咦?在App里好丑啊,虽然改动很小,但一块后端断定UA,一块前端模板选择渲染,代码分散在几处,改起来很麻烦
单纯是Bar的话不是问题,写进WAP基类就行,问题是相似的场景看业务功能,有时候不止是Bar,会有定制化的东西,在APP里表现,不能和WAP同样
这个太底层了,对天天几千万UV的WAP来讲,进行这么大的改动,风险高,收益低(毕竟这个界面适配APP只是搂草打兔子捎带手)有点难推进,后续确实能够尝试一下
最大的问题在于,JS在client里执行的时机,JS执行的时候,这个Dom已经被渲染出来了,当你判断UA,要移除的时候,画面那个bar会闪一下,总体效果是,整个页面带着bar加载出来了,可是会忽然闪一下bar消失
XXWAPBAR
(WAP只用写几个字母)XXWAPBAR
标记的Dom隐藏看起来靠谱,看起来是一种WAP开发人员几乎不用管不用操心,也不会影响WAP,只在APP里有独有效果的设计,试试看
对于Hybrid App来讲,向WebView里面注入JS(CSS也是经过JS代码的方式注入),是太常见的一件事情了,注入就是最多见的native to js的通讯方式
[self.webView stringByEvaluatingJavaScriptFromString:injectjs];复制代码
webView.loadUrl("javascript:" + injectjs);复制代码
咱们注入这么一行demo JS代码试试看
var style = document.createElement('style');
//XXWAPBAR 是咱们的WAP顶部Bar的class标记
style.innerHTML = '.XXWAPBAR { display: none;}';
document.head.appendChild(style)复制代码
习惯性的在iOS的webViewDidFinishLoad
,安卓的OnPageFinished
的时机去注入这个JS,Run一下看看效果,纳尼?仍是闪烁!看来是注入晚了,网页已经渲染完了,这时候注入css,会像前面提到的client端隐藏dom同样,画面会闪烁一下,那咱们早一点,webViewDidStartLoad
与onPageStarted
的时机注入?Run一下看看效果,纳尼?完全没反应?
JSContext是Webkit里面JavaScriptCore框架里面的js上下文,其实就至关于一个WebView里面的js运行时,也能够理解为JS运行环境,先拿iOS作个试验
iOS的同窗想必都知道能够用KVC的方式取出UIWebView的JSContext,那么作一个试验,分别在StartLoad
和FinishLoad
的delegate里打印一下JSContext
- (void)webViewDidStartLoad:(JSBridgeWebView *)webView {
JSContext* context =[self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSLog(@"%@",context);
}
- (void)webViewDidFinishLoad:(JSBridgeWebView *)webView
{
JSContext* context =[self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSLog(@"%@",context);
}复制代码
运行事后你就会发现,同一个webview的JSContext,在时机不一样,他根本就不是一个JS上下文对象,地址都不同。相同的JS,运行在不一样的JS环境里,天然效果是彻底不同的。
每次WebView加载一个新Url的时候,都会丢掉旧的JS上下文,从新启用一个新的JS环境新的JS上下文,所以你在webViewDidStartLoad
的时候即使使用stringByEvaluatingJavaScriptFromString
去注入js,也是把js代码在旧的上下文中执行,当新的js上下文彻底不受任何影响,没任何效果。
安卓的道理也是同样的,所以咱们选择OnPageFinished
已经晚了,此时页面已经渲染完了,再注入画面会闪,选择OnPageStarted
实际上是早了,注入到错误的js上下文里,等页面开始加载,就启用了新的js上下文,所以白注入了。
咱们得换一个事件,选一个恰到好处的事件回调,安卓的WebViewClient的onLoadResource
事件,这个能够知足咱们的需求,这个时间点新的js上下文已经生效,整个网页处于加载资源的阶段,还没开始进行排版与渲染,此时加入恰好知足需求
运行一下,效果很是好,画面打开的时候,页面中就已经看不到那个Bar了
蛋疼的问题来了:
iOS的UIWebView没有这个事件,UIWebView只有可怜的这4个事件
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;复制代码
iOS平台提供的NSURLProtocol
是一个能够Hook全部网络请求的工具,不管是由WebView发起的,仍是直接由App发起的。
NSURLProtocol
与Hybrid App
相结合能够碰撞出很是多的火花好比
NSURLProtocol
实现Web图片Native缓存
SDWebImage
,进行fetch&cache图片NSURLProtocol
实现Hybrid Web页面静态资源本地包
NSURLProtocol
实现Web图片native滤镜处理能力
怎么使用NSURLProtocol我就很少说了,随便搜搜你能搜出一大筐。
咱们这个场景也能够利用这样方式来实现,简单的说,App就是经过Hook的方式,直接修改了WAP页面的源代码。但有2个选择,能够选择修改css代码,也能够选择直接修改html页面
每一个页面都要加载不少CSS,通常咱们的WAP项目里都有一些基础模板通用css,假设是common.css
1.NSURLProtocol选择性hook咱们本身域名下的common.css
文件
2.经过iOS的字符串处理,给这个文件尾部增长css信息
3.和JS注入的代码里的css同样.XXWAPBAR { display: none;}
NSString *newcontent = [NSString stringWithFormat:@"%@\n\n.XXWAPBAR { display: none;}\n",content];复制代码
这样Run一下,效果很是好,画面打开的时候,页面中就已经看不到那个Bar了
若是不修改CSS,修改HTML也行,但这样就不限定文件了,任意本身域名下的HTML
1.NSURLProtocol选择性hook咱们本身域名下的任意HTML文件
2.经过iOS的字符串处理,给这个head
标签增长信息
3.给head
标签增长script
子标签
4.其实直接给head
标签直接增长style
子标签也能够
NSString *newcontent = [content stringByReplacingOccurrencesOfString:@"<head>" withString:@"<head>\n<script type=\"text/javascript\">\n var style = document.createElement('style'); style.innerHTML = '.wkWapX { display: none;}'; document.head.appendChild(style); \n</script>\n"];复制代码
这样Run一下,效果同样,画面打开的时候,页面中就已经看不到那个Bar了
这是一个黑科技
这个科技和KVC取JSContext同样,都属于UnDocumented API
自从iOS 7推出JavaScriptCore
,苹果本意是开放这个框架,让开发者根据本身的需求,本身独立运行和开发脚本引擎,但不少人都想在UIWebView上使用JavaScriptCore
里很是方便的API快速的进行js与oc的互通,使用里面的JSContext,抛弃以往iframe走shouldStartLoadWithRequest
的delegate方式。
UIWebView是基于Webkit的,内部自然存在着一个javascriptcore,之前只是iOS没对外开放,iOS7才对外开放
但很惋惜,对于UIWebView看起来苹果然是对它没多少爱了,并无把JSContext暴露出来,拿到不到webview的JSContext,整个JSC的API也玩不起来,因而聪明的开发者利用KVC的方式仍是把它拿了出来
documentView.webView.mainFrame.javaScriptContext
说到底这仍是一个Undocumented Api,没有记录在合法苹果开发者文档与头文件的一个Api,存在必定的风险,但即使如此,使用这个方式依然存在一个问题,也就是我上文强调过的WebView与JSContext的问题
每次WebView加载一个新Url的时候,都会丢掉旧的JS上下文,从新启用一个新的JS环境新的JS上下文,所以你在
webViewDidStartLoad
的时候即使使用stringByEvaluatingJavaScriptFromString
去注入js,也是把js代码在旧的上下文中执行,新的js上下文彻底不受任何影响,没任何效果。
你们在搜索javaScriptCore
使用指南的时候,总能看到相似这样的代码,在OC中给JSContext直接注入对象or函数
// Use JSExport Protocol 将oc对象注入给js
context[@"ViewController"] = self
// 将oc的block,注入给JS当作函数
context[@"hello"] = ^(void) {
NSLog(@"hello world");
};复制代码
若是咱们基于这种模式来构建Hybrid Bridge,那么将带来很大的便利,最直观的优点就是,这种bridge是同步直接return返回的
而之前iframe经过shouldStartLoadWithRequest
的delegate方式想要返回,必须得异步,而且用js语句注入来执行回调,才能返回数据给js。
这种基于JSContext的同步Hybrid Bridge构建的时机若是是webViewDidFinishLoad
就会存在一些问题,在loadfinish的时候,表明网页中的js代码已经执行完了,若是此时才将bridge构建完毕,那么loadfinish以前执行的js代码是不可以使用jsbridge
若是咱们能捕获到新JSContext刚建立的时机,那么咱们就能搞事情
搜索和寻找中发现了这样一个东西
简单的说,这个开源库也找到了一种UnDocumented API
来准确捕捉到了新JSContext刚建立的时机,经过WebFrameLoadDelegate
WebFrameLoadDelegate
这个词随便在网上一搜,你就能搜到API和OC/Swift代码,但很惋惜,这个代码仅限macOS
Apple关于WebFrameLoadDelegate的官方文档URL
从这个官方文档中你能够发现比UIWebViewDelegate多不少的各类Webkit内核的事件
看到其中最重要的一个delegate没?
webView:didCreateJavaScriptContext:forFrame:
没错就是他,意思是说,其实Webkit内核早就把这类事件都抛出来了,而且在macOS的SDK中把这些事件都暴露给了开发者,可是在iOS的SDK中,UIWebView的头文件设计却把这些事件都吞掉了,没暴露出来,不让开发者使用
按着苹果的尿性,源码里通常都会这么写
if (_xxDelegate && [_xxDelegate respondsToSelector:@selector(webView:didCreateJavaScriptContext:forFrame:)]) {
[_xxDelegate webView:webView didCreateJavaScriptContext:ctx forFrame:frame];
}复制代码
若是苹果把这个delegate给藏了起来,没有写进UIWebViewDelegate的Protocol里,但咱们本身把这个函数实现了,按着苹果的尿性,就应当能够触发
因而TS_JavaScriptContext这个项目就按着这个思路去尝试而且真的成功了,他给NSObject添加了一个category,使得NSObject拥有了webView:didCreateJavaScriptContext:forFrame:
的implement,所以respondsToSelector
的断定就会生效,从而咱们就拿到了JS环境的建立事件
既然已经拿到了正确的时机,后面注入JS就行了,效果杠杠的,
到了这一步,单纯的找到时机,已经能解决个人问题了,不过WebFrameLoadDelegate
里面的其余事件让我产生了很大的好奇心
Apple关于WebFrameLoadDelegate的官方文档URL
从这里能够看到不少不少的事件,都是UIWebView里没有的,能够说macOS下的WebKit框架对外暴露的Api,更加能窥视Webkit本来的运做机制以及事件周期
想要窥视更多Webkit也能够看这个
ios UIWebview runtime header 用于私有api调用查看
其实Webkit整个都是开源的,网上也有不少教你本身下Webkit源码,编译Webkit的,看些个是最直接的,但毕竟太庞大了,头疼看不进去,哈哈哈哈哈
我在以前的文章动态界面:DSL&布局引擎中画过这样一个图
而今天发现,在这图里面还须要补充不少环节,也就是html/css/js
在被加载以前都发生了啥
能够看看这篇文章来学习一下,而后梳理一个大概的理解
看了苹果的WebFrameLoadDelegate
文档和那篇私有api调用查看
,你会发现有不少forFrame
的Api&Delegate,可见FrameLoader仍是很重要的一个环节
并且,经过TS_JavaScriptContext这个项目,我还发现一个有趣的现象,就是若是页面中不包含任何的JS(不管是HTML中的JS代码,仍是额外JS文件)那么就彻底不会有webView:didCreateJavaScriptContext:forFrame:
的事件被抛出来,能够想象既然没有JS代码,要毛的JS引擎。
其实一开始咱们聊的要注入CSS隐藏WAPUI的业务场景,已经不重要了。这么总体review一下你会发现,客户端解决方案里只有安卓比较舒服,iOS UIWebView都不太尽如人意。并且换了WKWebView可能这些问题都不存在(恩,项目还没用,没深挖)
一个奇怪的业务场景,引起的胡乱思考
可是这个奇怪的场景,和胡乱发散的思考,确实让我多的了解了不少关于WebView内核的机制,这内核机制太庞大了,如今仍是靠发散思考和搜索查找进行学习,有时间和精力真的想好好看看,亲自编译一下Webkit的源码,光是纯纯的源码文本就20M呢,要想看进去还真是一个十足的挑战