記一次基於react、cra2、typescript的pwa專案由開發到部署(二)
在上一篇文章 ofollow,noindex">記一次基於react、cra2、typescript的pwa專案由開發到部署(一) 中,我們瞭解到了create-react-app 給我們提供了哪些pwa支援,也瞭解到了有哪些不足。雖然create-react-app會幫我們自動生成一個service-worker.js 去快取我們的app shell,但是並沒有提供讓開發者定製service worker的方法,除非我們eject專案,這篇文章繼續往下講,把在這個專案中學到的東西分享給大家。
專案回顧
這是一個移動端的pwa應用,使用react,typescript,react-redux,react-router,workbox 基於create-react-app 開發。可以新增到主螢幕,可以斷網條件下正常開啟和訪問資料。專案地址: React" rel="nofollow,noindex">browseExpbyReact


使用typescript
typescript是JavaScript的超級,一方面在typescript中我們可以使用最新的特性,另一方面typescript給我們帶來了型別系統,可以讓我們寫出健壯的程式碼,避免一些潛在的執行時錯誤。在create-react-app中使用typescript,官網推薦我們使用的是 TypeScript-React-Starter" rel="nofollow,noindex">create-react-app的ts版本 ,他會幫你配置好typescript的相關配置,並使用react-script-ts代替react-script來驅動專案。但是這個版本的更新會稍稍滯後於原版,而且也不利於我們擴充套件腳手架的配置,所以這裡不推薦使用。我們使用 react-app-rewired 來進行配置。
react-app-rewired
在create-react-app中修改預設配置有兩種常用的方法,
- 一種是 eject 專案,eject會把我們的腳手架中的配置暴露出來,然後我們就可以去修改了,但是這是一個不可逆的過程,而且講配置暴露出來也是一個不優雅的做法,所以不推薦。
- 第二種就是利用 react-app-rewired 去修改我們的配置,他可以讓我們在不eject專案的前提下修改我們的配置。比如配置typescript,我們可以找到對應的外掛 react-app-rewire-typescript 進行配置。具體可參考本專案
利用workbox 定製自己的service worker
這裡到了本文的重點:如何在create-react-app中定製自己的service-worker.js。目前的cra引用了Workbox webpack plugin 代替了先前的 sw-precache-webpack-plugin。我們可以藉助 react-app-rewired 去改寫預設的Workbox webpack plugin 配置。主要步驟:
- 在 react-app-rewired 的配置檔案 config.overrides.js 中修改 Workbox webpack plugin的配置
- 在public檔案目錄下建立自己的service-worker配置檔案
首先在 config.overrides.js 中配置,替換預設的workbox-webpack-plugin配置:
/* config-overrides.js */ // typescript的配置外掛 const rewireTypescript = require('react-app-rewire-typescript'); const workboxPlugin = require('workbox-webpack-plugin') const path = require('path') module.exports = { webpack: function (config, env) { // typescript的配置外掛 config = rewireTypescript(config, env); if (env === 'production') { // 在 ‘production’ 模式下加入自己的配置 const workboxConfigProd = { swSrc: path.join(__dirname, 'public', 'cus-service-worker.js'), swDest: 'cus-service-worker.js', importWorkboxFrom: 'disabled' } // 刪除預設的WorkboxWebpackPlugin配置 config = removePreWorkboxWebpackPluginConfig(config) // 加入我們的配置 config.plugins.push(new workboxPlugin.InjectManifest(workboxConfigProd)) } return config } } // 此函式用來找出 預設配置中的 WorkboxWebpackPlugin, 並把它刪除 function removePreWorkboxWebpackPluginConfig (config) { const preWorkboxPluginIndex = config.plugins.findIndex((element) => { return Object.getPrototypeOf(element).constructor.name === 'GenerateSW' }) if (preWorkboxPluginIndex !== -1) { config.plugins.splice(preWorkboxPluginIndex, 1) } return config } 複製程式碼
這部分的配置大概意思就是,當環境為生成環境時,找出webpack中關於workbox-webpack-plugin的配置,把它刪掉,然後用自己的配置替代它。
這裡解釋一下 removePreWorkboxWebpackPluginConfig 這個函式。我們可以自己用create-react-app新建一個無用的專案,然後eject它,那麼我們可以在暴露出來的config資料夾下的 webpack.config.prod.js 中看到關於 workbox-webpack-plugin 的配置
new WorkboxWebpackPlugin.GenerateSW({ clientsClaim: true, exclude: [/\.map$/, /asset-manifest\.json$/], importWorkboxFrom: 'cdn', navigateFallback: publicUrl + '/index.html', navigateFallbackBlacklist: [ // Exclude URLs starting with /_, as they're likely an API call new RegExp('^/_'), // Exclude URLs containing a dot, as they're likely a resource in // public/ and not a SPA route new RegExp('/[^/]+\\.[^/]+$'), ], }), 複製程式碼
所以我們可以通過下面這段程式碼找到這段配置的位置:
// 對plugins陣列呼叫findIndex方法,找到建構函式的name屬性為‘GenerateSW’的成員 const preWorkboxPluginIndex = config.plugins.findIndex((element) => { return Object.getPrototypeOf(element).constructor.name === 'GenerateSW' }) // 刪除這個成員 if (preWorkboxPluginIndex !== -1) { config.plugins.splice(preWorkboxPluginIndex, 1) } 複製程式碼
替換掉workbox-webpack-plugin的配置後,根據自己的配置在public目錄下新建cus-service-worker.js檔案,這個檔案會代替預設生成的service-worker.js檔案,我們就可以通過配置cus-service-worker.js來定製自己的pwa配置了,而且cus-service-worker.js 裡的內容也是有講究的,以本專案為例:
// 引入workbox全域性變數 importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js'); if (workbox) { console.log(`Yay! Workbox is loaded :tada:`); } else { console.log(`Boo! Workbox didn't load :grimacing:`); } // set the prefix and suffix of our sw's name workbox.core.setCacheNameDetails({ prefix: 'browse-exp', suffix: 'v1.0.0', }); // have our sw update and control a web page as soon as possible. workbox.skipWaiting(); workbox.clientsClaim(); // 將靜態資源進行預快取 self.__precacheManifest = [].concat(self.__precacheManifest || []); workbox.precaching.suppressWarnings(); workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); // 定製自己的需求 // cache our data, and use networkFirst strategy. workbox.routing.registerRoute( new RegExp('.*experiments\?.*'), workbox.strategies.networkFirst() ); workbox.routing.registerRoute( new RegExp('.*experiments/\\d'), workbox.strategies.networkFirst() ) workbox.routing.registerRoute( new RegExp('.*experiment_types.*'), workbox.strategies.networkFirst() ) 複製程式碼
首先通過importScripts 引入workbox全域性變數。在打包的時候,腳手架會為我們生成一個 precache-manifest列表,裡面會列舉一系列的靜態檔案,我們可以通過 self.__precacheManifest 拿到這個列表, 所以我們需要通過一下語句預快取這些靜態資源:
self.__precacheManifest = [].concat(self.__precacheManifest || []); workbox.precaching.suppressWarnings(); workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 複製程式碼
然後就是為了儘快的讓我們的service worker控制頁面,我們可以在開頭加入一下語句:
// 跳過等待 workbox.skipWaiting(); // 控制客戶端 workbox.clientsClaim(); 複製程式碼
剩下的部分自己就可以按自己的需求進行發揮了,像要什麼功能就配置什麼功能,這裡的話我為自己獲取資料的路由進行了快取,採用的是 networkFirst 策略,什麼是networkFirst策略呢?就是首先會進行網路請求,如果失敗的話再使用快取中的資料。

當我們打包專案的時候,就會發現再build檔案下,會生成一個cus-service-worker.js檔案,並且再開頭多了一句:
importScripts("/precache-manifest.cd8115bc0ff644d6d74bec08ffcbdeb4.js"); 複製程式碼
這就是我們可以通過 self.__precacheManifest 拿到預快取列表的原因。
到目前為止:我們已經可以定製自己的service-worker.js了。
manifest.json
manifest.json可以讓我們的web app新增到桌面,再create-react-app中配置manifest非常簡單,直接再public目錄下的manifest.json配置就可以了,關於什麼麼配置項,可以到這裡谷歌官網教程檢視,另外manifest.json的配置不會馬上生效,需要在https協議下,多次進入該網頁的時候才會彈出新增到桌面的提示。
總結:
- 利用 react-app-rewired 改寫我們的配置
- 在 config.overrides.js 中替換預設的 WorkboxWebpackPlugin 的配置
- 在 public 目錄下編寫自己的pwa配置
到這裡我們可以在create-react-app生成的腳手架中定製自己的pwa配置了,在 下一篇文章 中,我會繼續講解:
- 如何部署將該專案部署到nginx伺服器上。
- 為它配置證書,讓它執行在https協議上。
- 體驗該pwa專案。
感興趣的同學可以掃描下面二維碼體驗專案:
note:
- 建議用uc瀏覽器開啟,因為uc瀏覽器對pwa的支援較好。
- "新增到桌面的提示" 需要短時間多次進入web app 才會觸發

專案地址: browseExpByReact 如果感興趣,可以對比著基於vue的實現來看: browseExpByVue