PWA 在KLOOK的实践经验

2,304 阅读4分钟

本篇主要分享PWA在客路旅行实践经验:

  • PWA简介
  • 本地 https 调试
  • PWA 配置
  • 线上部署
  • PWA 更新
  • PWA 降级处理

主要技术栈:

  • vue
  • PWA
  • webpack
  • nginx

本方案更适合已经是SPA架构,想要升级PWA的项目,(service-worker.js后面简称sw)

PWA简介

离线应用

  • 一般前端加载优化模板以及资源缓存在用户就近的CDN(对比电商物流前置仓)
  • 引入PWA后包括模板的资源直接本地缓存(对比用户家里的售卖机)

可以去这里 outweb.io/ 感受下PWA应用,会看到国内很多出名的站点也陆续升级PWA了

PWA 调试

配置https本地环境利于pwa项目的调试, 本地证书生成可以参考mkcert
nginx配置:

        listen       443 ssl;
        server_name  localhost2;
        ssl_certificate      /Users/liuze/localhost2.pem;
        ssl_certificate_key  /Users/liuze/localhost2-key.pem;

这边是SPA项目,nginx主要做静态资源输出,更多关于服务端配置可参考vue 服务端配置

        location @ipad-index {
            add_header Cache-Control no-cache;
            expires 0;
            root /Users/liuze/workspace/klook-fe/klook-gds-hotel-ipad/dist/;
            try_files /index.html =404;
        }

        location /ipad {
            alias /Users/liuze/workspace/klook-fe/klook-gds-hotel-ipad/dist/;
            autoindex on;
            try_files $uri @ipad-index;
        }
        
         location /pwa-check { #检测是否降级处理PWA
            proxy_pass http://127.0.0.1:3008;
            proxy_pass_request_headers on;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

(注意在 hosts 添加 127.0.0.1 localhost2 映射)

PWA配置

配置主要基于 workbox-webpack-plugin 3.6.3/workbox-sw.js,选择的原因如下:

  • 相对原生,极大简单化了配置
  • 从2018年开始下载量一路飙升到现在,npm trends
  • 支持离线GA

主要配置基本如下,后面会分析到:

        workboxOptions: {
            importScripts:['./sw-prefile.js'],//插入生成的service-worker.js中
            runtimeCaching: [{
                urlPattern: /v1/,
                handler: "networkFirst",
                options: {
                    cacheName: "klook-gds-hotel-ipad-static",
                    expiration: {
                        maxEntries: 50,
                        purgeOnQuotaError: true,
                        maxAgeSeconds: 60 * 60 * 24,
                    },
                    cacheableResponse: {
                        statuses: [0, 200]
                    }
                }
            }],
            clientsClaim: true, //新的service-worker 将自己设置为controller and triggers a "controllerchange" event
            offlineGoogleAnalytics: true,
            navigateFallback: '/ipad/index.html',
            directoryIndex: '/ipad/index.html'
        }

workbox-webpack-plugin主要提供两种模式:

  1. GenerateSW 模式根据配置生成sw文件,适用场景:

    • sw只是涉及到简单配置
    • 不涉及Web Push
  2. InjectManifest 模式通过既有sw文件再加工,适用场景;

    • 涉及Web Push
    • 更复杂的自定义配置

这里使用的GenerateSW模式;

线上部署

线上部署与本地调试配置类似,除了根据部署项目静态资源build目录来调整nginx指向外,还需要进行证书替换

PWA更新

PWA控制页面,更新不当很容易导致重大页面错误;
这里选择用户主动更新方式;

  1. 监听sw的更新状况
  2. 如有更新就自动触发sw时,同时发送自定义事件
  3. 自定义事件被触发,显示更新按钮
  4. 用户点击更新按钮触发更新

实际中:

registerServiceWorker.js

//add interval check after registered
 registered(registration) {
      console.log('Service worker has been registered.')
      updateInterval = setInterval(() => {
        registration.update();//dynamically pull service-worker
        console.log('checking update!')
      }, 1000 * 10) // e.g 10s senconds checks
    }
    
//trigger custom event and export resgitration instance
    updated(registration) { //triggered whens service-worker.js changed
          console.log('New content is available; please refresh.')
          document.dispatchEvent(
            new CustomEvent('swUpdated', {
          detail: registration
        })
      );
    }

App.vue

    //add custom event
    document.addEventListener(
      'swUpdated', this.showRefreshUI, { once: true }
    );
      
    //show refresh button
    showRefreshUI(e) {
      this.registration = e.detail;
      this.updateExists = true;
    },
    //click to refresh and post web worker message
    refreshApp() {
      this.updateExists = false;
      if (!this.registration || !this.registration.waiting) { return; }
      this.registration.waiting.postMessage({
        type: 'SKIP_WAITING'
      });
    },

这里refreshApp主要是通过web worker进行message交互(解决了多tab同步更新问题)

有发送消息肯定就有接收方,还记得之前有个配置项:

importScripts:['./sw-prefile.js']

sw-prefile.js

self.addEventListener('message', (event) => {
     if (event.data && event.data.type === 'SKIP_WAITING') {
        self.skipWaiting();
     }
 });

主要往sw中注入message事件监听,使得接收消息时新的sw跳过waiting阶段,再结合配置项

clientsClaim:true

完成了新旧sw替换。

到这里似乎是皆大欢喜
现在页面已经由新的sw接管,不过前面的请求通过的旧的sw来,就造成一个页面存在两个版本的请求,所以还需要进一步处理

    navigator.serviceWorker && navigator.serviceWorker.addEventListener( //triggered by registration.claim
      'controllerchange', () => {
        if (this.refreshing) return;
        this.refreshing = true;
        console.log('controllerchange triggered, -> auto refresh!!')
        window.location.reload();
      }
    );

监听新的sw接管,然后主动触发一次页面的刷新,刷新后的就是完全新的sw接管的页面

PWA降级处理

现在主要通过定时发送信息处理降级:

  1. 定时check '/pwa-check'
  2. 根据返回判定是否注销PWA
  3. 注销的同时清理定时check更新的任务

这里通过node提供/pwa-check接口

小结:

Todos:

  • pwa转换amp
  • 前端监控信息离线发送
  • 添加到桌面统计
  • ...

欢迎纠错!

附录

关于我们
客路旅行正在开放前端,后端开发等岗位,这里工作1075,不打卡

参考资料: