背景

PWA 即 Progressive Web App,是谷歌于 2016 年在 Google I/O 大会上提出的下一代 Web App 概念,于2017年落地的 web 技术,旨在增强 Web 体验,缩小 Web App 与 Native App 的差距并创建类似的用户体验。目前 Chrome,Safari,Firefox,Edge 浏览器都在不同程度上支持了 PWA ,国内外一些网站也已进行 PWA 实践,如Twitter,Starbucks,饿了么,新浪,其中不乏成功案例,在应用 PWA 后得到了一些可量化的收益:

  • West Elm 应用 PWA 后数据显示用户使用时间增加了 15 %,收入增加了9 %。

  • 印度最大的电子商务网站 Flipkart 的用户使用时间增加了三倍,互动率增加了 40 %。

理论上来说,对于所有 Web App,只要参考 PWA 的标准对其进行改造,就可实现 PWA ,而对 App 来说,用户的体验是判断一个应用好坏的重要标准,PWA 的改造并不复杂,却可以很大程度上提升用户体验,是投入/产出比很高的一项技术,且 PWA 的相关功能和浏览器对 PWA 的支持程度也在不断地增加,未来还有很大的潜力。

但目前由于兼容性等问题尚未大面积地应用到实际中,且网上有关 PWA 实践的文章较为零散,我们在 Sharee 项目的移动端改造 PWA ,在本文中介绍了 PWA 的主要功能及实现,并记录了实现过程中遇到的一些问题。希望通过这篇文章能对大家了解这门技术和实践过程中遇到的问题有一定的帮助。

选型

我们都知道,Web App 在用户体验上远不如 Native App,因为 Web App 强烈依赖网络,存在弱网环境下加载速度慢,离线情况下无法访问的问题,而且 Web App 无法脱离浏览器 UI ,用户可视区域被压缩,也无法安装在手机桌面。Native App 虽然有诸多优点,也存在很多问题:开发成本高,需要维护多个版本的更新升级,且内容封闭,无法进行 SEO 检索,不利于 App 推广,用户使用前需要下载安装包。

作为 Web App 的开发者,我们更加关心如何将 Native App 的特性添加到 Web App 上,PWA 应运而生。

App类型

市面上现存的 App 类型可以分为以下四种,我们比较了它们在不同指标上的表现,可以看到 PWA 本质上还是一个 Web App ,但在表现上比 Web App 更加接近 Native App 。虽然我们在此与 Native App 进行了比较,但是 PWA 的目的并不是为了取代 Native App ,也不是为了与 Native App 一较高下,而是对 Web App 的升级,是为了带给用户更好的用户体验。

PWA(Progressive Web App)

目前我们已经知道 PWA 用于提升用户体验,下面给出 PWA 的官方定义 。

定义

Progressive Web Apps use modern web capabilities to deliver an app-like user experience. They evolve from pages in browser tabs to immersive, top-level apps, maintaining the web's low friction at every moment.

从定义中我们可以得知,PWA 不是特指某一项技术,而是应用了多项技术的 Web App ,本质上还是 Web App 。PWA 借助一些新技术将 Web App 和 Native App 各自的优势融合在一起,让用户在使用 Web App 时感觉在使用 Native App 。

特点

PWA 具有快速、可靠、粘性的特点。快速即快速响应,通过独立的线程进行资源缓存,提高页面的加载时间;可靠指在不稳当的网络环境下, App 也能瞬间加载并展现内容,在离线环境下也提供用户有效反馈;粘性则是通过沉浸式的用户界面、桌面图标、消息推送等手段来增强用户的粘度。

标准支持度

根据 Can I use 的统计(包括 PC 和 Mobile),实现 PWA 所涉及到的各项技术的兼容性如下,最重要的 APP Manifest 和 Service Worker 支持情况良好,且在主流浏览器和操作系统中都得到了支持:

  • App Manifest 的支持度达到 57.43%

  • Service Worker 的支持度达到 72.82%

  • Notifications API 的支持度达到 43.3%

  • Push API 的支持度达到 72.39%

  • Background Sync 暂未统计到,Chrome 49 以上均支持

目的

在 Sharee 移动端实现 PWA ,预期目标为优化用户体验和增加用户的留存率和访问量。

