Giter Site home page Giter Site logo

sw-tools's Introduction

使用说明

安装构建依赖

npm install

构建全部示例项目

npm run build
  • 示例项目位于 /examples
  • 为了发布到 Github Page 上,修改了 PUBLIC_PATH

sw-precache

使用 sw-precache-webpack-plugin 创建预缓存列表。

会生成一个全新的 Service Worker。

sw-toolbox

由于 sw-toolbox 已经集成到了 sw-precache 中,因此同样可以使用 sw-precache-webpack-plugin 配置动态缓存列表。

new SWPrecacheWebpackPlugin({
    cacheId: 'sw-tools',
    filename: 'service-worker.js',
    minify: false,
    navigateFallback: PUBLIC_PATH + 'index.html',
    staticFileGlobsIgnorePatterns: [/\.map$/, /\.png$/],
    runtimeCaching: [{
        urlPattern: /\.png$/,
        handler: 'networkFirst'
    }]
})

会生成一个全新的 Service Worker。

workbox

使用 workbox-webpack-plugin 扩展已有 Service Worker。

sw-tools's People

Contributors

xiaoiver avatar

Stargazers

Walker avatar 梅浩 avatar  avatar wkz.leo avatar ashley avatar MelodyMS avatar

Watchers

James Cloos avatar  avatar

sw-tools's Issues

[WIP] Service Worker 开发工具

目前流行的 Service Worker 开发工具大多以命令行或者构建工具(Gulp、Webpack)插件形式提供,因此可以很方便地集成到我们已有的开发流程中。从工具的使用方式上又可以分成向已有 Service Worker 注入代码,和生成全新 Service Worker 两种。

Service Worker 需要处理两类缓存:预缓存和动态缓存。
下面我们先介绍下两者的概念。

预缓存

首先我们需要了解预缓存的概念。在 Service Worker 安装阶段,我们可以请求并缓存一些需要被长时间缓存的静态资源,这种在 Service Worker 正式工作之前就进行的缓存就叫做“预缓存”。

从内容上看,App Shell 所需的静态资源就是十分适合被预缓存的,后续这些资源可以使用 CacheFirst 策略进行响应。

// service-worker.js

function precache() {
  return caches.open(CACHE).then(function(cache) {
    // 预缓存列表
    return cache.addAll([
      './index.html',
      './index.js',
      './index.css'
    ]);
  });
}
self.addEventListener('install', function(evt) {
  evt.waitUntil(precache());
});

在实际的项目开发中,每次代码发生变更,为了保证客户端能同步到最新版本的代码,这个预缓存列表也是需要更新的。这就要求在每次项目的构建阶段,我们都需要生成最新的预缓存列表。

另外,在 Service Worker 中,如果按照上述代码编写,显然会带来一个问题。那就是当列表中的资源已经存在于缓存中且没有发生改变,依然会重新发送请求,造成带宽的浪费。理想状态下,Service Worker 应该只请求发生了变化的静态资源。

最后,为了对比最新预缓存列表和老版本中的资源情况,我们需要为列表中每一个资源生成对应的版本号。这个版本号可以体现在文件名中,例如 index.[hash:8].js,也可以单独存在于构建时注入 Service Worker 的资源列表中。在后续具体工具的介绍中,我们会看到这两种方式更加详细的使用情况。

动态缓存

通过对预缓存的介绍,我们很容易看出某些资源并不适合放在预缓存列表中。例如列表中的图片,用户头像,API 请求的 JSON 响应数据等等。通常我们希望实际请求这部分资源时才放入缓存,这就是动态缓存。

不同于预缓存使用 CacheFirst 响应策略,对于不同的资源请求类型,应该采取不同的动态缓存策略。
关于这些缓存策略的介绍以及应用场景,可以参考 offline-cookbook

下面我们将正式介绍 sw-precache sw-toolboxworkbox 这三款工具的使用方法。

sw-precache

首先需要声明,这两款工具推出的时间较早,因此不感兴趣的可以直接跳到下一小节对于 Workbox 的介绍。另外,我们的介绍也仅包含集成到 Webpack 构建流程的情况。

首先是 sw-precache,顾名思义这是负责在构建生成预缓存列表的插件,使用方式十分简单。值得一提的是 Vue 官方的 PWA 模板也使用了这款插件。

示例项目中我们使用了最基本的配置,即缓存名称和输出的 Service Worker 文件名:

// webpack.config.js

