Android 和 Webview 如何相互 sayHello(一)

villainhr發表於2018-08-21

本系列文章一共有兩篇:主要來講解 webview 和客戶端的互動。 本篇為第一篇:Android 和 webview 的互動 後續一篇是:IOS 和 webview 的互動 如需獲得最新的內容,可以關注微信公眾號:前端小吉米

在移動時代 Web 的開發方式逐漸從 PC 適配時代轉向 Hybird 的 Webview。以前,我們只需要瞭解一下 PC Chrome 提供的幾個操作行為,比如 DOM、BOM、頁面 window.location 跳轉等等。你的一切行為都是直接和 瀏覽器打交道,只要規規矩矩的按照 W3C/MDN 上面的文件開發即可。比如,我需要你實現一個截圖的需求,後面一查文件,發現 API 不支援,沒法做,直接打回~

後面,你開始做 Hybird APP,產品又提了這個截圖的需求,你查了一下文件,發現 API 還是不支援,但是,客戶端同學就在邊上,你一拍大腿說,老鐵,給我一個截圖的 jsbridge 沒問題吧?

對於 PC Web 和 Hybird App 來說,給 HTML5 開發者最直觀的感受就是,以前 PC 上一些底層基礎功能,你可以直接在 App 裡面,配合客戶端直接使用。除了這一點還有一些其它的區別點,比如:

  • 使用 window.location,並不能一定能實現跳轉
  • unload 事件並不一定會觸發
  • 302/301 重定向問題會讓客戶端同學崩潰
  • https 證照問題 log,只能從客戶端同學那取
  • 客戶端可以直接拿到你的 cookie
  • UA 的定製需要客戶端來手動設定
  • ServiceWorker 開不開還得問客戶端
  • 不一而足...

這裡,將從一個 Web 開發者的角度觸發,仔細探尋一下 Webview 開發下,Web 開發者將遇見哪些問題,瞭解和 客戶端 互動的底層原理。本系列文章將分別介紹一下在 Android 和 IOS 系統下,開發 Hybird APP 大致流程和其中的需要注意、優化的地方。

本文主要介紹的是 Android 下 Webview 的開發。

tl;dr

本文主要從 H5 開發者的角度來簡單講解一下在 Hybird 開發過程中遇到的相關問題和對應的解決方案。

  • android 兩種呼叫 H5 的方式
  • javascript 呼叫 android 方式的對比
  • jsbridge.js 檔案的起源
  • android 如何 inject JS 檔案
  • 客戶端對於 webview 的效能優化

Anriod 開發 Webview 基礎

Webview 在 Android 裡面其實就是一個元件而已,它可以像其他的 Android 元件一樣在 screen 中定位佈局。對比於 HTML5 開發來說,可以類比為一個 Div,也就是說,webview 可以重疊 webview,同一個 screen 可以展示多個 webview 內容。

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:showIn="@layout/activity_main">
    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>
複製程式碼

上面就是一個簡單的 webview-activity 定義。順便提一下:activity 是 Android 開發的一個非常重要的概念,相當於 Router 中的一個子頁面。所以說,你新開啟的 webview 的樣式和佈局,都需要通過客戶端發版本才能更新的。比如,微信的 webview-acitivit 和 手Q 的 webview-activity 是兩個完全不一樣的 activity.

手Q
微信

在定製特有的 acitvity 之後,對於一個可用的 webivew,還需要對 webview 做相關的配置。整個流程圖為:

image.png-55.5kB

參考實際程式碼為:

// activity 的 onCreate 事件中
WebView webView = (WebView) findViewById(R.id.webview);
webView.setWebViewClient(defaultViewClient);
webView.setWebChromeClient(mChromeClient);

// 設定 webSettings
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setAllowContentAccess(true);
webSettings.setDatabaseEnabled(true);
webSettings.setDomStorageEnabled(true);


// 初始化完畢之後,就可以直接呼叫 loadUrl,來載入頁面
webView.loadUrl(url);
複製程式碼

