PWA 漸進式實踐 (2) - Service Worker

作為 PWA 的象徵之一,我們首先做的,就是加上 Service Worker。

添加 Service Worker

Service worker是一個註冊在指定源和路徑下的事件驅動worker。它採用JavaScript控制關聯的頁面或者網站,攔截並修改訪問和資源請求,細粒度地緩存資源。你可以完全控制應用在特定情形(最常見的情形是網路不可用)下的表現。

Service worker運行在worker上下文,因此它不能訪問DOM。相對於驅動應用的主JavaScript線程,它運行在其他線程中,所以不會造成阻塞。它設計為完全非同步,同步API(如XHR和localStorage)不能在service worker中使用。

出於安全考量,Service workers只能由HTTPS承載,畢竟修改網路請求的能力暴露給中間人攻擊會非常危險。在Firefox瀏覽器的用戶隱私模式,Service Worker不可用。

—— MDN

通過 Service Worker,我們可以直接擺脫服務端,不需要 304 一樣能使用本地緩存。不僅如此,還能執行網頁本身行為外的腳本(不過不能訪問DOM),比如預先抓取後面可能要訪問的資源,甚至在飛行模式下同樣能使用 webapp。

註冊

我們的項目是使用 ejs 在 webpack 階段注入幾個變數生成最後的 index.html 的,所以直接拿 index.ejs 動刀即可:

<body> <div id="container"></div> <script src="<%= bundle %>"></script> <script> if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/service-worker.js", { scope: "./" }).then(function(registration) { registration.onupdatefound = function() { if (navigator.serviceWorker.controller) { const installingWorker = registration.installing; installingWorker.onstatechange = function() { switch (installingWorker.state) { case "installed": break; case "redundant": throw new Error("The installing " + "service worker became redundant."); default: // Ignore } }; } }; }).catch(function(e) { console.error("Error during service worker registration:", e); }); } else { console.log("service worker is not supported"); } </script>

即 body 中,第二個 script 標籤的內容,其參數 service-worker.js,是與 index.html 在同一個目錄的空文件:

// This file is intentionally without code.// It"s present so that service worker registration// will work when serving from the "public" directory.

實際上打包後會生成真正的 service-worker.js,所以現在只是用來佔個位子。

納尼?這樣就好了?

確實,這樣,我們就已經完成了註冊,這也是 PWA 和微信小程序這種二流方案不同的地方,其更注重於如何提高現有設計實現下的體驗,使用開放的標準並進行推進。

Cache 策略

下一步就是增加我們的緩存策略了,我們需要安裝 2 個小工具:

npm install sw-precache --savenpm install sw-toolbox --save

然後在 package.json 裡面更新一下我們的 script:

"scripts": { "build": "npm run copy && node run build && npm run precache", "build:debug": "npm run copy && node run build --debug && npm run precache", "copy": "cp node_modules/sw-toolbox/sw-toolbox.js public/sw-toolbox.js", "precache": "./node_modules/sw-precache/cli.js --root=public --config=sw-precache-config.json"}

如上,增加 copy 和 precache 任務,並更新 build,在 build 前後插入新的 task。

然後就是配置文件了,在項目目錄下,增加 sw-precache-config.json 文件:

{ "staticFileGlobs": [ "public/dist/**.css", "public/dist/**.png", "public/dist/**.js" ], "importScripts": [ "sw-toolbox.js", "runtime-caching.js" ], "stripPrefix": "public/", "verbose": true}

在 public 目錄下,增加 runtime-caching.js 文件:

// global.toolbox is defined in a different script, sw-toolbox.js, which is part of the// GoogleChrome/sw-toolbox project.// That sw-toolbox.js script must be executed first, so it needs to be listed before this in the// importScripts() call that the parent service worker makes.(function (global) { // See GoogleChrome/sw-toolbox // and https://github.com/GoogleChrome/sw-toolbox/blob/6e8242dc328d1f1cfba624269653724b26fa94f1/README.md#toolboxfastest // for more details on how this handler is defined and what the toolbox.fastest strategy does. global.toolbox.router.get("/(.*)", global.toolbox.fastest, { origin: /.(?:googleapis|gstatic|firebaseio|appspot).com$/, }); global.toolbox.router.get("/(.+)", global.toolbox.fastest, { origin: "https://api.pai.bigins.cn/", }); global.toolbox.router.get("/(.+)", global.toolbox.fastest, { origin: "https://qa.api.pai.bigins.cn/", }); global.toolbox.router.get("/*", global.toolbox.fastest);}(self));

運行一下 npm run build,發現 service-worker.js 被更新了,裡面是生成的策略腳本。

評測

再次運行 Lighthouse 後,發現我們的評分已經嗖嗖嗖上去了:

離線依然返回 200

這裡的秘密就在 runtime-caching.js 文件里,我們更新一下:

// global.toolbox is defined in a different script, sw-toolbox.js, which is part of the// GoogleChrome/sw-toolbox project.// That sw-toolbox.js script must be executed first, so it needs to be listed before this in the// importScripts() call that the parent service worker makes.(function (global) { // See GoogleChrome/sw-toolbox // and https://github.com/GoogleChrome/sw-toolbox/blob/6e8242dc328d1f1cfba624269653724b26fa94f1/README.md#toolboxfastest // for more details on how this handler is defined and what the toolbox.fastest strategy does. global.toolbox.router.get("/(.*)", global.toolbox.fastest, { origin: /.(?:googleapis|gstatic|firebaseio|appspot).com$/, }); global.toolbox.router.get("/(.+)", global.toolbox.fastest, { origin: "https://api.pai.bigins.cn/", }); global.toolbox.router.get("/(.+)", global.toolbox.fastest, { origin: "https://qa.api.pai.bigins.cn/", }); global.toolbox.router.get("/(.+)", global.toolbox.fastest, { origin: "海綿保保", }); global.toolbox.router.get("/*", global.toolbox.fastest); global.toolbox.precache(["/index.html", "/index.css", "/img/logo.png"]);}(self));

然後再提交構建一下,在 Chrome 的 Network Panel 中,勾選 Offline,然後刷新頁面,哇,依然可以用誒。

評測

通過完善 Service Worker,我們的評分已經嗖嗖嗖上了80,達到了83分。

What』s next

剩下的就是一些比較棘手的性能和體驗問題了,我們下回見。


推薦閱讀:

原生addEventListener比jq的on慢了60倍, 為什麼?
前端打包如何在減少請求數與利用並行下載之間找到最優解?
怎麼樣向不懂前端的人介紹前端?
前端開發中,對圖片的優化技巧有哪些?
Google 的 HTML 代碼看著很亂,為什麼要寫成這樣呢?

TAG:前端开发 | 前端性能优化 | React |