new SWPrecacheWebpackPlugin({
  cacheId: 'sw-tools',
  filename: 'service-worker.js'
})

Service Worker 内容

插件会自动生成 Service Worker,其中包含了预缓存列表,列表中每一项包含了当前资源文件名和根据内容生成的版本号。

// service-worker.js

var precacheConfig = [
  [".../dist/index.js","636d...fdb4"],
  [".../dist/index.css","ddc3...bf73"],
  [".../dist/index.html","4d5b...9086"]
];

运行效果

打开 Chrome 开发者工具我们可以发现在 Service Worker 安装完毕之后,预缓存列表中的资源就被放入了我们指定的缓存中了:
image

sw-toolbox

接下来我们来看看动态缓存的使用方式,sw-toolbox 已经集成到了 sw-precache 中,因此可以在上述 sw-precache 的基础上增加配置项 runtimeCaching,其中缓存规则列表中每一项都包含了匹配规则和对应的缓存策略。示例项目

// webpack.config.js

new SWPrecacheWebpackPlugin({
  cacheId: 'sw-tools',
  filename: 'service-worker.js',
  runtimeCaching: [{ //  增加的配置项
    urlPattern: '/.*\.png$', // 匹配规则
    handler: 'networkFirst' // 缓存策略
  }]
})

Service Worker 内容

上述配置生成的 Service Worker 包含如下内容,在之前 sw-precache 生成的内容基础之上,包含了对 sw-toolbox 的引用以及根据匹配规则生成的调用相应 API 代码:

// service-worker.js

// 之前的 precache 代码...
// 引入 sw-toolbox 代码...

toolbox.router.get("/.*.png$", toolbox.networkFirst, {});

运行效果

当页面请求一张图片(fog.png)时,我们可以看到命中了匹配规则,被放入了动态缓存中。
image

Workbox

相比 sw-precache 和 sw-toolbox,Workbox 作为 Google 力推的 Service Worker 开发工具,拥有更加灵活友好的 API 设计和开发 debug 信息。如果之前是 sw-precache 和 sw-toolbox 的使用者,也可以遵循官方的迁移教程方便地完成迁移工作。

除了使用对应的 Webpack 插件,Workbox 还提供了 CLI 和 Node 模块的使用方式。另外,除了生成全新 Service Worker,也支持注入已有 Service Worker。

在下面的介绍中,我们选取了如下使用方式:

  1. Workbox v3,如果是之前 v2 的使用者,可以按照官方的迁移教程完成升级
  2. 使用 Webpack 插件
  3. 注入已有 Service Worker

其中包含以下内容:

  • Webpack 插件配置
  • 在 Service Worker 中使用 Workbox API
  • 其他注意事项

完整的示例项目地址

Webpack 插件配置

首先 Workbox Webpack 插件提供了两种使用方式,即完全依赖 Workbox 生成(generateSW) Service Worker 和注入(injectManifest)已有Service Worker。两者各有优劣,对于简单场景前者其实完全够用,而在需要对 Service Worker 进行更细粒度控制的复杂场景(复杂动态路由规则,结合 Web Push 等)下,后者显然更加合适。

其次在两种模式下,Workbox Webpack 插件都会根据配置生成预缓存列表,这一点和 sw-precache 其实是一样的。只不过是以单独文件形式存在。

下面我们将选取注入模式进行介绍,其中几个重要的配置项如下:

  • 已有 Service Worker 路径 swSrc 和目标输出文件名 swDest
  • Webpack 最终输出的静态资源(assets)未必都需要预缓存,因此需要 exclude 进行过滤
  • 默认情况下运行时会使用 Google CDN 上的 Workbox 地址,而国内访问并不稳定,因此我们需要使用 importWorkboxFrom 指定引用本地的 Workbox 文件
// webpack.config.js

const {InjectManifest} = require('workbox-webpack-plugin');
// 注入模式
new InjectManifest({
  // 已有 SW 路径
  swSrc: path.resolve(__dirname, 'src/service-worker.js'),
  // 目标文件名
  swDest: 'service-worker.js',
  // 过滤掉图片
  exclude: [/\.png$/],
  // 使用本地 Workbox 文件
  importWorkboxFrom: 'local'
})

其他配置项可以参考 workbox-webpack-plugin#configuration

构建结束后可以发现生成了一个单独的 precache-manifest 文件,其中包含了完整资源名称和版本号:

// precache-manifest.7cf672a.js

