web離線技術原理

注:本篇研究重點不在于某個離線方案的具體使用,而在于對方案的優缺點分析、探究和選型,以及一些我個人的看法。

前言

web離線技術顧名思義就是將H5/CSS/JS和資源文件打包提前下發到App中,這樣App在加載網頁的時候實際上加載的是本地的文件,減少網絡請求來提高網頁的渲染速度,并實現動態更新效果。

就目前情況來看,離線包的方案也是層出不窮的,本篇將列舉市面最常見的四種離線方案,進行探討分析,選擇最優方案構建離線包功能。如果你有優化h5渲染速度的需求,可以用來參考,本篇僅做技術選型和方案原理刨析,后續篇章會選出最優方案進行深入探討,加具體實現。目錄部分為后續延伸。

方案

  1. 通過獲取沙盒H5路徑直接加載
  2. 基于NSURLProtocol進行請求攔截
  3. 基于WKURLSchemeHandler進行自定義scheme注冊攔截
  4. 起本地服務器加載本地資源

選型

方案一:通過獲取沙盒H5路徑直接加載

直接加載本地h5,大名鼎鼎的《cordova》框架便是基于此實現。

  • 1.將所有的h5文件都放入一個文件夾中。

  • 2.將這個文件夾以相對路徑的方式倒入到工程代碼中。

  • 3.獲取本地的文件路徑。

這個方案就是將部署在服務器上面的前端代碼直接解壓到本地沙盒。加載js的時候直接加載本地沙盒中的html進行離線加載。將每個前端的模塊都定義為一個應用,打上id下發給客戶端,當用戶點擊對應模塊的時候根據id去沙盒查找對應的離線資源進行加載實現秒開。

  • 優點:簡單。
  • 缺點:
image
    1. 實際上從截圖中可以看到,我們在訪問本地html的時候可以看到實際路徑為file:///.../index.html。這是在使用file協議訪問html,有些html樣式并不支持file協議,在樣式和功能上會有缺失,還會有一些api上的差異,前端開發好的代碼可能下載到沙盒里導致有些資源無法使用,產生一些適配問題。
    1. 訪問本地資源還會導致資源路徑泄漏產生安全問題。
    1. 還會有一些瀏覽器的安全設置無法通過。
    1. 無法實現跨域資源請求,會讓前端開發人員無法訪問外部cdn。

file協議&http協議:file協議主要用于訪問本地計算機中的文件,好比通過資源管理器打開文件一樣,針對本地的,即file協議是訪問你本機的文件資源。http協議訪問本地html是在本地起了一臺http服務器,然后你訪問自己電腦上的本地服務器,http服務器再去訪問你本機的文件資源。

瀏覽器對兩種協議的處理有時會不同,譬如某些網頁中直接調用file協議來打開圖片,這樣的功能會被瀏覽器的安全設置阻擋,因為默認上,html是運行于客戶端的超文本語言,從安全性上來講,服務端不能對客戶端進行本地操作。即使有一些象cookie這類的本地操作,也是需要進行安全級別設置的。倘若你需要載入外部cdn的資源,比如livereload、browserSync等工具的使用,由于瀏覽器的同源策略,從本地文件系統載入外部文件將會失敗,會拋出安全性異常。

總的來說,這個方案會對前端產生嚴重的入侵,限制了前端只能通過相對路徑對js,css,image等資源的加載,還有file協議的跨域問題導致無法引入外部cdn,這樣會限制前端開發,雖然用起來最簡單,但這并不是一個好的方案。

方案二:基于NSURLProtocol進行請求攔截

既然直接加載本地資源文件不是最好方案,那我們是否可以考慮一下另一種方案基于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安卓就是采用的請求攔截方式,但是,是安卓,看下圖:


image

圖片來源《百度APP-Android H5首屏優化實踐》

通過上圖可以分析第11、12步,WebView對html解析的時候可以發現資源請求并攔截,返回對應的緩存資源并渲染。實際上這個方案在iOS上是行不通的,安卓可以使用自家瀏覽器,可以魔改瀏覽器,比如支付寶的UC,百度的T7等。iOS應用內是不允許使用魔改瀏覽器的,很遺憾,也就是說蘋果爸爸開放了什么,我們才能使用什么。

總結來說,這個方案并不會對前端產生入侵,前端依然可以不需要任何改變按部就班開發就好了。但對于body的攔截和對私有api的使用,依然是存在風險,但是據我所知這個方案也是有項目在使用的,所以選則推薦。

方案三:基于WKURLSchemeHandler進行自定義scheme注冊攔截

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;

通過urlSchemeTaskrequest對象可以拿到請求對應的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];
}

實際上這個方案很好的解決了資源攔截的問題,并且能像第二個方案一樣去做處理。看起來沒什么問題。但是它依然有短板:

    1. 因為使用的自定義scheme,并不是http協議,所以它依然無法解決跨域問題。
    1. 由于自定義了scheme,對于前端來說,需要額外將scheme設置為我們自定義的customScheme,這又會給前端帶來大量的改造,所以對前端還是產生了入侵。
    1. 上面提到在安卓完全不需要像iOS這樣大費周章的繞彎路,所以安卓可能就不需要這個自定義的scheme,這樣又會導致面臨著與安卓差異化嚴重問題。
    1. 因為API的限制,只能支持iOS11之后的系統。