這裡簡單解釋一下上述程式碼。webview 本身只是用來作為開啟 H5 頁面的容器,其本身並不能很好的處理頁面之間跳轉或者載入事件等行為。而 setWebViewClient 和 setWebChromeClient 主要是用來作為補充使用。具體解釋可以參考:

  • webview: 僅僅用來渲染和解析頁面
  • webviewClient: 解決頁面跳轉問題,重定向、非同步請求傳送,https 證照問題。
  • webChromeClient: 處理頁面 console.xx、alert、prompt 的資訊、定製化設定頁面的標題、頁面載入進度等。

更多的內容,大家可以直接參考 Android 官方文件的 public method 查閱即可。如果對 react 開發有了解的同學,應該能很容易理解上面 public method 的大致含義。當設定對應的 webview 配置之後,開啟一個頁面就非常簡單了,就兩行程式碼:

WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.loadUrl("https://www.example.com");
複製程式碼

findViewById 和前端的 document.getElementById 很類似,直接找到對應的 webview 節點,然後利用 loadUrl API 直接開啟指定的地址。後面,我們就主要來介紹一下,android 是如何和 js 進行通訊的。

android 如何和 js 相互通訊

首先,我們提出這個問題的時候,可以想一想為什麼?為什麼 android 和 js 之間一定要進行通訊呢?

回想一下平常的 hybird 的開發,我們通常在前端呼叫客戶端介面來獲取相關內容:

  • 獲取使用者地理位置
  • 獲取使用者選擇照片的內容(通常返回的是 base64)
  • 拿到靠譜的 visibilityChange 事件
  • 呼叫客戶端的訊息傳送介面 加快請求速度,比如騰訊內部的 Webso
  • ...

所以,兩者之間的通訊,不僅必須,而且很重要。下面我們來簡單介紹一下 通訊

所謂的通訊,其實更確切的來說就是傳遞訊息。不過,這兩者之間並不是簡單的建立起一個通道,就可以直接進行通訊。他們之間的通訊方向和方式還是有些區別的。

  • android => js: 是通過 javascript:window.jsbridge_visibilityChange(xxx) 直接呼叫 window 裡面繫結的執行函式,如果要傳參的話,是直接轉換成字串 inline 到函式裡面去。
  • js => android: 簡單來說,就是讓 android 監聽相關的事件。這些事件對應著 JS API 裡面的某些方法,比如 console、alert、prompt 等。

android 呼叫 js

我們深入到 API 層面來看一下,他們之間是如何相互進行呼叫的:

  • android => js: 方法只有兩個非常簡單
    • 使用 loadUrl("javascript:window.jsbridge_visibilityChange ")
    • API > 19。
mWebView.evaluateJavascript("(function() { return 'this'; })();", new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String s) {
        // 上述定義函式執行完成時,return 的內容
        Log.d("LogName", s); // Prints: "this"
    }
});
複製程式碼
  • js => android
    • 呼叫 android 設定的 JavascriptInterface (4.2 以上才能使用)
    • 通過 WebViewClient.shouldOverrideUrlLoading() 事件攔截對應的呼叫
    • WebChromeClient.onConsoleMessage() 監聽 js 的 console 觸發內容。
    • WebChromeClient.onJsPrompt 監聽 js 的 prompt 觸發內容。

js => android 的方法比較多,其中比較常用的有:WebChromeClient.onJsPrompt、WebViewClient.shouldOverrideUrlLoading、JavascriptInterface。

這裡,我們著重來講解一下 js 呼叫 android 的簡單過程。

js 直接呼叫 android

這裡,我們分方法來介紹一下上面對應的呼叫方式。首先是 addJavaScriptInterface。

addJavaScriptInterface

通過 addJavaScriptInterface 方法,可以直接在 window 上注入一個物件,上面掛載這 JavaScriptInterface 裡面定義的所有方法和內容。

我們直接看一個 addJavascriptInterface 內容。

# 定義一個 interface 物件
public class WebAppInterface {
    Context mContext;

    /** Instantiate the interface and set the context */
    WebAppInterface(Context c) {
        mContext = c;
    }

    // 可以直接呼叫 Android 上面的
    @JavascriptInterface
    public void showToast(String toast) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
    }
}

# 在 webview 例項裡面新增該 interface
mWebView.addJavascriptInterface(new JavaScriptInterface(), "jsinterface");
複製程式碼

然後,我們可以直接在 js 程式碼裡面呼叫物件上掛載的 API。

