浅谈如何运用 Worker 提升 web 应用体验

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

接续上篇【浅谈浏览器 Worker】,我们谈谈可以用Worker来做些什么。

用 Worker 提升性能

分担重活

在前面的介绍中,我提到可以让 Worker 分担一些重活,这样能保证主线程不会因为跑这些费时的脚本失去响应而影响用户体验。

比如当我们不得已要在前端处理大量数据的时候,我们可以让 Worker 在后台运算不影响前台交互。我们只需要在主线程中将脚本交给 Worker,并等待 Worker 发送回处理完的数据。像这样:

function getData() {
  const myWorker = new Worker("processData.js");
  return new Promise((resolve, reject) => {
    myWorker.onMessage = ({ data }) => {
      resolve(data);
      myWorker.terminate();
    };
    myWorker.onError = (e) => {
      reject(e);
      myWorker.terminate();
    };
    myWorker.postMessage({ sortBy: "update_time" });
  });
}

getData().then((data) => console.log(data));
复制代码

processData.js脚本中我们处理数据,然后将处理完的数据发送回主线程:

function process(rawData, options) {
  const processedData = rawData;
  // 省略处理数据代码一万字
  return processedData;
}

onmessage = ({ data: options }) => {
  fetch("/get_raw_data")
    .then((res) => res.json())
    .then((rawData) => process(rawData, options))
    .then((data) => postMessage(data));
};
复制代码

另一个经典的用法是在浏览器中压缩/解压缩文件。这有无数的第三方库支持,而且在库中已经内置了交托任务给 Worker 的逻辑。举栗像unzipitjs-untar……

预处理数据

Service worker 提供了一系列用于缓存数据、拦截浏览器请求的接口。这给予了我们预先拉取数据的能力。

那在主线程种,我们只需要注册 Service worker 的脚本如下:

if ("serviceWorker" in navigator) {
  // 注意脚本的路径决定了它控制的范围,如果要控制全局,就放在根目录下
  navigator.serviceWorker.register("/sw.js");
}
复制代码

然后照旧获取数据,比如用fetch什么的:

fetch("/get_data").then((res) => {
  // 如果Service worker已经针对该请求缓存了数据,那这个promise会立即被resolve
});
复制代码

在 Service worker 脚本sw.js中,我们则需要预先拉取数据并缓存:

const preFetchUrls = ["/get_data"];
caches.open("prefetchedData").then((prefetchedData) => {
  preFetchUrls.forEach((requestUrl) => {
    fetch(requestUrl).then((res) => prefetchedData.put(requestUrl, res));
  });
});
复制代码

并且利用对应的事件如fetch来拦截主线程发出的请求,返回我们预先缓存的数据:

addEventlistener("fetch", (e) => {
  const { method, url } = e.request;
  if (method !== "GET" || !preFetchUrls.includes(url)) return;
  e.respondWith(async () => {
    const prefetchedData = await caches.open("prefetchedData");
    const cachedResponse = await cache.match(event.request);
    return cachedResponse || fetch(event.request);
  });
});
复制代码

用 Workbox 简化做缓存的步骤

在上节用例中,我们利用 Service worker 缓存和请求拦截的能力预先加载了一些数据。这个能力同样也可以帮我们缓存文件,以便下次用户访问更加快速。对此,我推荐使用Workbox,它能大大简化这些缓存策略的设置。

在主线程种,我们同之前一样注册Service worker:

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/sw.js");
}
复制代码

但在 Service worker 的脚本sw.js中,我们需要引入 Workbox 库。这也意味着你的脚本需要打包工具打包。

Workbox 官网上的示例代码可以覆盖大部分的用例,照搬就可以了:

import { registerRoute } from "workbox-routing";
import {
  NetworkFirst,
  StaleWhileRevalidate,
  CacheFirst,
} from "workbox-strategies";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { ExpirationPlugin } from "workbox-expiration";

// 用网络优先的策略缓存页面(html) 。网络优先策略会先请求最新数据并缓存,请求出错再返回已缓存的数据。
registerRoute(
  ({ request }) => request.mode === "navigate",
  new NetworkFirst({
    cacheName: "pages",
    plugins: [
      // 这里设置只有当返回结果是200的才缓存
      new CacheableResponsePlugin({
        statuses: [200],
      }),
    ],
  })
);

// 用SWR策略缓存CSS、JS及Worker的脚本。SWR策略会优先返回已缓存的数据,同时在后台更新缓存。
registerRoute(
  ({ request }) =>
    request.destination === "style" ||
    request.destination === "script" ||
    request.destination === "worker",
  new StaleWhileRevalidate({
    cacheName: "assets",
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200],
      }),
    ],
  })
);

// 用缓存优先策略缓存图片。缓存优先策略会优先返回已缓存的数据,只有在没找到对应缓存的情况下才会请求网络数据并缓存。
registerRoute(
  ({ request }) => request.destination === "image",
  new CacheFirst({
    cacheName: "images",
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200],
      }),
      // 这里额外限制了缓存的最大数量,和过期的时长。
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 60 * 60 * 24 * 30, // 30天
      }),
    ],
  })
);
复制代码

假如这些是你在Service worker中只需要以上这些逻辑,那你甚至可以不写脚本,直接加个打包工具的插件就好。

Webpack用户可以用Workbox Webpack plugin

Rollup用户可以尝试rollup-plugin-workbox

你可能还没想到的一些其他用途

脱离Node.js服务器做API mock

有了拦截请求的能力,我们就可以玩点有意思的像API mock什么的。最近流行的一个库MSW就是一个很好的栗子。

原理也很简单,基本上就是:

  1. 监听"fetch"事件;
  2. 检查拦截到的请求是否满足已设定的规则;
  3. 如果满足规则,用事先缓存好的数据响应请求。

与前面在"预请求数据"一节中提到的雷同。

用作数据处理层

另一个有趣的用法是将数据处理从我们的UI层中抽离。比如说当我们追踪应用使用情况时,可以让UI层将所有数据一股脑交给Worker处理。在Worker中我们可以根据要求筛选出有意义的数据,并转换成合理的格式再发送给收集数据的服务器。

这层隔离开了相对来说没有那么重要却会影响用户体验的数据处理,即使有异常也能保证不让应用崩溃。

后台同步

你是否有遇到过像上传大量文件或服务器太忙之类的卡顿?在很多情况下用户必须等待服务器操作完成才能离开当前页面。然而Service worker的存在可以帮我们跨越这一限制!

只要浏览器还开着,Service worker就可以继续在一个独立的线程上活动。因此,我们可以先将用户操作的数据缓存,再交由Service worker在后台与服务端保持通信。用户就可以去做别的事情而不必等待数据发送完成才能离开了。

还有更多等你发现

除了以上提到的之外,worker还有更多的可能性。比如有名的PWA、推送通知、沙箱等等。发挥你的创意,让我们把Web应用再推上一层楼吧!

猜你喜欢

转载自juejin.im/post/7107785658411778056