Webpack實戰-構建離線應用

認識離線應用

你的網頁性能優化的再好,若是網絡很差那也會致使網頁的體驗差。 離線應用是指經過離線緩存技術,讓資源在第一次被加載後緩存在本地,下次訪問它時就直接返回本地的文件,就算沒有網絡鏈接。javascript

離線應用有如下優勢:css

  • 在沒有網絡的狀況下也能打開網頁。
  • 因爲部分被緩存的資源直接從本地加載,對用戶來講能夠加速網頁加載速度,對網站運營者來講能夠減小服務器壓力以及傳輸流量費用。

離線應用的核心是離線緩存技術,歷史上曾前後出現2種離線離線緩存技術,它們分別是:html

  1. AppCache 又叫 Application Cache,目前已經從 Web 標準中刪除,請儘可能不要使用它。
  2. Service Workers 是目前最新的離線緩存技術,是 Web Worker 的一部分。 它經過攔截網絡請求實現離線緩存,比 AppCache 更加靈活。它也是構建 PWA 應用的關鍵技術之一。

Service Workers 相比於 AppCache 來講更加靈活,由於它能夠經過 JavaScript 代碼去控制緩存的邏輯。 因爲第1種技術已經廢棄,本節只專一於講解如何用 Webpack 構建使用了 Service Workers 的網頁。java

認識 Service Workers

Service Workers 是一個在瀏覽器後臺運行的腳本,它生命週期徹底獨立於網頁。它沒法直接訪問 DOM,但能夠經過 postMessage 接口發送消息來和 UI 進程通訊。 攔截網絡請求是 Service Workers 的一個重要功能,經過它能完成離線緩存、編輯響應、過濾響應等功能。webpack

想更深刻的瞭解 Service Workers,推薦閱讀文章服務工做線程:簡介git

Service Workers 兼容性

目前 Chrome、Firefox、Opera 都已經全面支持 Service Workers,但對於移動端瀏覽器就不太樂觀了,只有高版本的 Android 支持。 因爲 Service Workers 沒法經過注入 polyfill 去實現兼容,因此在你打算使用它前請先調查清楚你的網頁的運行場景。github

判斷瀏覽器是否支持 Service Workers 的最簡單的方法是經過如下代碼:web

// 若是 navigator 對象上存在 serviceWorker 對象,就表示支持
if (navigator.serviceWorker) {
  // 經過 navigator.serviceWorker 使用
}
複製代碼

註冊 Service Workers

要給網頁接入 Service Workers,須要在網頁加載後註冊一個描述 Service Workers 邏輯的腳本。 代碼以下:chrome

if (navigator.serviceWorker) {
  window.addEventListener('DOMContentLoaded',function() {
    // 調用 serviceWorker.register 註冊,參數 /sw.js 爲腳本文件所在的 URL 路徑
      navigator.serviceWorker.register('/sw.js');
  });
}
複製代碼

一旦這個腳本文件被加載,Service Workers 的安裝就開始了。這個腳本被安裝到瀏覽器中後,就算用戶關閉了當前網頁,它仍會存在。 也就是說第一次打開該網頁時 Service Workers 的邏輯不會生效,由於腳本尚未被加載和註冊,可是之後再次打開該網頁時腳本里的邏輯將會生效。npm

在 Chrome 中能夠經過打開網址 chrome://inspect/#service-workers 來查看當前瀏覽器中全部註冊了的 Service Workers。

使用 Service Workers 實現離線緩存

Service Workers 在註冊成功後會在其生命週期中派發出一些事件,經過監聽對應的事件在特色的時間節點上作一些事情。

在 Service Workers 腳本中,引入了新的關鍵字 self 表明當前的 Service Workers 實例。

在 Service Workers 安裝成功後會派發出 install 事件,須要在這個事件中執行緩存資源的邏輯,實現代碼以下:

// 當前緩存版本的惟一標識符,用當前時間代替
var cacheKey = new Date().toISOString();

// 須要被緩存的文件的 URL 列表
var cacheFileList = [
  '/index.html',
  '/app.js',
  '/app.css'
];