所以這樣來看,WKURLSchemeHandler的攔截方案也并不是很友好。

方案四:起本地服務器加載本地資源

根據支付寶的文章《支付寶移動端動態化方案實踐》對離線包的描述:

當 H5 容器發出資源請求時,其訪問本地資源或線上資源所使用的 URL 是一致的。H5 容器會先截獲該請求,截獲請求后,發生如下情況:

1.如果本地有資源可以滿足該請求的話,H5 容器會使用本地資源。

2.如果沒有可以滿足請求的本地資源,H5 容器會使用線上資源。 因此,無論資源是在本地或者是線上,WebView 都是無感知的。

可以看出,支付寶并不是采用的上述三種方案,因為上述方案除了protocol攔截以外,都無法做到讓WebView無感知,據我所知,支付寶目前應該采用的是起本地服務器方案。起本地服務器自然就是http協議了,http協議和本地的file協議差異第一種方案里面已經做了詳細介紹,那么如果能夠使用http協議加載本地資源的話,這樣做能夠最大程度的讓前端對于離線包“無感”,也就是說前端不需要修改scheme,不需要考慮會不會因為file協議而帶來一些問題,也能忽略掉攔截api的平臺差異導致的框架實現差異,這樣一來前端開發好的代碼一份即可,布在服務器的同時,也上傳到我們的離線包平臺就OK了。所以稱之為“無感知”。

  • 優點:優點前面都說了,同網絡服務器加載的樣式和功能完全一致,不入侵前端,前端并不用關心當前頁面是離線還是非離線,做到最大無感知。當然有優點就有缺點,這也并不是一個完美方案。

  • 缺點:

      1. 需要額外搭建本地服務器,html文件的路徑需要做處理。
      1. 對于本地服務器的搭建存在成本問題,本地服務器的管理問題,例如服務器的打開、關閉時機等等。
      1. 對于本地服務器會不會帶來其他問題對于我來說也是未知的,并不是所有團隊都能像支付寶一樣搭建一個自己的服務器來處理。

這個方案的實施可以參考:《基于 LocalWebServer 實現 WKWebView 離線資源加載》的處理,但是文末也提到了幾個問題:

  • 資源訪問權限安全問題。
  • APP前后臺切換時,服務重啟性能耗時問題。
  • 服務運行時,電量及CPU占有率問題。
  • 多線程及磁盤IO問題。

這些問題對于我來說也是未知的。如果有成熟的搭建本地服務器方案歡迎留言。

本篇旨在分析一條最優方案來構建離線包核心功能,但是因為有小伙伴提出一些預加載等優化問題,所以從`bang's`的博客中摘了幾條優化方案可供參考。

Fallback 技術

題外話:從上面提到的支付寶文章來看,還有一段我們可以分析一下:

為了解決離線包不可用的場景,fallback 技術應運而生。每個離線包發布的時候,都會同步在 CDN 發布一個對應的線上版本,目錄結構和離線包結構一致。fallback 地址會隨離線包信息下發到本地。在離線包沒有下載好的場景下,客戶端會攔截頁面請求,轉向對應的 CDN 地址, 實現在線頁面和離線頁面隨時切換。

這個不可用場景應該就是離線包不可用,未更新,資源有損壞,md5不匹配或者驗簽不通過等等。

    1. 如果本地離線包沒有或不是最新,就同步阻塞等待下載最新離線包。這種方案用戶體驗最差,因為離線包體積相對較大。
    1. 如果本地有舊包,用戶本次就直接使用舊包,如果沒有再同步阻塞等待,這種會導致更新不及時,無法確保用戶使用最新版本。(據我所知微信小程序為此方案)
    1. 對離線包做一個線上版本,離線包里的文件在服務端有一一對應的訪問地址,在本地沒有離線包時,直接訪問對應的線上地址,跟傳統打開一個在線頁面一樣,這種體驗相對等待下載整個離線包較好,也能保證用戶訪問到最新。

第三種方案應該就是支付寶的fallback 技術,可以解決上述問題。當然前兩種方案也不是不可取,還是要看需求和場景。

公共資源包

每個包都會使用相同的 JS 框架和 CSS 全局樣式,這些資源重復在每一個離線包出現太浪費,可以做一個公共資源包提供這些全局文件。

預加載 webview

無論是 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》(離線包后續引入)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容

  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,120評論 1 32
  • 導語 WKWebView 是蘋果在 WWDC 2014 上推出的新一代 webView 組件,用以替代 UIKit...
    Jecky丶閱讀 8,586評論 2 22
  • 1、WKWebView 白屏問題WKWebView 自詡擁有更快的加載速度,更低的內存占用,但實際上 WKWebV...
    iosRn閱讀 2,105評論 1 10
  • 轉載鏈接:騰訊Bugly 導語 WKWebView 是蘋果在 WWDC 2014 上推出的新一代 webView ...
    Jelly_沫閱讀 2,868評論 0 3
  • WKWebView 是蘋果在 WWDC 2014 上推出的新一代 webView 組件,用以替代 UIKit 中笨...
    Aiana閱讀 4,609評論 1 8