前言
最近公司項目迭代逐漸放緩,下班時間逐漸變早,所以本着漸進增加的理念,在下班後,將公司項目進行了一下PWA改造
為何要改造成PWA
- 用户需求。我們的用户有許多電腦小白,不想記網址,又不會使用瀏覽器的收藏功能。以前使用的同類軟件都有桌面版,有一種覺得桌面版比網頁版可靠,使用簡單的錯覺,曾多次在釘釘售後羣裏反映,如何將網頁保存至桌面,方便他下次直接在桌面打開
- PWA是漸進式的,如果用户的瀏覽器不支持ServiceWorker等構建PWA所需的API,並不會對其造成使用上的影響,並且通過埋點平台獲知,我們用户Chrome瀏覽器數量佔到80%左右
- 離線緩存,可安裝,可攔截fetch等功能,對我個人有一定的吸引力,希望學習使用
開始改造
為了快速改造成PWA, 我這裏選擇使用了谷歌推出的PWA工具庫workbox, 並且結合webpack創建serviceWorker文件
安裝依賴
npm install --save-dev workbox-webpack-plugin
npm install --save workbox-core workbox-routing workbox-strategies workbox-precaching workbox-expiration workbox-cacheable-response
workbox-webpack-plugin裏提供了兩種插件,GenerateSW以及InjectManifest。
GenerateSW
GenerateSW插件可以通過配置直接編譯生成對應的serviceWorker文件,不需要我們直接編寫serviceWorker文件。使用方式大致如下:
import { InjectManifest } from 'workbox-webpack-plugin';
new GenerateSW({
skipWaiting: true,
clientsClaim: true,
mode: 'development',
runtimeCaching: [
{
urlPattern: /^https?\:\/\/.+?\.alicdn.com\/.+$/,
handler: 'StaleWhileRevalidate'
},
],
});
通過GenerateSW編譯生成serviceWorker文件雖然簡單,但不夠靈活,所以實際上我使用了另一個InjectManifestPlugin插件
InjectManifest
InjectManifest主要做了兩件事
- 將webpack編譯生成的資源文件清單,以變量
self.__WB_MANIFEST的形式注入到我們提供的serviceWorker模板文件中 - 編譯我們提供的模板文件,生成目標
serviceWorker文件
使用方式大致如下:
const { InjectManifest } = require('workbox-webpack-plugin');
new InjectManifest({
swSrc: path.resolve('src/sw.js'),
swDest: path.resolve(BUILD_DEST, 'sw.js'),
}),
編寫serviceWorker模板文件
預緩存靜態資源
預緩存會在serviceWork激活後,立即請求並緩存所有預緩存清單中的文件, 之後下載請求同一資源時,會使用緩存優先策略,優先使用已經預緩存的資源
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
路由請求緩存
- 使用NavigationRoute來緩存html文件
registerRoute(
new NavigationRoute(
new NetworkFirst({
cacheName: 'navigation-cache',
plugins: [
new CacheableResponsePlugin({
statuses: [200]
}),
new ExpirationPlugin({
maxEntries: 300,
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
}),
),
);
- 緩存本地靜態資源文件
registerRoute(
/\.(css|js|png|jpg|jpeg|svg|webp)$/,
new CacheFirst({
cacheName: 'static-cache',
plugins: [
new CacheableResponsePlugin({
statuses: [200]
}),
new ExpirationPlugin({
maxEntries: 300,
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
}),
);
- 緩存cdn中的靜態資源文件
registerRoute(
/^https?\:\/\/.*?\.alicdn.com\/.+?\.(css|js|png|jpg|jpeg|svg|gif|webp)$/,
new CacheFirst({
cacheName: 'alicdn-cache',
plugins: [
new CacheableResponsePlugin({
statuses: [200]
}),
new ExpirationPlugin({
maxEntries: 300,
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
}),
);
這裏有一個需要注意的點,alicdn靜態資源與我司網頁域名不是同域名,存在跨域,當請求靜態資源的時候,會返回不透明響應(opaque response); 當我們使用Cache-First策略緩存不透明響應時,workbox會提示我們不要使用這個策略來緩存不透明響應,因為不透明響應對JavaScript來説是一個黑盒,無法獲取到正確的status code, headers, body, 所以我們緩存中的資源是不可靠的;並且當我們緩存不透明響應時,緩存所佔有的空間遠大於實際資源的大小,容易造成DOMException: Quota exceeded. 所以需要處理下不透明響應的緩存
不透明響應變成透明響應
既然不透明響應會造成問題,那隻要把不透明響應變成透明響應,那就應該沒問題了。
經過查看,我發現alicdn的響應頭會返回access-control-allow-origin: *, 後端是支持cors跨域資源共享的。既然如此,只要當我們請求靜態資源的時候,讓請求走cors應該就可以了。於是,我嘗試在其中一個img標籤中,啓用cors
<img crossorigin="anonymous" />
不透明響應成功變成透明響應。但如果給所有<img /><script /><link />標籤添加crossorigin, 這工作量也太大了。有沒有統一處理的方法呢?有。可以通過攔截fetch請求來統一處理, 在使用workbox的場景下,可以通過設置緩存策略類中fetchOptions來實現
registerRoute(
/^https?\:\/\/.*?\.alicdn.com\/.+?\.(css|js|png|jpg|jpeg|svg|gif|webp)$/,
new CacheFirst({
cacheName: 'alicdn-cache',
plugins: [
new CacheableResponsePlugin({
statuses: [200]
}),
new ExpirationPlugin({
maxEntries: 300,
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
// 添加如下fetch options
fetchOptions: {
mode: 'cors',
credentials: 'omit',
},
}),
);
創建manifest.json文件
通過manifest配置文件,可以指定pwa應用的圖標,初始頁面,背景色,主題色,顯示模式等內容
// manifest.json
{
"name": "xxx",
"short_name": "xxx",
"icons": [
{
"src": "/static/images/favicon@144x144.png",
"sizes": "144x144",
"type": "image/png"
}
],
"start_url": "/index.html",
"display": "standalone",
"background_color": "#000",
"theme_color": "#000"
}
<link rel="manifest" href="/manifest.json">
結語
最後,我們的PWA應用改造就完成了。PWA技術是一系列技術的集合,這裏,我只用到了serviceWorker, manifest,push/notification等沒有涉及到,如果日後有這個必要,再增加相應功能
延伸擴展
什麼是不透明響應(opaque response)
簡單的説,不透明響應就是當我們使用fetch,並且設置no-cors,來請求跨域資源時獲取到的響應
fetch('https://www.baidu.com/img/flexible/logo/pc/result@2.png', {
mode: 'no-cors'
}).then(response => {
return console.log(response)
}).catch(error => {
return console.log(error)
});
打印的結果為
Response {
body: null
bodyUsed: false
headers: {},
ok: false
redirected: false
status: 0
statusText: ""
type: "opaque"
url: ""
}
從Response中,我們可以發現不透明響應
- status為0,而非200等http status code
- statusText為空
- headers也為空
- body也為空
總之,我們(JavaScript)獲取不到這個Response中的內容