self.__precacheManifest = [
  {
    "revision": "4bc1274aea045523f107d03725a9ed41",
    "url": "/sw-tools/examples/workbox/dist/index.html"
  },
  {
    "revision": "6637fa32535dcb55b8e6",
    "url": "/sw-tools/examples/workbox/dist/index.css"
  },
  {
    "revision": "6637fa32535dcb55b8e6",
    "url": "/sw-tools/examples/workbox/dist/index.04ef0e20.js"
  }
];

那么这个单独的文件是如何被 Service Worker 使用的呢?

使用 Workbox API

打开注入后的 Service Worker 文件,可以发现文件顶部多出了如下两行语句:

// dist/service-worker.js

importScripts("/sw-tools/examples/workbox/dist/precache-manifest.7cf614407318b61f9842d1dbb811672a.js",
  "/sw-tools/examples/workbox/dist/workbox-v3.3.1/workbox-sw.js");
workbox.setConfig({modulePathPrefix: "/sw-tools/examples/workbox/dist/workbox-v3.3.1"});

其中第一行通过 importScripts 引用了之前生成的 precache-manifest 文件以及 workbox-sw 代码,其中 workbox-sw 会在运行时根据运行环境(是否在 localhost 下)自动决定引用开发 dev 版本还是 prod 版本。而第二行则是为了帮助 workbox-sw 找到其余 Workbox 类库代码(例如都在 dist/workbox-v3.3.1下)。

下面我们将介绍常用的 Workbox API,这也是 Service Worker 开发者最关心的部分。值得一提的是 Workbox 将这些功能划分成了独立的模块,确保在运行时只引用所需模块。

预缓存

由于 Workbox 已经帮助我们自动生成了预缓存列表文件,而且也已经向 Service Worker 注入了引用代码,
我们开发者要做的只剩下调用一行 API。如果想了解 Workbox 在 Service Worker 安装阶段如何高效地进行预缓存工作,可以阅读 how workbox-precaching works

// src/service-worker.js

workbox.precaching.precacheAndRoute(self.__precacheManifest);

你可能会问为什么 Workbox 不帮我们连这句都自动注入呢?答案是对于这份预缓存列表,开发者还有更多可配置的选项,最终会影响到 URL 的匹配行为。

例如默认情况下 URL 中 utm_ 参数会被忽略,因此 /?utm_campaign=123&utm_source=zhihu 会匹配缓存中的 / 路径,如果想忽略全部参数,配置如下:

// src/service-worker.js

workbox.precaching.precacheAndRoute(
  self.__precacheManifest,
  {
    ignoreUrlParametersMatching: [/.*/]
  }
);

更多 workbox.precaching 模块的使用方式可以参考 incoming requests to precached files

动态缓存

动态缓存由 workbox.routing 模块负责。开发者可以定义一系列路由规则及其处理逻辑,每个被拦截的 fetch 请求都会进行 URL 规则匹配,命中则进入定义的处理逻辑。如果想了解更多的处理细节,可以阅读 how routing is performed

注册路由方法签名如下,其中 pattern 的类型可以是字符串,正则表达式或者函数,而 handler 可以是 workbox.strategies 模块定义的缓存策略或者是自定义处理函数:

workbox.routing.registerRoute(
  pattern,
  handler
);

以最常用的正则表达式为例,我们想针对所有图片类型资源采用 CacheFirst 策略:

workbox.routing.registerRoute(
  /.*\.(?:png|jpg|jpeg|svg|gif)/g,
  workbox.strategies.cacheFirst({
    cacheName: 'my-image-cache',
  })
);

更多针对不同类型资源以及对应缓存策略的场景可以参考 common-recipes

设置缓存名称

设置缓存名称由 workbox.core 模块负责:

workbox.core.setCacheNameDetails({
  prefix: 'sw-tools',
  suffix: 'v1',
  precache: 'precache',
  runtime: 'runtime-cache'
});

另外在这个模块中还可以使用 setLogLevel 设置开发模式下的输出日志级别

Skip Waiting & Clients Claim

了解 Service Worker 生命周期的开发者应该知道,为了让已安装的 Service Worker 立即进入 activate 状态,我们会在 install 事件处理函数中调用
self.skipWaiting()。在 workbox 模块中,我们只需要这样:

workbox.skipWaiting();

另外,为了尽快控制还未受控制的客户端,我们会在 activate 事件处理函数中调用 clients.claim() 。在 workbox 模块中,我们需要这样:

workbox.clientsClaim();

