[Blazor] PWA 靜態檔案問題
在 Blazor WebAssembly 中可以開啟 PWA 的模式,也就是會產生 Service Worker,要加入像是 Excel 或是 PDF 到靜態檔案,會發現在本機開發時都能夠正常下載的檔案,怎麼上到掛 domain 的 server 就會去走 routing,跟預期的差很多,今天會來解析中間發生了什麼事情。
以下版本為 Dotnet 7.0.4
Reproduce
專案建置
先來試著模擬一下情境,為了更明確的知道其中的差異性,接下來會需要建置兩個專案(或是同一個專案改成 PWA)。
1 | 沒有 PWA |
到人事行政局下載 2024 年(民國 113 年)的行事曆,Excel 或是 PDF 都可以。
把檔案放到 wwwroot 底下,並且在 Index.razor
加上一個下載的連結。
1 | @page "/" |
兩個專案都要做一次同樣的事情,並且用 dotnet run
來確認結果。
模擬server
接著要來模擬 deploy 到 server 後的情境,搭配使用 ngrok 工具來實現 domain 的情境。
執行 publish
的指令,並且將輸出目錄指定為 Release
。
後面再來說為什麼要用這樣的模式
1 | dotnet publish -c Release -o Release |
先執行看看,確保網站能夠正確的呈現,接著要啟動 ngrok 來做 reverse proxy。
1 | ngrok http 8080 |
依序執行兩個專案,記得到 sample2 的時候要強制 refresh 才會吃到正確的內容。
簡單一點也可以是關掉 ngrok 重新執行,會取得新的 domain
用 domain 開啟 sample2 網站,試著點擊行事曆的連結,就會發現到系統竟然是重新載入頁面並且說找不到路由。
##Root Cause
使用 localhost 開啟不管是不是 PWA 網站都沒事,只要用 Domain 開啟 PWA 就會發生路由問題,這到底是為什麼?
PWA
按照前面來看最大問題點應該是 PWA 及 Domain,那就先來看看微軟如何介紹 PWA,並且搞清楚有沒有 PWA 的專案差異在哪。
- Working offline and loading instantly, independent of network speed.
- Running in its own app window, not just a browser window.
- Being launched from the host’s operating system start menu, dock, or home screen.
- Receiving push notifications from a backend server, even while the user isn’t using the app.
- Automatically updating in the background.
跟一般的網頁最大差異有兩個,第一個是可以離線運作,第二個是可以安裝。
接著要來看看差異是什麼,從檔案的差異來看,可以看到 wwwroot 底下多了兩個檔案,service-worker.js
及 service-worker.published.js
。
csproj
裡面的設定也有兩個地方不同,都是跟 service-worker
有關係。
1 | <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> |
Service Worker
既然重點會是在 Service Worker,那就來看看 MDN 的介紹。
Secure context: This feature is available only in secure contexts (HTTPS), in some or all supporting browsers.
在最開頭就有說到,這個功能只會在 HTTPS
的環境下啟動,那這樣也能夠解釋為什麼 localhost 執行會是正常。
接著的問題就會是,為什麼會去走路由,而不是直接下載檔案。分別來看看兩個 service-worker.*.js
裡面做了什麼事情。
service-worker.js
在開發模式下,就會是這個檔案內容,可以看到的是根本沒有關於到 service worker 的功能,因此在開發階段怎麼樣的測試都會是正常的。
1 | // In development, always fetch from the network and do not enable offline support. |
service-woker.published.js
發行過後,dotnet 編譯會將這個檔案內容取代原本的 service-worker.js
,底下就試著去搞懂這段邏輯在做些什麼。
1 | // Caution! Be sure you understand the caveats before publishing an application with |
首先,看到 import 一個額外的檔案 service-worker-assets.js
,但在 source code 裡面沒找到,可以先推測是編譯的時候才產生。往下看到 offlineAssetsInclude
這個變數,宣告各種常見的靜態檔案,像是圖片或是字形檔;offlineAssetsExclude
變數則是把 service-worker.js
宣告為不可快取,畢竟 service-worker 一旦被快取,那 server 有更新的話就會沒跟上。
底下的三個方法,就是決定哪些請求是可以直接走快取,哪些則是要去跟 server 做請求。
onInstall
當 service worker 安裝的時候,會把 service-worker-assets.js
檔案中設定且符合 offlineAssetsInclude
的檔案加入快取清單。
onActivate
清除掉已經無效的快取。
onFetch
請求是 GET 的情況下,會檢查請求路徑是否符合快取的檔案,如果是的話則直接取得檔案;其他則是直接跟 server 做請求。
service-worker-assets.js
如前面所說,這個檔案是編譯才會有的,在 sample2/Release 資料夾找到該檔案。
檔案結構很簡單,就是 assetsManifest
這個物件中有兩個屬性 assets
及 version
。可以在 assets 中看到 wwwroot 底下的檔案幾乎都有被放進去,唯獨我們這次的目標 113年辦公日曆表.pdf
並沒有在清單中,依照前面的 onFetch
邏輯,這時候自然會直接跟 server 做請求也就是會走路由的行為,並且回報路由錯誤。
1 | self.assetsManifest = { |
Fix
前面講了那麼多就是為了要搞懂原理,接下來就是修正問題,service-worker-assets.js
是編譯才產生,因此需要想辦法讓我們新增的檔案能夠被加入,在微軟的文件中其實有提到,如何增加靜態檔案的快取。
在 csproj
中設定 ServiceWorkerAssetsManifestItem
檔案以及輸出的路徑。
1 | <ItemGroup> |
設定後,執行 publish
的指令。
1 | 習慣先清除,避免有不預期的檔案影響 |
執行指令不用急著去把系統跑起來,可以先看看 service-worker-assets.js
有沒有如預期地把檔案加進去,使用搜尋的方式找 pdf
字串,就能找到檔案如預期的被加入了。
1 | self.assetsManifest = { |
接著使用前面架網站的方式來啟動,用瀏覽器的方式來看是否可以正常下載;可以發現還是失敗,偵錯 onFetch
看到 request.mode
是 navigate
,因此會去走一般的請求。
解法一
調整 onFetch
的邏輯,讓 pdf 副檔名可以走快取的邏輯。
1 | async function onFetch(event) { |
如果有多個檔案的話,建議用一個資料夾會比較好控制
解法二
前面的問題在 Dotnet 8 有被調整過,可以參考這個 github issue,基本上就是有在 assets 控制下的就會採用快取,底下是 Dotnet 8 部分修正的內容。
個人覺得這個解法才是正確的
1 | const base = "/"; |
解法三
這個解法應該是最簡單的,採用標準 html 的作法,也就是加上 download
的屬性。
murmur: 雖然這是標準但就算沒有加也不應該去走路由阿
1 | <a href="113年辦公日曆表.pdf" download>台灣 2024 年行事曆</a> |