var String = window.jsinterface.showToast("update");
複製程式碼

不過,該方法在 4.2 版本之前存在嚴重的安全漏洞--利用 Java 反射機制,直接能直接執行敏感的 android 許可權。詳細可以參考 addJavascriptInterface 遠端指定程式碼漏洞。所以,你需要在指定的方法上面加上 @JavascriptInterface 裝飾符。

對於這種方式,客戶端同學是非常認可而且推崇的。因為不需要和其它複雜的方法耦合在一起,使用起來乾淨整潔。不過,有個問題是,4.2 一下的版本不能使用。

對於比使用其它的,比如通過 shouldOverrideUrlLoading 來處理的方法,這種方法實現的效率更高,更有效率。但是,一旦考慮的低版本,就不得不對於同一份 jsbridge 實現兩次,所以這對於客戶端就像是 Achilles' Heel。

onJsPrompt

使用 onJsprompt 的邏輯很簡單,通過直接監聽 WebChromeClient.onJsPrompt 事件,設定好對應協議的內容即可。jsPrompt 在 Web 中對應的行為是彈出一個框,裡面有使用者的輸入框和確定、取消按鈕。

image.png-17.1kB

具體程式碼如下:

 mWebView.setWebChromeClient(new WebChromeClient() {
    /**
     * msg: 是通過 prompt(msg) 裡面的內容
     * 
     */
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
  
        Uri uri = Uri.parse(message);
        
        boolean handle = message.startsWith("jsbridge:");

        if(handle){
            result.confirm("trigger"); // 有客戶端直接返回結果,不會弔起 input 
            return true;
        }

        return super.onJsPrompt(view, url, message, defaultValue, result);
    }
});
複製程式碼

然後,我們只要在 webview 裡面直接使用 prompt 呼叫即可。

function jsbridgePrompt(url){
    if(url) {
        nativeReady = false;
        var jsoncommand = JSON.stringify(url);
        var _temp = prompt(jsoncommand,'');
        return true;
    } else {
        // there is invalid jsbridge url
        return false;
    }
}
複製程式碼

效率低下的 shouldOverrideUrlLoading

上面兩種方法呼叫非常簡單,不需要再對應方法裡面額外耦合一些其它處理邏輯。另外,還有一種呼叫方式,是直接用來監聽頁面的請求來做相應處理的 -- WebViewClient.shouldOverrideUrlLoading。

這種方式在 Android 裡面用起來比較複雜,不僅需要處理對應的 302/301 跳轉,還需要做相關 webview 的許可權處理。雖然,呼叫處理是在主執行緒中完成的,但是裡面程式碼複雜度和實現效率比起來是無法和上面兩種方法相比的。

這裡對 shouldOverrideUrlLoading 方法進行簡單的介紹一下。shouldOverrideUrlLoading 一般只對於 a 標籤的跳轉和 HTML 的請求有相關的響應。但是,有個問題,我們怎樣去構造這樣的請求?

對於 a 標籤來說,如果沒有使用者的手動行為,你是無法觸發 onclick 事件的。所以,這裡可以考慮使用構造 iframe 請求來實現類 shouldOverrideUrlLoading 的請求。這裡提供一個最簡版本:

const fakeInvokeSchema = (url, errHandler) => {
  let iframe = document.createElement('iframe');

  let onload = function () {
    // 如果 shouldOverrideUrlLoading 沒有很好的捕獲並且取消 iframe 請求,則會直接執行 iframe 的 onload 事件
    if (typeof errHandler === 'function') {
      errHandler("trigger faile", url);
    }


  };
  iframe.src = url;
  iframe.onload = onload;
  (document.body || document.documentElement).appendChild(iframe);

  // 觸發完成後移除,減少頁面的渲染。
  setTimeout(function () {
    iframe && iframe.parentNode && iframe.parentNode.removeChild(iframe);
  }, 0);
}
複製程式碼

正常思維邏輯按照上面這樣處理就沒啥問題了,但是實際往往會給你一巴掌。具體細節可以參考如下:

  • 如果是 IOS 平臺:
    • 需要先進行 onload 和 src 的繫結,然後再將 iframe append 到 body 裡面,否則會造成連續 api 的呼叫,會間隔執行成功。
  • 如果是 Android:
    • 則需要先 append 到頁面,然後再繫結 onload 和 src。否則會造成 onload 失敗 和額外觸發一次 about:blank 的 onload 事件。about:black 的現象可以參考 juejin.com 開啟 github 登入。