对于用户体验的优化主要包含以下两个方面:

  • 离线环境下增加兜底页面:捕捉到用户请求失败时,返回兜底页面来避免浏览器的网络崩溃页面。

  • 提高冷启动时的资源加载速度:预缓存静态资源并保存在 Local Storage 中,在冷启动时通过 Service Worker 响应,避免 304 。

用户的留存率和访问量的增加通过以下两个方面实现:

  • 对于网页端用户增加常驻入口,减小网站入口的深度。

  • 推送消息:推送消息也是吸引用户访问的一种方式,但推送消息需要运营维护,同时 Sharee 目前消息推送主体还是 Native App ,该功能并未落地。

指标

技术落地都需要数据指标量化效果,我们统计以下数据用于长期观测:

  • PWA install (安装弹窗)的点击、安装、取消的渗透率

  • PWA 入口(桌面图标)的访问量占比

  • 静态资源加载时间

方案

本节是对 PWA 实现细节的详细介绍。首先介绍了 PWA 依托的主要技术 App Manifest 、Service Worker 和 Push API 所产生的收益及其具体实现,然后介绍了 Google 推荐的 PWA 最佳实践 Workbox ,最后简要介绍了如何测试 PWA 。整体结构如图所示。

App Manifest

App Manifest 是一个 JSON 格式的文件,用于配置网站应用的相关信息。通过该文件,我们可以配置桌面留存图标、安装弹窗和启动动画的相关信息。

屏幕留存图标

我们可以在配置文件中配置桌面留存图标的 icon 和名称,当用户将网站保存在桌面后,会自动应用配置信息。

收益

添加到主屏幕的好处有很多,主要体现在用户粘性和用户体验上。桌面图标减少了网站的入口深度,用户可以从主屏幕直达站点,而无需从浏览器首页一层一层进入。添加到主屏幕的图标具有接近 Native App 的体验,如下图所示,左二为 Native App ,左三为 PWA 其他均为 Native App :

从桌面图标进入网站时具有启动页面和脱离浏览器 UI 的全屏体验,添加到主屏幕的网站会被纳入应用抽屉中。添加屏幕图标无需下载,类似桌面快捷键,减少了用户安装 App 的成本。

但 PWA 的屏幕留存图标与快捷方式不同。

与快捷方式的区别:

  1. 屏幕留存图标拥有独立的图标和名称。

  2. 点击图标打开网站,资源加载的过程并不会像普通网页那样出现白屏,取而代之的是一个展示应用图标和名称的启动页面,资源加载结束时加载页消失。

  3. 当网页最终展现时,地址栏、工具栏等浏览器元素将不会展现出来,网页内容占满屏幕,看起来与 Native App 一样。

实现

编写配置文件后在head中引入, <link rel="manifest" href="/manifest.json">。Mainfest 文件通过一些文本描述,具体定义显示到桌面上的内容,并询问用户是否添加,添加之后可在 Chrome 浏览器的 Application - Manifest 来查看你的 Mainfest 文件是否生效

配置项介绍:web.dev/add-manifes…

配置文件示例:

// manifest.json
{
  "name": "Sharee PWA",  // 用于安装横幅、启动画面显示
  "short_name": "Sharee PWA",  // 用于主屏幕显示
  // 浏览器会根据有效图标的 sizes 字段进行选择。首先寻找与显示密度相匹配并且尺寸调整到 48dp 屏幕密度的图标
  "icons": [
    {
      "src": "../logo/180.png",
      "sizes": "180x180",
      "type": "image/png"
    },
    {
      "src": "../logo/192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "../logo/512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "/?from=homescreen",  // 启动网址,相对于manifest.json所在路径
  "scrope": "/", // sw的作用范围只能在此路径或子路径
  "display": "standalone",
  "theme_color": "#FFF",
  "background_color": "#FFF"  // 启动时的背景色
}

// iOS不支持manifest配置,通过meta标签添加到head中
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="#fff">
<meta name="apple-mobile-web-app-title" content="Sharee PWA">
<link rel="apple-touch-icon" sizes="180x180" href="../logo/180.jpg">
<meta name="msapplication-TileColor" content="#fff">
<meta name="theme-color" content="#fff" />

安装弹窗

网站添加 Manifest 配置文件并满足一定要求后,浏览器会根据用户的访问频率在合适的时间弹出弹窗询问用户是否需要添加屏幕图标,弹窗如下图所示。

收益

安装弹窗主要用于引导用户留存屏幕图标,提高屏幕图标的添加率。

实现

浏览器展现应用安装提示需满足以下条件「web.dev/install-cri…」:

  1. 尚未安装

  2. 通过 HTTPS 访问(调试模式下允许 http://127.0.0.1http://localhost 访问)

  3. manifest.json 文件包含以下配置:

    1. name / short_name,优先采用 short_name

    2. start_url

    3. icons

    4. display 为 standalonefullscreenminimal-ui

  4. 站点必须注册 Service Worker

    1. Chrome 要求 Service Worker 且必须监听 fetch 事件

  5. 用户访问频率足够高(浏览器未明确说明频率)

开发者无法主动触发安装提示的弹出,但可监听 beforeinstallprompt 事件拦截弹窗事件并保存,然后提供按钮触发:

let appPromptEvent = null;
const installBtn = document.getElementById('install-btn');
self.addEventListener('beforeinstallprompt', function(e) {
  e.preventDefault();
  // 保存弹窗事件
  appPromptEvent = event;
  installBtn.classList.add('visible');
  return false;
});
window.addEventListener('appinstalled', function () {
  console.log('应用已安装');
  installBtn.classList.remove('visible');
});
installBtn.addEventListener('click', function () {
    if (appPromptEvent !== null) {
      console.log(appPromptEvent)
      // 触发弹窗
      appPromptEvent.prompt();
      appPromptEvent.userChoice.then(function (result) {
        if (result.outcome === 'accepted') {
          console.log('同意安装应用');
        } else {
          console.log('不同意安装应用');
        }
        appPromptEvent = null;
      });
    }
});

beforeinstallprompt的兼容性如下,大多数浏览器不支持弹窗。其实在支持弹窗的浏览器中,它的触发策略也是很低频的,需要用户在短时间内与网站进行高频互动才会触发,且在用户选择取消安装后很长一段时间内都不会再次触发,浏览器需要保证用户的使用体验,没有人会喜欢频繁弹出的广告影响自己浏览:

启动动画

从屏幕图标进入时,根据 Manifest 文件中的配置项会自动生成启动动画,用来过渡资源拉取前的白屏时间,改善用户体验。启动界面如下图所示:

收益

启动动画可以优化交互,利用首屏时间展示有效内容,加深用户对网站的记忆,还可以缓和等待的时间,在体验上类 Native App 。

实现

启动动画根据 manifest.json 中的配置项自动生成,配置项与动画界面的对应关系如下图所示:

Service Worker

Service Worker 是一种独立于浏览器主线程且可以在离线环境下运行的工作线程,与当前的浏览器主线程是完全隔离的,并有自己独立的执行上下文。 HTML5 提供的一个 Service Worker API,能够进行 Service Worker 线程的注册、注销等工作。且 Service Worker 一旦被安装成功就永远存在,除非线程被程序主动解除,而且 Service Worker 在访问页面的时候可以直接被激活,如果关闭浏览器或者浏览器标签的时候会自动睡眠,以减少资源损耗。利用 Service Worker 的这些特性我们可以预缓存 offline 页面和静态资源。

离线 offline 页面

在用户断网情况下,通常会出现浏览器自带的网络崩溃页面,给人一种 App 可访问性差的印象。通过 Service Worker 我们可以在用户第一次访问网站时就预缓存一个 offline 的静态页面,在监听到请求失败时返回该页面,来改善用户的体验。

收益

离线页面可以在无网络时改善用户的体验,对于 app shell 结构的网站还可以缓存 app shell 。

实现

Service Worker 是浏览器在后台独立于网页运行的脚本,浏览器和服务器之间的代理服务器。浏览器的支持情况如下:

Service Worker的生命周期如图所示,我们可以在对应生命周期编写不同的代码,自动触发执行:

install

在安装时通常添加预缓存文件,我们在此时预缓存 offline 页面。caches 是一个特殊的 CacheStorage 对象,它能在 Service Worker 指定的范围内提供数据存储的能力,这里,我们使用给定的名字开启了一个缓存,并且将我们的应用所需要缓存的文件全部添加进去。

const CACHE_NAME = 'sharee-v1';
const FILES_TO_CACHE = ['offline.html'];

// Install SW
self.addEventListener('install', e => {
  console.log('[Service Worker] Install');
  // 添加预缓存文件
  e.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      self.skipWaiting();  // 如果检测到新的service worker文件,就会立即替换掉旧的
      return cache.addAll(FILES_TO_CACHE);
    }),
  );
});
fetch

