標籤:

Webpack實戰-構建離線應用

認識離線應用

你的網頁性能優化的再好,如果網路不好那也會導致網頁的體驗差。

離線應用是指通過離線緩存技術,讓資源在第一次被載入後緩存在本地,下次訪問它時就直接返回本地的文件,就算沒有網路連接。

離線應用有以下優點:

  • 在沒有網路的情況下也能打開網頁。
  • 由於部分被緩存的資源直接從本地載入,對用戶來說可以加速網頁載入速度,對網站運營者來說可以減少伺服器壓力以及傳輸流量費用。

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

  1. AppCache 又叫 Application Cache,目前已經從 Web 標準中刪除,請盡量不要使用它。
  2. Service Workers 是目前最新的離線緩存技術,是 Web Worker 的一部分。

它通過攔截網路請求實現離線緩存,比 AppCache 更加靈活。它也是構建 PWA 應用的關鍵技術之一。

Service Workers 相比於 AppCache 來說更加靈活,因為它可以通過 JavaScript 代碼去控制緩存的邏輯。

由於第1種技術已經廢棄,本節只專註於講解如何用 Webpack 構建使用了 Service Workers 的網頁。

認識 Service Workers

Service Workers 是一個在瀏覽器後台運行的腳本,它生命周期完全獨立於網頁。它無法直接訪問 DOM,但可以通過 postMessage 介面發送消息來和 UI 進程通信。

攔截網路請求是 Service Workers 的一個重要功能,通過它能完成離線緩存、編輯響應、過濾響應等功能。

想更深入的了解 Service Workers,推薦閱讀文章服務工作線程:簡介。

Service Workers 兼容性

目前 Chrome、Firefox、Opera 都已經全面支持 Service Workers,但對於移動端瀏覽器就不太樂觀了,只有高版本的 Android 支持。

由於 Service Workers 無法通過注入 polyfill 去實現兼容,所以在你打算使用它前請先調查清楚你的網頁的運行場景。

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

// 如果 navigator 對象上存在 serviceWorker 對象,就表示支持nif (navigator.serviceWorker) {n // 通過 navigator.serviceWorker 使用n}n

註冊 Service Workers

要給網頁接入 Service Workers,需要在網頁載入後註冊一個描述 Service Workers 邏輯的腳本。

代碼如下:

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

一旦這個腳本文件被載入,Service Workers 的安裝就開始了。這個腳本被安裝到瀏覽器中後,就算用戶關閉了當前網頁,它仍會存在。

也就是說第一次打開該網頁時 Service Workers 的邏輯不會生效,因為腳本還沒有被載入和註冊,但是以後再次打開該網頁時腳本里的邏輯將會生效。

在 Chrome 中可以通過打開網址 chrome://inspect/#service-workers 來查看當前瀏覽器中所有註冊了的 Service Workers。

使用 Service Workers 實現離線緩存

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

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

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

// 當前緩存版本的唯一標識符,用當前時間代替nvar cacheKey = new Date().toISOString();nn// 需要被緩存的文件的 URL 列表nvar cacheFileList = [n /index.html,n /app.js,n /app.cssn];nn// 監聽 install 事件nself.addEventListener(install, function (event) {n // 等待所有資源緩存完成時,才可以進行下一步n event.waitUntil(n caches.open(cacheKey).then(function (cache) {n // 要緩存的文件 URL 列表n return cache.addAll(cacheFileList);n })n );n});n

接下來需要監聽網路請求事件去攔截請求,復用緩存,代碼如下:

