這是一個系列文章,後續會逐篇展開具體實現。H5離線技術顧名思義就是將H5/CSS/JS和資源文件
打包提早下發到App中,這樣App在加載網頁的時候實際上加載的是本地的文件,減小網絡請求來提升網頁的渲染速度,並實現動態更新效果。css
就目前狀況來看,離線包的方案也是層出不窮的,本篇將列舉市面最多見的四種離線方案,進行探討分析,選擇最優方案構建離線包功能。若是你有優化h5渲染速度的需求,能夠用來參考,本篇僅作技術選型和方案原理刨析,後續篇章會選出最優方案進行深刻探討,加具體實現。目錄部分爲後續延伸。html
直接加載本地h5,大名鼎鼎的《cordova》框架即是基於此實現。前端
1.將全部的h5文件都放入一個文件夾中。ios
2.將這個文件夾以相對路徑的方式倒入到工程代碼中。git
3.獲取本地的文件路徑。github
這個方案就是將部署在服務器上面的前端代碼直接解壓到本地沙盒。加載js的時候直接加載本地沙盒中的html進行離線加載。將每一個前端的模塊都定義爲一個應用,打上id下發給客戶端,當用戶點擊對應模塊的時候根據id去沙盒查找對應的離線資源進行加載實現秒開。web
file:///.../index.html
。這是在使用file協議
訪問html,有些html樣式並不支持file協議,在樣式和功能上會有缺失,還會有一些api上的差別,前端開發好的代碼可能下載到沙盒裏致使有些資源沒法使用,產生一些適配問題。file協議&http協議:file協議主要用於訪問本地計算機中的文件,比如經過資源管理器打開文件同樣,針對本地的,即file協議是訪問你本機的文件資源。http協議訪問本地html是在本地起了一臺http服務器,而後你訪問本身電腦上的本地服務器,http服務器再去訪問你本機的文件資源。ajax
瀏覽器對兩種協議的處理有時會不一樣,譬如某些網頁中直接調用file協議來打開圖片,這樣的功能會被瀏覽器的安全設置阻擋,由於默認上,html是運行於客戶端的超文本語言,從安全性上來說,服務端不能對客戶端進行本地操做。即便有一些象cookie
這類的本地操做,也是須要進行安全級別設置的。假若你須要載入外部cdn的資源,好比livereload、browserSync
等工具的使用,因爲瀏覽器的同源策略,從本地文件系統載入外部文件將會失敗,會拋出安全性異常。apache
總的來講,這個方案會對前端產生嚴重的入侵,限制了前端只能經過相對路徑對js,css,image
等資源的加載,還有file協議的跨域問題
致使沒法引入外部cdn,這樣會限制前端開發,雖然用起來最簡單,但這並非一個好的方案。小程序
既然直接加載本地資源文件不是最好方案,那咱們是否能夠考慮一下另外一種方案基於NSURLProtocol攔截呢?固然可行了,可是往下看:
在UIWebView
上,protocol攔截確實是咱們的首選方案,建立個子類,在子類裏面實現protocol的代理方法便可實現對全部請求的攔截,固然也包括html裏面對css、js、img等資源加載
的請求。
- (void)startLoading
{
NSData *data = [NSData dataWithContentsOfFile:filePath];
if (mimeType == nil) {
mimeType = @"text/plain";
}
NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:[[self request] URL] statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{@"Content-Type" : mimeType}];
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
if (data != nil) {
[[self client] URLProtocol:self didLoadData:data];
}
[[self client] URLProtocolDidFinishLoading:self];
}
複製代碼
這樣便可完美解決h5的資源請求問題。
那麼在WKWebView
上,這個方案是行不通的,關於這方面的解釋已經不少了,WKWebView在獨立於app進程以外的進程中執行網絡請求,請求數據不通過主進程,所以,在WKWebView上直接使用 NSURLProtocol 沒法攔截請求。固然經過私有api能夠解決問題:
//僅iOS8.4以上可用
Class cls = NSClassFromString(@"WKBrowsingContextController」);
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
// 註冊http(s) scheme, 把 http和https請求交給 NSURLProtocol處理
[(id)cls performSelector:sel withObject:@"http"];
[(id)cls performSelector:sel withObject:@"https"];
#pragma clang diagnostic pop
}
}
複製代碼
但依然存在缺陷,post請求body數據被清空。因爲WKWebView在獨立進程裏執行網絡請求。一旦註冊http(s) scheme後,網絡請求將從Network Process發送到App Process,這樣 NSURLProtocol 才能攔截網絡請求。在webkit2
的設計裏使用MessageQueue進行進程之間的通訊,Network Process會將請求encode成一個Message,而後經過 IPC 發送給 App Process。出於性能的緣由,encode的時候HTTPBody和HTTPBodyStream這兩個字段被丟棄掉了。
若是使用Get請求攔截離線資源是沒有問題的,攔截到請求後映射爲本地資源生成NSHTTPURLResponse* response
,像上面的方案同樣去處理就能夠了。可是使用私有API又會面臨另一個風險:被拒
。
說一點題外話,目前據我所瞭解到百度App安卓就是採用的請求攔截方式,可是,是安卓,看下圖:
經過上圖能夠分析第十一、12步
,WebView對html解析的時候能夠發現資源請求並攔截,返回對應的緩存資源並渲染。實際上這個方案在iOS上是行不通
的,安卓可使用自家瀏覽器,能夠魔改瀏覽器,好比支付寶的UC,百度的T7等。iOS應用內是不容許使用魔改瀏覽器的,很遺憾,也就是說蘋果爸爸開放了什麼,咱們才能使用什麼。
總結來講,這個方案並不會對前端產生入侵,前端依然能夠不須要任何改變循序漸進開發就行了。但對於body的攔截和對私有api的使用,依然是存在風險,可是據我所知這個方案也是有項目在使用的,因此選則推薦。
WKURLSchemeHandler是iOS11就推出的,用於處理自定義請求的方案,不過並不能處理Http、Https等常規scheme。
WKWebViewConfiguration開放了setURLSchemeHandler:forURLScheme:
函數,須要指定一個自定義的scheme和一個用來處理WKURLSchemeHandler回調
的自定義對象。
根據註釋來看,若是註冊了一個無效的scheme或者使用WebKit內部已經處理的scheme,例如http、https、file等將會引起異常。咱們最好使用WKWebView的handlesURLScheme:
類方法來檢查給定scheme的可用性,以避免帶來一些未知問題。 使用方法也很簡單:
if (@available(iOS 11.0, *)) {
BOOL allowed = [WKWebView handlesURLScheme:@""];
if (allowed) {
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
//設置URLSchemeHandler來處理特定URLScheme的請求,CustomURLSchemeHandler須要實現WKURLSchemeHandler協議,用來攔截customScheme的請求。
[configuration setURLSchemeHandler:[CustomURLSchemeHandler new] forURLScheme: @"customScheme"];
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
self.view = webView;
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"customScheme://"]]];
}
} else {
// Fallback on earlier versions
}
複製代碼
WKURLSchemeHandler提供了兩個回調函數由上面自定義的CustomURLSchemeHandler對象來處理:
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;
- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;
複製代碼
經過urlSchemeTask
的request
對象能夠拿到請求對應的url
,若是是咱們自定義的scheme就去攔截它,經過url映射到對應的本地資源,並加載本地資源。
若是本地資源不存在,那麼經過url直接構建request對象訪問服務器,若是本地資源存在,那麼就能夠直接加載本地資源,和第二個方案同樣去使用它:
- (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
NSString *urlString = urlSchemeTask.request.URL.absoluteString;
//定位本地資源並映射到本地資源地址 filePath
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{@"Content-Type" : @"text/plain"}];
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
[urlSchemeTask didFinish];
}
複製代碼
實際上這個方案很好的解決了資源攔截的問題,而且能像第二個方案同樣去作處理。看起來沒什麼問題。可是它依然有短板:
因此這樣來看,WKURLSchemeHandler的攔截方案也並非很友好。
根據支付寶的文章《支付寶移動端動態化方案實踐》對離線包的描述:
當 H5 容器發出資源請求時,其訪問本地資源或線上資源所使用的 URL 是一致的。H5 容器會先截獲該請求,截獲請求後,發生以下狀況:
1.若是本地有資源能夠知足該請求的話,H5 容器會使用本地資源。
2.若是沒有能夠知足請求的本地資源,H5 容器會使用線上資源。 所以,不管資源是在本地或者是線上,WebView 都是無感知的。
能夠看出,支付寶並非採用的上述三種方案,由於上述方案除了protocol攔截之外,都沒法作到讓WebView無感知
,據我所知,支付寶目前應該採用的是起本地服務器方案
。起本地服務器天然就是http協議了,http協議和本地的file協議差別第一種方案裏面已經作了詳細介紹,那麼若是可以使用http協議加載本地資源的話,這樣作可以最大程度的讓前端對於離線包「無感」,也就是說前端不須要修改scheme,不須要考慮會不會由於file協議而帶來一些問題,也能忽略掉攔截api的平臺差別致使的框架實現差別,這樣一來前端開發好的代碼一份便可,布在服務器的同時,也上傳到咱們的離線包平臺就OK了。因此稱之爲「無感知」。
優勢:優勢前面都說了,同網絡服務器加載的樣式和功能徹底一致,不入侵前端,前端並不用關心當前頁面是離線仍是非離線,作到最大無感知。固然有優勢就有缺點,這也並非一個完美方案。
缺點:
這個方案的實施能夠參考:《基於 LocalWebServer 實現 WKWebView 離線資源加載》的處理,可是文末也提到了幾個問題:
這些問題對於我來講也是未知的。若是有成熟的搭建本地服務器方案歡迎留言。
本篇旨在分析一條最優方案來構建離線包核心功能,可是由於有小夥伴提出一些預加載等優化問題,因此從`bang's`的博客中摘了幾條優化方案可供參考。
題外話:從上面提到的支付寶文章來看,還有一段咱們能夠分析一下:
爲了解決離線包不可用的場景,fallback 技術應運而生。每一個離線包發佈的時候,都會同步在 CDN 發佈一個對應的線上版本,目錄結構和離線包結構一致。fallback 地址會隨離線包信息下發到本地。在離線包沒有下載好的場景下,客戶端會攔截頁面請求,轉向對應的 CDN 地址, 實如今線頁面和離線頁面隨時切換。
這個不可用場景
應該就是離線包不可用,未更新,資源有損壞,md5不匹配或者驗籤不經過等等。
第三種方案應該就是支付寶的fallback 技術
,能夠解決上述問題。固然前兩種方案也不是不可取,仍是要看需求和場景。
每一個包都會使用相同的 JS 框架和 CSS 全局樣式,這些資源重複在每個離線包出現太浪費,能夠作一個公共資源包提供這些全局文件。
不管是 iOS 仍是 Android,本地 Webview 初始化都要很多時間,能夠預先初始化好 Webview。這裏分兩種預加載:
首次預加載:在一個進程內首次初始化 Webview 與第二次初始化不一樣,首次會比第二次慢不少。緣由預計是 Webview 首次初始化後,即便 Webview 已經釋放,但一些多 Webview 共用的全局服務或資源對象仍沒有釋放,第二次初始化時不須要再生成這些對象從而變快。咱們能夠在 APP 啓動時預先初始化一個 Webview 而後釋放,這樣等用戶真正走到 H5 模塊去加載 Webview時就變快了。
Webview 池:能夠用兩個或多個 Webview 重複使用,而不是每次打開 H5 都新建 webview。不過這種方式要解決頁面跳轉時清空上一個頁面,另外若一個 H5 頁面上 JS 出現內存泄漏,就影響到其餘頁面,在 APP 運行期間都沒法釋放了。
理想狀況下離線包的方案第一次打開時全部HTML/JS/CSS 都使用本地緩存,無需等待網絡請求,但頁面上的用戶數據仍是須要實時拉,這裏能夠作個優化,在 Webview 初始化的同時並行去請求數據,Webview初始化是須要一些時間的,這段時間沒有任何網絡請求,在這個時機並行請求能夠節省很多時間。
具體實現上,首先能夠在配置表註明某個離線包須要預加載的 URL,客戶端在 Webview 初始化同時發起請求,請求由一個管理器管理,請求完成時緩存結果,而後 Webview 在初始化完畢後開始請求剛纔預加載的 URL,客戶端攔截到請求,轉接到剛纔提到的請求管理器,若預加載已完成就直接返回內容,若未完成則等待。
網路和存儲接口若是使用 webkit 的 ajax 和 localStorage 會有很多限制,難以優化,能夠在客戶端提供這些接口給 JS,客戶端能夠在網絡請求上作像 DNS 預解析/IP直連/長鏈接/並行請求等更細緻的優化,存儲也使用客戶端接口也能作讀寫併發/用戶隔離等針對性優化。 服務端渲染 早期 web 頁面裏,JS 只是負責交互,全部內容都是直接在 HTML 裏,到現代 H5 頁面,不少內容已經依賴 JS 邏輯去決定渲染什麼,例如等待 JS 請求 JSON 數據,再拼接成 HTML 生成 DOM 渲染到頁面上,因而頁面的渲染展示就要等待這一整個過程,這裏有一個耗時,減小這裏的耗時也是白屏優化的範圍以內。 優化方法能夠是人爲減小 JS 渲染邏輯,也能夠是更完全地,迴歸到原始,全部內容都由服務端返回的 HTML 決定,無需等待 JS 邏輯,稱之爲服務端渲染。是否作這種優化視業務狀況而定,畢竟這種會帶來開發模式變化/流量增大/服務端開銷增大這些負面影響。手Q的部分頁面就是使用服務端渲染的方式,稱爲動態直出。
關於這四種方案,都有優劣,關於選型,我偏向於NSURLProtocol攔截
和起本地服務器
的方案。固然仍是要參照本身的需求,就應用來講,都是能夠的。固然對於一個優秀的Hybird
框架,這些仍是遠遠不夠的,無論是從支付寶的方案仍是手百的方案來看,須要作的優化還有不少,無論是手Q的動態直出
,仍是支付寶的Nebula
,都還有不少東西須要咱們探討學習。不知道你們有沒有發現,不僅是手百,包括頭條,騰訊新聞,在頁面沒有所有push出以前就已經渲染完畢了,說明都存在對h5頁面進行預加載
的處理,這也是值得咱們深刻探討的環節。固然這一塊還要視具體需求和人力來定了。關於離線包的處理,這是我目前能想到的全部方案,對於他們的優劣也有總結,若是你有什麼建議或者更好的方案,歡迎留言。
開源地址:《WKJavaScriptBridge》(離線包後續引入)