前言
最近公司项目迭代逐渐放缓,下班时间逐渐变早,所以本着渐进增加的理念,在下班后,将公司项目进行了一下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
不透明响应成功变成透明响应。但如果给所有标签添加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"
}
结语
最后,我们的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中的内容