# 使用Workbox创建PWA应用

By [Untitled](https://paragraph.com/@0x9493695d028e374e8e462810b7f1836c3cdfb26e) · 2021-11-10

---

前言
--

最近公司项目迭代逐渐放缓，下班时间逐渐变早，所以本着渐进增加的理念，在下班后，将公司项目进行了一下PWA改造

为何要改造成PWA
---------

1.  用户需求。我们的用户有许多电脑小白，不想记网址，又不会使用浏览器的收藏功能。以前使用的同类软件都有桌面版，有一种觉得桌面版比网页版可靠，使用简单的错觉，曾多次在钉钉售后群里反映，如何将网页保存至桌面，方便他下次直接在桌面打开
    
2.  PWA是渐进式的，如果用户的浏览器不支持ServiceWorker等构建PWA所需的API，并不会对其造成使用上的影响，并且通过埋点平台获知，我们用户Chrome浏览器数量占到80%左右
    
3.  离线缓存，可安装，可拦截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`主要做了两件事

1.  将webpack编译生成的资源文件清单，以变量`self.__WB_MANIFEST`的形式注入到我们提供的`serviceWorker`模板文件中
    
2.  编译我们提供的模板文件，生成目标`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);
    

#### 路由请求缓存

1.  使用NavigationRoute来缓存html文件
    
        registerRoute(
          new NavigationRoute(
            new NetworkFirst({
              cacheName: 'navigation-cache',
              plugins: [
                new CacheableResponsePlugin({
                  statuses: [200]
                }),
                new ExpirationPlugin({
                  maxEntries: 300,
                  maxAgeSeconds: 7 * 24 * 60 * 60,
                }),
              ],
            }),
          ),
        );
        
    
2.  缓存本地静态资源文件
    
        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,
              }),
            ],
          }),
        );
        
    
3.  缓存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中，我们可以发现不透明响应

1.  status为0，而非200等http status code
    
2.  statusText为空
    
3.  headers也为空
    
4.  body也为空
    

总之，我们(JavaScript)获取不到这个Response中的内容

---

*Originally published on [Untitled](https://paragraph.com/@0x9493695d028e374e8e462810b7f1836c3cdfb26e/workbox-pwa)*