注册成功后监听网站的所有请求,拦截特定请求由 Service Worker 相应。respondWith 方法将会接管响应控制,它会作为服务器和应用之间的代理服务。它允许我们对每一个请求作出我们想要的任何响应。

// 1.缓存所有访问过的文件
self.addEventListener('fetch', function(e) {
    e.respondWith(
        // 先从缓存中查找
        caches.match(e.request).then(function(r) {
          console.log(`[Service Worker] Fetching resource: ${e.request.url}`);
          return (
            r ||
            // 缓存中没有则从网络请求数据并缓存
            fetch(e.request).then(function(response) {
              return caches.open(CACHE_NAME).then(function(cache) {
                console.log(
                  `[Service Worker] Caching new resource: ${e.request.url}`,
                );
                cache.put(e.request, response.clone());
                return response;
              });
            })
          );
        }),
    );
}

// 2.不缓存,离线直接返回offline
self.addEventListener('fetch', e => {
    e.respondWith(
      caches.match(e.request, { mode: 'cors' }).then(
        () => {
          return fetch(e.request).catch(() => caches.match('offline.html'));
        },
      ),
    );
});
activate

activate时通常用来删除那些我们已经不需要的文件或者做一些清理工作。当我们把版本号更新到 v2,Service Worker 会将我们所有的文件(包括那些新的文件)添加到一个新的缓存中。这个时候一个新的 Service Worker 会在后台被安装,而旧的 Service Worker 仍然会正确的运行,直到没有任何页面使用到它为止,这时候新的 Service Worker 将会被激活,然后接管所有的页面。

self.addEventListener('activate', function(e) {
  e.waitUntil(
    // 清除旧缓存
    caches.keys().then(function(keyList) {
      return Promise.all(
        keyList.map(function(key) {
          if (CACHE_NAME.indexOf(key) === -1) {
            return caches.delete(key);
          }
        }),
      );
    }),
  );
});

资源缓存

资源缓存也是利用 Service Worker ,在 fetch 阶段将特定响应缓存下来,然后在下次监听到相同请求时,直接返回缓存,提高响应时间并减少服务器压力。

收益

我们通过请求路径的正则匹配缓存了更新较少的所有静态资源,在用户第一次访问网站时进行缓存,之后都从缓存中更新,来提高冷启动的响应时间。

实现

const cacheList = [
  '/static/css/',
  '/static/js'
];

self.addEventListener('fetch', e => {
  const cached = cacheList.find(c => {
    return e.request.url.indexOf(c) !== -1;
  });
  if (cached) {
    e.respondWith(
      caches.match(e.request).then(
        function(r) {
          return (
            r ||
            fetch(e.request).then(function(response) {
              return caches.open(CACHE_NAME).then(function(cache) {
                if (cached) {
                  cache.put(e.request.url, response.clone());
                }
                return response;
              });
            })
          );
        },
      ),
    );
});

Push API & Notification API

PWA 还提供了 API 在网站上向用户推送消息,通常有 Push API 和 Notification API 。

推送通知

收益

PWA 提供的消息推送有很多优点,首先可以吸引用户访问;而且消息的推送只要浏览器在运行即可,无需用户打开网页;消息推送需要获取用户授权,但对于同一个域名下的网页,只需要获取一次授权。

实现

推送通知使用两个API进行组装: Notifications APIPush API 。Notifications API 使应用程序可以向用户显示系统通知。Notification 和 Push API 构建在 Service Worker API 之上,该 API 在后台响应推送消息事件并将它们中继到应用程序。

Notification API

Notification API 是 HTML5 新增的桌面通知 API,用于向用户显示通知信息。

self.registration.showNotification('PWA-Book-Demo 测试 actions', {
  body: '点赞按钮可点击',
  actions: [
    {
      action: 'like',
      title: '点赞',
      icon: '/assets/images/like-icon.png',
    },
  ],
});
// 监听通知点击事件
self.addEventListener('notificationclick', function(e) {
  // 关闭通知
  e.notification.close();

  if (e.action === 'like') {
    // 点击了“点赞”按钮
    console.log('点击了点赞按钮');
  } else {
    // 点击了对话框的其他部分
    console.log('点击了对话框');
  }
});
Push API
// 监听 push 事件
self.addEventListener('push', function (e) {
  if (!e.data) {
    return
  }
  // 解析获取推送消息
  let payload = e.data.text()
  // 根据推送消息生成桌面通知并展现出来
  let promise = self.registration.showNotification(payload.title, {
    body: payload.body,
    icon: payload.icon,
    data: {
      url: payload.url
    }
  })
  e.waitUntil(promise)
})
// 监听通知点击事件
self.addEventListener('notificationclick', function (e) {
  // 关闭窗口
  e.notification.close()
  // 打开网页
  e.waitUntil(self.clients.openWindow(e.data.url))
})

这两个 API 的浏览器兼容性如下:

Workbox 最佳实践

Workbox 是 Google Chrome 团队推出的一套 PWA 的解决方案,这套解决方案当中包含了核心库和构建工具,我们可以利用 Workbox 实现 Service Worker 的快速开发。定义如下:

Workbox is a library that bakes in a set of best practices and removes the boilerplate every developer writes when working with service workers.

Workbox 的功能非常完善,插件机制也能够很好的满足各种业务场景需求,如果自己手动维护一个应用的原生的 Service Worker 文件工作量非常巨大,而且有很多潜在的问题不容易被发现,Workbox 很好的规避了很多 Service Worker 潜在的问题,也大大减小了 Service Worker 的维护成本,所以建议大家在开始考虑使用 Service Worker 的时候优先考虑 Workbox。下面介绍一下 Workbox 的使用。

引入Workbox

在 Service Worker 中引入 Workbox,引入后获得全局对象 workbox,一旦 Workbox 加载完成,我们便可以使用挂载到 workbox 对象上的各种功能了:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.2.0/workbox-sw.js')复制代码

预缓存

PWA 中预缓存的文件只要按照如下所示的接口给出文件路径即可,在第一次访问网站时便会预缓存相应文件:

// 预缓存,同ws中的cacheName
workbox.core.setCacheNameDetails({
  prefix: 'sharee',
  suffix: 'v1',
  precache: 'precache',
  runtime: 'runtime'
})

// 动态缓存
workbox.routing.precacheAndRoute([
  {
    url: '/index.html',
    revision: 'asdf'
  },
  '/index.abc.js',
  '/index.bcd.css'
])

路由匹配 & 请求响应

Workbox 对资源请求匹配和对应的缓存策略执行进行了统一管理,采用路由注册的组织形式,以此来规范化动态缓存,使用下面这个函数:

workbox.routing.registerRoute(match, handlerCb)

match:路由匹配规则

上述函数中的match指路由匹配规则,有三种匹配方式。

  1. 对 URL 进行字符串匹配,绝对路径/相对路径

workbox.routing.registerRoute('http://127.0.0.1:8080/index.css', handlerCb)
workbox.routing.registerRoute('/index.css', handlerCb)  // 以当前url为基准
workbox.routing.registerRoute('./index.css', handlerCb)复制代码
  1. 正则匹配

workbox.routing.registerRoute(``/\/index\.css$/``, handlerCb)

  1. 自定义匹配方法

该自定义方法是一个同步执行函数,在表示资源请求匹配成功时返回一个真值。

const match = ({url, event}) => {
  return url.pathname === '/index.html';
}复制代码

handlerCb:资源请求处理方法

对匹配到的资源请求进行处理的方法,开发者可以在这里决定如何响应请求,无论是从网络、从本地缓存还是在 Service Worker 中直接生成都是可以的。该方法是异步方法并返回一个 Promise,要求Promise 解析的结果必须是一个 Response 对象。

// url:event.request.url 经 URL 类实例化的对象;
// event:fetch 事件回调参数;
// params:自定义路由匹配方法所返回的值。
const handlerCb = ({url, event, params}) => {
  return Promise.resolve(new Response('Hello World!'));
}复制代码

缓存策略

拦截请求后我们可能会缓存响应,通常我们需要自己来编写相应的策略, Workbox 提供了常用的几种策略可以直接使用。 workbox.strategies 对象提供了一系列常用的动态缓存策略来实现对资源请求的处理,有以下五种:

  • NetworkFirst:网络优先

  • CacheFirst:缓存优先

  • NetworkOnly:仅使用正常的网络请求

  • CacheOnly:仅使用缓存中的资源

  • StaleWhileRevalidate:从缓存中读取资源的同时发送网络请求更新本地缓存

CacheFirst 为例,使用方法如下:

workbox.routing.registerRoute(
  /\.(jpe?g|png)/,
  new workbox.strategies.CacheFirst({
    cacheName: 'image-runtime-cache',
    plugins: [  // 通过插件来强化缓存策略
      new workbox.expiration.Plugin({
        maxAgeSeconds: 7 * 24 * 60 * 60,  // 对图片资源缓存1星期
        maxEntries: 10  // 匹配该策略的图片最多缓存10张
      })
    ],
    fetchOptions: {  // 跨域请求的资源
      mode: 'cors'
    }
  })
)

插件

Workbox 还提供了一些插件在 Workbox API 中使用,比如缓存过期时间「developers.google.com/web/tools/w…」:

每当使用或更新缓存的请求时,此插件都会查看使用的缓存并删除所有旧的或多余的请求。

使用时maxAgeSeconds,请求可以在到期后使用一次,因为直到使用了缓存的请求后才进行到期清理。

workbox.routing.registerRoute(
  new RegExp('.*/static/.*'),
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: CACHE_NAME,
    plugins: [
      new workbox.expiration.ExpirationPlugin({
        maxAgeSeconds: 7 * 24 * 60 * 60,
      }),
    ],
  }),
);