listener 監聽回撥之謎

通過前文的 android js 的相互呼叫,我們大致能瞭解兩者之前互相呼叫的基礎。但在真正實踐當中,jsbridge 的相互呼叫其實可以歸納為兩種型別:

  • java call js:
    • with callback
      • once
      • permanent: 比如,用來獲取使用者狀態的變換資訊。相當於就是 listener。
    • without callback
  • js call java:
    • with callback
      • once
      • permanent: 比如,用來獲取頁面 visibility 的變更狀態。
    • without callback

這裡,我們一步一步的來解決(我們只瞭解 H5 相關的內容),首先簡單瞭解一下 once callback 如何解決。

once callback: 該類的 callback 非常好解決,可以通過在自定義的 jsbridge 檔案裡面通過 _CLIENT_CALLBACK + globalId++ 的方式生成唯一的 callback 方法。對於此類 callback 有時候為了節省記憶體在執行完畢後,還需要刪除該 callback

// jsCode call jsbridge
jsbridge.getDeviceId(callback)

// jsbridge.js register once callback
let onceCallback = "__ONCE_CALLBACK__" + globalId++;
window[onceCallback] = function(data){
    callback(data);
    delete(window[onceCallback]);
}
複製程式碼

with callback: 這類帶 callback 或者說是帶 listener 的方式,比較難處理,因為客戶端需要一直保留當前的 Listener ,如果 webview 通過 removeListener 移除還需要做相應的操作。另外,客戶端還需要判斷當前的 listener 是否和對應 register 的 webview 一致,不一致還需要銷燬當前註冊的 listener. 不過,具體思路也很簡單,直接通過 jsbridge 將在 window 裡面註冊的函式傳遞給客戶端。

// jsCode call jsbridge
jsbridge.qbVisibilityChange((vis)=>{
    // xxx
})


# 底層的解析程式碼為:
const jsbridge.qbVisibilityChange = function(callback){

        let CALLBACK_Listner = function(param){
            callback(param);
        }
        window["CALLBACK_Listner"] = CALLBACK_Listner;
        prompt(`jsbridge://qq.com/visibility#__callback__=CALLBACK_Listner`);
}
複製程式碼

jsbridge.js 檔案的起源

上面這些呼叫程式碼,其實都是和業務程式碼無關的。你可以仔細預想一下,如果 H5 需要適配多個 app 的 jsbridge,那麼你需要寫一個 switch/case 的語句。

switch(){
    case xx:
        load('bridgeA.js')
    case xx:
        load('bridgeB.js');
    case xx:
        load('bridgeC.js');
    break;
}
複製程式碼

而且如果他們對應的 API 介面名不一致的話,你還需要再包一層進行優化。這也就會導致,你可能會想自己寫一個 jsbridge,將所有不一致的 API 介面名,放到一個函式裡面進行處理。

// WrapBridge.js
jsbridge.visibilityChange = function(cb){
    if(UA.isQQ){
        jsbridge.qqVisibilityChange(cb)
    }else if(UA.isWeChat){
        jsbridge.wxVisibilityChange(cb)
    }
    ...
}
複製程式碼

所以,有時候你呼叫一個 jsbridge 的時候,其實並不知道該方法下面包了多少層。但是,有時候有些 app 為了解決該 jsbridge.js 侵入業務層業務引入的步驟,選擇使用由客戶端直接侵入載入。

下面我們來簡單介紹一下,客戶端如何做到直接侵入 webview 載入 jsbridge.js 檔案的。

android 侵入 webview 載入 bridge.js

這裡我們瞭解到如果 java 呼叫 js 是需要額外引入定製化的 invokeSchame://xxx ,方便提供給 web 進行呼叫。對於這類定製化需求,需要額外引入 jsbridge.js。這裡一般提供兩種方式來引入 jsbridge.js。一是通過官方文件的形式,告訴 H5 開發者,在開發之前需要額外引入指定檔案。而是直接利用 webview 注入的方式,將指定的 js 檔案打進去。

  • 知會 H5 開發額外引入檔案:這通常是搭配 hybird 開發使用,一來共同方便,二來也方便 debugger
  • 直接客戶端引入:對於平臺級的應用,常常會用到這種辦法,減少 H5 不必要的溝通和複雜度。