你可能会问为什么 Workbox 不默认开启这两者呢?
关于这个问题可以参考 Workbox 的核心开发人员的回答: what-are-the-downsides-to-using-skipwaiting-and-clientsclaim-with-workbox。我们已经知道在 install 事件处理函数中会进行预缓存列表的请求,Workbox 的做法是将这些资源放入一个临时的缓存中(带有 -temp 后缀),然后在 activate 事件处理函数中将临时缓存的内容移入正式缓存,同时删除已经失效的条目。

熟悉 PRPL 模式的开发者应该知道我们会进行路由级别的 Code Splitting (代码分割),同时预缓存剩余路由,这样在实际路由跳转时才会请求对应文件(Lazy-load)。假设首次 Service Worker 安装完毕,路由 /user 对应的文件 user.123.js 已经存在于缓存之中,此时 Service Worker 发生了更新(服务器上的 user.123.js 已经变成了 user.456.js),经历了 install 阶段 user.456.js 已经处于临时缓存中,由于开启了 skipWaiting(),立刻进入 activate 阶段进行缓存清理,此时正式缓存中也只剩下了 user.456.js。假如此时用户进行路由跳转,前端运行时代码依旧会请求 user.123.js,发现缓存和服务端都已经不存在,则出现错误。而如果不开启 skipWaiting(),至少能命中缓存中的旧版本,应用依然可用。

细心的读者可能已经发现,问题的根源在于部分资源更新时用户已经打开的页面却能及时刷新。换言之,如果每次 Service Worker 更新时能通过页面 UI 引导用户刷新页面,则完全可以放心开启。这里仅给出监听 Service Worker 更新的参考实现:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('path-to-sw').then(function(reg) {
    reg.onupdatefound = function() {
      var installingWorker = reg.installing;
      installingWorker.onstatechange = function() {
        switch (installingWorker.state) {
          case 'installed':
            if (navigator.serviceWorker.controller) {
              // 触发 CustomEvent
              var event = document.createEvent('Event');
              event.initEvent('sw.update', true, true);
              window.dispatchEvent(event);
            }
            break;
        }
      };
    };
  }).catch(function(e) {
      console.error('Error during service worker registration:', e);
  });
}

完整示例

最后附上完整的 Service Worker 代码:

workbox.core.setCacheNameDetails({
  prefix: 'sw-tools',
  suffix: 'v1',
  precache: 'precache',
  runtime: 'runtime-cache'
});

workbox.skipWaiting();
workbox.clientsClaim();

workbox.precaching.precacheAndRoute(self.__precacheManifest);

workbox.routing.registerRoute(
  /.*\.(?:png|jpg|jpeg|svg|gif)/g,
  workbox.strategies.cacheFirst({
    cacheName: 'my-image-cache',
  })
);

其他注意事项

除了以上常用的 API,还有一些注意事项需要开发者留意。

超出存储大小

很多开发者都会关心缓存的使用上限问题。根据 what-is-the-storage-limit-for-a-service-worker 的问答:

In Chrome and Opera: Your storage is per origin (rather than per API). Both storage mechanisms will store data until the browser quota is reached. Apps can check how much quota they’re using with the Quota Management API (as described above).
Firefox no limits, but will prompt after 50MB data stored
Mobile Safari 50MB max
Desktop Safari unlimited (prompts after 5MB)
IE10+ maxes at 250MB and prompts at 10MB

在运行时,可以通过 StorageManager 接口获取当前缓存的使用估计值,具体使用方法可以参考 estimating-available-storage-space。不过目前支持度并不高。

为了避免超出缓存大小的情况频繁出现,我们可以为指定的缓存设置存储上限,例如存储数目和过期时间。
下面的例子来自官方文档

workbox.routing.registerRoute(
  new RegExp('\.(?:png|gif|jpg|svg)$'),
  workbox.strategies.cacheFirst({
    // You need to provide a cache name when using expiration.
    cacheName: 'images',
    plugins: [
      new workbox.expiration.Plugin({
        // Keep at most 50 entries.
        maxEntries: 50,
        // Don't keep any entries for more than 30 days.
        maxAgeSeconds: 30 * 24 * 60 * 60,
        // Automatically cleanup if quota is exceeded.
        purgeOnQuotaError: true,
      }),
    ],
  }),
);

这里有两点需要注意:

  1. 要使用 workbox.expiration 模块必须指定缓存名称,也就是这里的 cacheName
  2. purgeOnQuotaError 表示当超出缓存上限抛出 QuotaExceededError 异常时,该缓存是可以被清理的。开发者最了解各个动态缓存的使用场景,因此手动标记某些缓存为“清理安全”是十分有必要的