测试 PWA

通常我们使用 Chrome Lighthouse 来测试网站的 PWA 实现情况,在 Chrome 浏览器的 Audits 中选择 Progressive Web App ,运行测试,可生成测试报告。下图中我们提供了 Sharee 的 PWA 测试报告,它会从多个方面来进行评估。

线上环境遇到的问题

我们在上线后,遇到了一些测试环境中没有的问题,我们对这些问题及其解决方案进行了记录,以供大家在开发过程中进行参考。

  1. 缓存资源失败

先考虑请求拦截阶段,是否是由于跨域请求导致。

Workbox 要求与同一服务器的 regex 相匹配,如果是同源的请求,只要请求的 URL 与正则表达式匹配,该正则表达式就会匹配。如果是跨域请求,正则表达式 必须与 URL 的开头匹配

eg. 如果想匹配 https://abc.def.com/aaa/bbb/static/css/xxxxx.chunk.css 使用正则表达式new RegExp('https://abc\\.def\\.com.*/static/(css|js)/.*')

再考虑请求拦截成功后的缓存阶段,是否是由于缓存策略导致。CacheFirst 无法缓存不透明的响应。不透明响应类型为Response-Type: opaque,为了避免跨域信息泄漏,在计算储存空间时,不透明响应的大小会被添加大量填充,填充的大小因浏览器而异,对于 Chrome 来说,缓存单个不透明响应将至少占用约 7MB 空间

  1. SecurityError ,详细报错信息如下

SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported:b.setAttribute('href', e.canvas.toDataURL());复制代码

函数HTMLCanvasElement.toDataURL()会将画布转为 png 图像,返回 base64 格式数据 URL。如果你的图片 URL 和页面不在同一域下,在调用 toDataURL()函数的时候就会报安全性错误。没有 CORS 授权虽然可以在 canvas 中使用图像, 但这样做就会污染画布。只要 canvas 被污染, 就不能再从画布中提取数据, 也就不能再调用 toDataURL() 方法, 否则会抛出 security error 。这实际上是为了保护用户的个人信息,避免未经许可就从远程 Web 站点加载用户的图像信息,造成隐私泄漏。

对于这个问题,首先确认服务器返回的图片属性access-control-allow-headers: *,然后在配置文件中设置图片属性image.setAttribute('crossOrigin', 'anonymous');,最后更新线上 pwacompat.min.js 文件,由于 pwacompat.min.js 文件预缓存且存在过期时间,为保证所有用户更新文件,需更新文件名称和缓存版本。


作者:字节前端
链接:https://juejin.cn/post/6896426453303476238
来源:掘金