// 監聽 install 事件
self.addEventListener('install', function (event) {
  // 等待全部資源緩存完成時,才能夠進行下一步
  event.waitUntil(
    caches.open(cacheKey).then(function (cache) {
      // 要緩存的文件 URL 列表
      return cache.addAll(cacheFileList);
    })
  );
});
複製代碼

接下來須要監聽網絡請求事件去攔截請求,複用緩存,代碼以下:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // 去緩存中查詢對應的請求
    caches.match(event.request).then(function(response) {
        // 若是命中本地緩存,就直接返回本地的資源
        if (response) {
          return response;
        }
        // 不然就去用 fetch 下載資源
        return fetch(event.request);
      }
    )
  );
});
複製代碼

以上就實現了離線緩存。

更新緩存

線上的代碼有時須要更新和從新發布,若是這個文件被離線緩存了,那就須要 Service Workers 腳本中有對應的邏輯去更新緩存。 這能夠經過更新 Service Workers 腳本文件作到。

瀏覽器針對 Service Workers 有以下機制:

  1. 每次打開接入了 Service Workers 的網頁時,瀏覽器都會去從新下載 Service Workers 腳本文件(因此要注意該腳本文件不能太大),若是發現和當前已經註冊過的文件存在字節差別,就將其視爲「新服務工做線程」。
  2. 新 Service Workers 線程將會啓動,且將會觸發其 install 事件。
  3. 當網站上當前打開的頁面關閉時,舊 Service Workers 線程將會被終止,新 Service Workers 線程將會取得控制權。
  4. 新 Service Workers 線程取得控制權後,將會觸發其 activate 事件。

新 Service Workers 線程中的 activate 事件就是最佳的清理舊緩存的時間點,代碼以下:

// 當前緩存白名單,在新腳本的 install 事件裏將使用白名單裏的 key 
var cacheWhitelist = [cacheKey];

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          // 不在白名單的緩存所有清理掉
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            // 刪除緩存
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
複製代碼

最終完整的代碼 Service Workers 腳本代碼以下:

// 當前緩存版本的惟一標識符,用當前時間代替
var cacheKey = new Date().toISOString();

// 當前緩存白名單,在新腳本的 install 事件裏將使用白名單裏的 key
var cacheWhitelist = [cacheKey];

// 須要被緩存的文件的 URL 列表
var cacheFileList = [
  '/index.html',
  'app.js',
  'app.css'
];

// 監聽 install 事件
self.addEventListener('install', function (event) {
  // 等待全部資源緩存完成時,才能夠進行下一步
  event.waitUntil(
    caches.open(cacheKey).then(function (cache) {
      // 要緩存的文件 URL 列表
      return cache.addAll(cacheFileList);
    })
  );
});

// 攔截網絡請求
self.addEventListener('fetch', function (event) {
  event.respondWith(
    // 去緩存中查詢對應的請求
    caches.match(event.request).then(function (response) {
        // 若是命中本地緩存,就直接返回本地的資源
        if (response) {
          return response;
        }
        // 不然就去用 fetch 下載資源
        return fetch(event.request);
      }
    )
  );
});