Opaque Response

在请求跨域资源时,非 CORS 模式下会得到 Opaque Response。这样的响应无法读取其内容,也无法获取状态码,因此 Workbox 面对这样的资源在某些缓存策略下就会出问题。

例如我们的请求得到了一个 Opaque Response,假如此时请求失败,由于无法获取状态码,Service Worker 并不知情依旧将错误的结果放入缓存,由于选择了 CacheFirst,将再也没有机会更新。另外,由于无法获取内容,浏览器在估计缓存大小时也只能采取十分保守的策略,有可能出现实际只有几 K 的响应被浏览器认为有几 M,造成缓存空间的浪费。

workbox.routing.registerRoute(
  'https://cdn.xxx.com/lib.min.js',
  workbox.strategies.cacheFirst(),
);

因此 Workbox 在默认情况下仅在 NetworkFirst 和 StaleWhileRevalidate 策略下才会缓存 Opaque Response,除此之外都会报错。

当然开发者也可以强制开启缓存,不过并不推荐这种做法。

cache polyfill

由于 Cache API 中 addAll 方法在某些低版本的安卓机型上不一定支持,所以可以引用 cache-polyfill

// src/service-worker.js

importScripts('serviceworker-cache-polyfill.js');

GA 离线统计

离线状态下的数据统计是一个很大的问题。常用的方法是离线状态下将统计数据持久化到 localStorage 中,上线时再同步。使用 workbox.googleAnalytics 模块让这一切变得十分简单:

// src/service-worker.js

workbox.googleAnalytics.initialize();

[WIP] SPA/SSR 下使用 Service Worker

一个实际项目从架构上可以分成 Server-side 或者 Client-side 两类。前者首屏加载速度快,但是后续每次页面间跳转都需要重新下载全部资源。相应的,后者首屏加载速度慢,但后续页面跳转迅速。同构应用能够集两者之长,首屏采用 SSR(Server-side Rendering)后续路由前端渲染,目前流行的前端框架都有对应的解决方案,例如 Next.jsNuxt.js

下面我们将介绍 Service Worker 在这些架构下的使用方式。

SPA 下的实践

现在我们已经了解了 App Shell 的基本概念。在 SPA 中

PRPL 模式

PRPL 模式

image

SSR 下的实践

同构思路

Service Worker 最重要的功能便是控制缓存。这里先简单介绍下预缓存或者说 sw-precache 插件的基本工作原理。

在项目构建阶段,将静态资源列表(数组形式)及本次构建版本号注入 Service Worker 代码中。在 SW 运行时(Install 阶段)依次发送请求获取静态资源列表中的资源(JS,CSS,HTML,IMG,FONT…),成功后放入缓存并进入下一阶段(Activated)。这个在实际请求之前进行缓存的过程就是预缓存。

在 SPA/MPA 架构的应用中,App Shell 通常包含在 HTML 页面中,连同页面一并被预缓存,保证了离线可访问。但是在 SSR 架构场景下,情况就不一样了。所有页面首屏均是服务端渲染,预缓存的页面不再是有限且固定的。如果预缓存全部页面,SW 需要发送大量请求不说,每个页面都包含的 App Shell 部分都被重复缓存,也造成了缓存空间的浪费。

既然针对全部页面的预缓存行不通,我们能不能将 App Shell 剥离出来,单独缓存仅包含这个空壳的页面呢?要实现这一点,就需要对后端模板进行修改,通过传入参数控制返回包含 App Shell 的完整页面 OR 代码片段。这样首屏使用完整页面,而后续页面切换交给前端路由完成,请求代码片段进行填充。这也是基于 React、Vue 等技术实现的同构项目的基本思路。

总结一下这个思路:

  1. 改造后端模板以支持返回完整页面和内容片段
  2. 服务端增加一条针对 App Shell 的路由规则,返回仅包含 App Shell 的 HTML 页面
  3. 预缓存 App Shell 页面
  4. Service Worker 拦截所有 HTML 请求,统一返回缓存的 App Shell 页面
  5. 前端路由负责代码片段的填充,完成前端渲染

实际效果是,用户第一次访问应用站点时,首屏由服务端渲染,随后 SW 安装成功后,后续的路由切换包括刷新页面都将由前端渲染完成,服务端将只负责提供 HTML 代码片段的响应。

在 Vue 中的实践

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.