這裡,簡單介紹一下,客戶端如何引入 JS 檔案,並保證其能夠生效。一般情況下,客戶端注入的時機應該是在 DomContentLoaded 事件之後,保證不會阻塞相關的內容和事件。反映到 webviewClient 裡面的事件也就是:

  • onPageStarted
  • onPageFinished

最保險的方式,是直接在 onPageFinished 事件裡面注入 JS 檔案. 下面是一個虛擬碼,直接在全域性裡面執行一個函式。

webView.setWebViewClient(new WebViewClient(){
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        webView.loadUrl(
            "javascript:(function() { " +
                "var element = document.getElementById('hplogo');"
                + "element.parentNode.removeChild(element);" +
            "})()");
    }
});

複製程式碼

如果仔細思考一下就會發現,當前的 webview 是否值得注入,即,判斷 webview 的有效性,通常我們認為如下 webview 是有效的:

  • 重定向完畢後,最新開啟穩定的 webview
  • 已經開啟的 webview ,並且沒有被注入過

一般處理做法是直接在 webview 的 onPageFinished 事件裡面直接注入 jsbridge 檔案。當然,如果你的檔案簡單可以直接根據上面,寫在 inline 裡面,但是一般情況下,一般會抽離成一個單獨的 js 檔案。這裡,可以直接將 jsbridge 檔案轉換成 base64 編碼,然後利用 window.atob 直接解析即可。這其實和解析圖片有些類似。這樣的好處是可以直接外帶檔案,但壞處是增加 js 的解析時間。具體如下程式碼:

        @Override
       public void onPageFinished(WebView view, String url) {
          super.onPageFinished(view, url);

          injectScriptFile(view, "js/script.js"); // 注入外鏈程式碼

          // test if the script was loaded
          view.loadUrl("javascript:setTimeout(test(), 500)");
       }

       private void injectScriptFile(WebView view, String scriptFile) {
          InputStream input;
          try {
            // 一般會直接在初始化時,將該 js 檔案預讀為 base64
             input = getAssets().open(scriptFile);
             byte[] buffer = new byte[input.available()];
             input.read(buffer);
             input.close();
             String encoded = Base64.encodeToString(buffer, Base64.NO_WRAP);

             view.loadUrl("javascript:(function() {" +
                          "var parent = document.getElementsByTagName('head').item(0);" +
                          "var script = document.createElement('script');" +
                          "script.type = 'text/javascript';" +
             // 將 base64 轉成 string 程式碼
                          "script.innerHTML = window.atob('" + encoded + "');" +
                          "parent.appendChild(script)" +
                          "})()");
          } catch (IOException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
          }
       }
複製程式碼

具體參考:standalone js

前面我也告誡過大家:

教科書式的解決辦法,啥也解決不了

客戶端一般選擇侵入的時機通常會選在 onPageFinished 中,這已經是最簡單的了。但是,由於重定向的問題,又讓實現方法變得不那麼優雅。

webview 重定向解決辦法

現在最關鍵的是如何判斷當前開啟的 webview 是有效果的?

開啟一個網頁有兩個辦法:

  • webivew 自身控制:點選 a 標籤直接跳轉、通過 window.location 直接修改
  • 呼叫WebView的loadUrl()方法

和 URL 開啟相關的三個事件有:

  • shouldOverrideUrlLoading(): 攔截頁面的 GET/POST 請求,注意 HTML 其實就是一個簡易 GET 請求 同樣也會攔截。
  • onPageStarted():頁面開始載入時,會直接觸發
  • onPageFinished(): 頁面載入完成時會觸發。當請求重定向地址,並且成功返回結果時,也會觸發該事件
  • onProgressChanged: 主要是用來計算頁面載入的進度,會在 onPageStarted 和 onPageFinished 之間觸發多次,通常是 20-50-80-100 這樣的次數。另外,在重定向載入時,也會多次觸發該函式。