// 新 Service Workers 線程取得控制權後,將會觸發其 activate 事件
self.addEventListener('activate', function (event) {
  event.waitUntil(
    caches.keys().then(function (cacheNames) {
      return Promise.all(
        cacheNames.map(function (cacheName) {
          // 不在白名單的緩存所有清理掉
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            // 刪除緩存
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
複製代碼

接入 Webpack

用 Webpack 構建接入 Service Workers 的離線應用要解決的關鍵問題在於如何生成上面提到的 sw.js 文件, 而且sw.js文件中的 cacheFileList 變量,表明須要被緩存文件的 URL 列表,須要根據輸出文件列表所對應的 URL 來決定,而不是像上面那樣寫成靜態值。

假如構建輸出的文件目錄結構爲:

├── app_4c3e186f.js
├── app_7cc98ad0.css
└── index.html
複製代碼

那麼 sw.js 文件中 cacheFileList 的值應該是:

var cacheFileList = [
  '/index.html',
  'app_4c3e186f.js',
  'app_7cc98ad0.css'
];
複製代碼

Webpack 沒有原生功能能完成以上要求,幸虧龐大的社區中已經有人爲咱們作好了一個插件 serviceworker-webpack-plugin 能夠方便的解決以上問題。 使用該插件後的 Webpack 配置以下:

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const { WebPlugin } = require('web-webpack-plugin');
const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin');

module.exports = {
  entry: {
    app: './main.js'// Chunk app 的 JS 執行入口文件
  },
  output: {
    filename: '[name].js',
    publicPath: '',
  },
  module: {
    rules: [
      {
        test: /\.css/,// 增長對 CSS 文件的支持
        // 提取出 Chunk 中的 CSS 代碼到單獨的文件中
        use: ExtractTextPlugin.extract({
          use: ['css-loader'] // 壓縮 CSS 代碼
        }),
      },
    ]
  },
  plugins: [
    // 一個 WebPlugin 對應一個 HTML 文件
    new WebPlugin({
      template: './template.html', // HTML 模版文件所在的文件路徑
      filename: 'index.html' // 輸出的 HTML 的文件名稱
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,// 給輸出的 CSS 文件名稱加上 Hash 值
    }),
    new ServiceWorkerWebpackPlugin({
      // 自定義的 sw.js 文件所在路徑
      // ServiceWorkerWebpackPlugin 會把文件列表注入到生成的 sw.js 中
      entry: path.join(__dirname, 'sw.js'),
    }),
  ],
  devServer: {
    // Service Workers 依賴 HTTPS,使用 DevServer 提供的 HTTPS 功能。
    https: true,
  }
};
複製代碼

以上配置有2點須要注意:

  • 因爲 Service Workers 必須在 HTTPS 環境下才能攔截網絡請求實現離線緩存,使用在 2-6 DevServer https 中提到的方式去實現 HTTPS 服務。
  • serviceworker-webpack-plugin 插件爲了保證靈活性,容許使用者自定義 sw.js,構建輸出的 sw.js 文件中會在頭部注入一個變量 serviceWorkerOption.assets 到全局,裏面存放着全部須要被緩存的文件的 URL 列表。

須要修改上面的 sw.js 文件中寫成了靜態值的 cacheFileList 爲以下:

// 須要被緩存的文件的 URL 列表
var cacheFileList = global.serviceWorkerOption.assets;
複製代碼

以上已經完成全部文件的修改,在從新構建前,先安裝新引入的依賴:

npm i -D serviceworker-webpack-plugin webpack-dev-server
複製代碼

安裝成功後,在項目根目錄下執行 webpack-dev-server 命令後,DevServer 將以 HTTPS 模式啓動,並輸出以下日誌:

> webpack-dev-server

Project is running at https://localhost:8080/
webpack output is served from /
Hash: 402ee6ce5bffb16dffe2
Version: webpack 3.5.5
Time: 619ms
     Asset       Size  Chunks                    Chunk Names
    app.js     325 kB       0  [emitted]  [big]  app
   app.css   21 bytes       0  [emitted]         app
index.html  235 bytes          [emitted]         
     sw.js    4.86 kB          [emitted]         
複製代碼

用 Chrome 瀏覽器打開網址 https://localhost:8080/index.html 後,就能訪問接入了 Service Workers 離線緩存的頁面了。

驗證結果

爲了驗證 Service Workers 和緩存生效了,須要經過 Chrome 的開發者工具來查看。

經過打開開發者工具的 Application-Service Workers 一欄,就能看到當前頁面註冊的 Service Workers,正常的效果如圖:

圖3.12.1 查看當前頁面註冊的 Service Workers

經過打開開發者工具的 Application-Cache-Cache Storage 一欄,能看到當前頁面緩存的資源列表,正常的效果如圖:

圖3.12.2 查看當前頁面的 Cache Storage

爲了驗證網頁在離線時能訪問的能力,須要在開發者工具中的 Network 一欄中經過 Offline 選項禁用掉網絡,再刷新頁面能正常訪問,而且網絡請求的響應都來自 Service Workers,正常的效果如圖:

圖3.12.3 離線狀況下訪問頁面

本實例提供項目完整代碼

《深刻淺出Webpack》全書在線閱讀連接

閱讀原文

相關文章
相關標籤/搜索