self.addEventListener(fetch, function(event) {n event.respondWith(n // 去緩存中查詢對應的請求n caches.match(event.request).then(function(response) {n // 如果命中本地緩存,就直接返回本地的資源n if (response) {n return response;n }n // 否則就去用 fetch 下載資源n return fetch(event.request);n }n )n );n});n

以上就實現了離線緩存。

更新緩存

線上的代碼有時需要更新和重新發布,如果這個文件被離線緩存了,那就需要 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 nvar cacheWhitelist = [cacheKey];nnself.addEventListener(activate, function(event) {n event.waitUntil(n caches.keys().then(function(cacheNames) {n return Promise.all(n cacheNames.map(function(cacheName) {n // 不在白名單的緩存全部清理掉n if (cacheWhitelist.indexOf(cacheName) === -1) {n // 刪除緩存n return caches.delete(cacheName);n }n })n );n })n );n});n

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

// 當前緩存版本的唯一標識符,用當前時間代替nvar cacheKey = new Date().toISOString();nn// 當前緩存白名單,在新腳本的 install 事件里將使用白名單里的 keynvar cacheWhitelist = [cacheKey];nn// 需要被緩存的文件的 URL 列表nvar cacheFileList = [n /index.html,n app.js,n app.cssn];nn// 監聽 install 事件nself.addEventListener(install, function (event) {n // 等待所有資源緩存完成時,才可以進行下一步n event.waitUntil(n caches.open(cacheKey).then(function (cache) {n // 要緩存的文件 URL 列表n return cache.addAll(cacheFileList);n })n );n});nn// 攔截網路請求nself.addEventListener(fetch, function (event) {n event.respondWith(n // 去緩存中查詢對應的請求n caches.match(event.request).then(function (response) {n // 如果命中本地緩存,就直接返回本地的資源n if (response) {n return response;n }n // 否則就去用 fetch 下載資源n return fetch(event.request);n }n )n );n});nn// 新 Service Workers 線程取得控制權後,將會觸發其 activate 事件nself.addEventListener(activate, function (event) {n event.waitUntil(n caches.keys().then(function (cacheNames) {n return Promise.all(n cacheNames.map(function (cacheName) {n // 不在白名單的緩存全部清理掉n if (cacheWhitelist.indexOf(cacheName) === -1) {n // 刪除緩存n return caches.delete(cacheName);n }n })n );n })n );n});n

接入 Webpack

用 Webpack 構建接入 Service Workers 的離線應用要解決的關鍵問題在於如何生成上面提到的 sw.js 文件,

並且sw.js文件中的 cacheFileList 變數,代表需要被緩存文件的 URL 列表,需要根據輸出文件列表所對應的 URL 來決定,而不是像上面那樣寫成靜態值。

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

├── app_4c3e186f.jsn├── app_7cc98ad0.cssn└── index.htmln

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

var cacheFileList = [n /index.html,n app_4c3e186f.js,n app_7cc98ad0.cssn];n

Webpack 沒有原生功能能完成以上要求,幸好龐大的社區中已經有人為我們做好了一個插件 serviceworker-webpack-plugin 可以方便的解決以上問題。

使用該插件後的 Webpack 配置如下:

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

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

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

需要修改上面的 sw.js 文件中寫成了靜態值的 cacheFileList 為如下:

// 需要被緩存的文件的 URL 列表nvar cacheFileList = global.serviceWorkerOption.assets;n

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

npm i -D serviceworker-webpack-plugin webpack-dev-servern

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

> webpack-dev-servernnProject is running at https://localhost:8080/nwebpack output is served from /nHash: 402ee6ce5bffb16dffe2nVersion: webpack 3.5.5nTime: 619msn Asset Size Chunks Chunk Namesn app.js 325 kB 0 [emitted] [big] appn app.css 21 bytes 0 [emitted] appnindex.html 235 bytes [emitted] n sw.js 4.86 kB [emitted] n

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

驗證結果

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

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

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

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

本實例提供項目完整代碼

《深入淺出Webpack》全書在線閱讀鏈接

閱讀原文


推薦閱讀:

加速Webpack-縮小文件搜索範圍
如果HTTP2普及了,Webpack、Rollup這種打包工具還有意義嗎?
Webpack 之 Loader 的使用

TAG:webpack | PWA |