所以,為了得到頁面真正載入完畢的 flag,我們需要仔細瞭解一下在 301/302 時,上述對應事件觸發的流程。這裡就對應了兩種不同的開啟方式,以及是否存在重定向的 2x2 的選擇。

  • 200 正常一次性直接返回

    • loadUrl 開啟
      • onPageStarted()-> onPageFinished()
        • 注意,這裡並不會觸發 shouldOverrideUrlLoading 事件,這個很重要
    • a 標籤,window.location 開啟
      • shouldOverrideUrlLoading() => onPageStarted() => onPageFinished()
  • 301/302 重定向返回

    • loadUrl 開啟
      • repeat( onPageStarted()->shouldOverrideUrlLoading()->onPageFinished() ) * 重定向次數N => onPageStarted()->onPageFinished()
    • a 標籤,window.location 開啟
      • shouldOverrideUrlLoading => repeat( onPageStarted()->shouldOverrideUrlLoading()->onPageFinished() ) * 重定向次數N => onPageStarted()->onPageFinished()

簡單歸納一下,在 webview 中新開啟頁面,一定會觸發 shouldOverrideUrlLoading。在 native 裡面開啟 url,則只會走正常邏輯 (pageStart => onPageFinished ),除非重定向。

也就是凡是在 onPageStarted 和 onPageFinished 之間,觸發了 shouldOverrideUrlLoading 都是重定向,這個就是關鍵點。那麼我們可以設定一個 flag 標誌位,記錄此時文件是否真正的載入完成。基本步驟為:

  • onPageStarted 裡面,設定一個全域性變數 this.loaded = true
  • 在 shouldOverrideUrlLoading,將 this.loaded = false
  • 在 onPageFinished 判斷 this.loaded === true, 是代表當前 webview 已經載入完畢。不是,則代表重定向

webview 的效能優化

眾所周知,webview 的渲染效能在 Android 機上算是差強人意。但是,其本身的效能永遠是無法和客戶端相提並論的。當然,為了讓 webview 優化效能更進一步提升,平常做的方案有:

  • 離線包:通過客戶端預先下載 web 的離線包資源,極大的減少 webview 的載入時延。
  • RN/Flutter: 通過 JsBundle 的形式將客戶端元件的 API 進行封裝,將使用程式碼解析為 DSL 樹,由 JsBundle 解析渲染。由於參照物件完全是客戶端,所以,如果要將程式碼完全設計為 H5 程式碼來說是非常困難的,特別是實現像 CSS 一樣的佈局語法。
    • 他還有一個致命的劣勢,即,如果存在客戶端元件的更新,必須每次更新底層的解析版本,然後釋出到 Store 裡面並更新。這對於緊急 Bug 和新功能的提審來說影響非常大。

本文後續涉及的內容,只針對於偏向前端的 H5 資源載入優化和渲染優化。

離線包優化

對於 H5 資源載入優化,離線包可以說是碾壓一切,不過弊端和 RN 差不多。同樣也需要客戶端的聯動,如果發生 bug 只能按照版本的更替進行釋出。僅僅考慮到更新和版本問題來說,離線包確實很渣。However,你仔細想一想,離線包機制有 RN 複雜?它會涉及 UI 麼?實現難度大麼?

這個問題,我想應該不需要做太多解釋。首先,離線包僅僅是一個資源管理的邏輯 package,出了問題頂多就是走線上的資源而已。對於這一點來說,離線包機制更勝於 RN、效能更優於 H5。

ServiceWorker webview 內優化

ServiceWorker 其實不僅僅只侷限於 H5,對所有用到 網頁開發 來說都意義重大。究其緣由主要是他的效能優勢,以及可程式設計性開發。對標於 Android 的四大元件的 Service 來說,ServiceWorker 本身的想象力就可以理解為一個駐留 Web 程式以及網路中間層的代理控制。

但,弊端也不是沒有,主要在於它自身業務邏輯是獨立於當前對應的 Web 專案,需要在專案裡面額外引入一個 sw.js。對於某些新手同學來說,上手難度還是有一點,不過影響不大。但對比於離線包機制來說,處理快取差一點,其他的應該算是碾壓。

歡迎關注 前端小吉米

相關文章