alibaba.com的秘籍:使用 Service Worker 让首页秒开!

因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享

点击关注#互联网架构师公众号,领取架构师全套资料 都在这里

0、2T架构师学习资料干货分

上一篇:2T架构师学习资料干货分享

大家好,我是互联网架构师!

我们可以用 Stale-While-Revalidate 加速页面访问,策略分 3 步

  1. 在收到页面请求时首先检查缓存,如果命中缓存就直接从缓存中返回给用户

  2. 将缓存返回用户的同时,在后台异步发起网络请求,尝试获取资源的最新版本

  3. 获取成功后更新缓存,下次使用

图片

而这一切的幕后功臣便是 Service Worker,作为一个后台代理在网络与缓存之间搭建桥梁,提供了丰富的缓存管理和资源控制能力,从而实现这一高效策略

为了实现这一策略,需要首先了解一下 Service Worker 的核心 API

Service 基础概念

Service Worker 基础概念可以在这里了解

此处为语雀内容卡片,点击链接查看:www.yuque.com/sunluyong/f…[1]

拦截修改 Response 对象

使用 event.respondWith 可以在 fetch 事件中拦截网络请求并提供自定义响应,一旦调用浏览器会等待提供的 Promise 解析,并将其结果作为响应返回给发起请求的代码

self.addEventListener('fetch', event => {
  event.respondWith(
    /* 自定义响应逻辑 */
  );
});

比如实现拦截特定请求,可以首先尝试从缓存中获取资源,如果缓存命中则返回缓存内容,否则从网络获取资源并缓存

self.addEventListener('fetch', event => {// 过滤非页面请求const url = new URL(event.request.url);if (!url.pathname.startsWith('/page/')) return;  event.respondWith(    caches.match(event.request) // 尝试匹配缓存      .then(cachedResponse => {        if (cachedResponse) {          return cachedResponse; // 缓存命中,返回缓存内容        }        // 缓存未命中,从网络获取        return fetch(event.request);      })  );});

必须在 fetch 事件监听器内部的第一时间调用 event.respondWith,否则浏览器将继续使用默认的网络请求处理方式

clone Response 对象缓存

在 Service Worker 中处理网络请求和缓存时,经常会遇到需要 clone 响应对象

const responseToCache = networkResponse.clone();

这是由于 Response 对象是一个可读流,而流具有以下特性

  • 单次消费:Streams 在被消费后就会关闭,不能重新读取

  • 节省资源:适合处理大型数据,如视频流、文件下载等

当读取 Response 的 body 返回给浏览器后,Stream 会被读取并关闭,之后无法再次读取用于缓存。通过 clone Response 对象,可以创建一个独立的副本,确保每个副本的 Stream 都可单独消费

fetch(event.request).then(networkResponse => {
// 克隆响应用于缓存
const responseToCache = networkResponse.clone();

// 返回给客户端
  event.respondWith(networkResponse);

// 缓存克隆的响应
  caches.open(CACHE_NAME).then(cache => {
    cache.put(event.request, responseToCache);
  });
});

event.waitUntil 确保异步任务完成

Service Worker 事件都是异步的,浏览器可能在这些异步操作完成之前终止 Service Worker,导致关键任务(如缓存资源或清理旧缓存)无法正确完成

通过调用 event.waitUntil(promise),可以告诉浏览器要“等待”某个 Promise 完成之后,才认为事件处理完成,这确保了浏览器不会在关键异步操作完成之前终止 Service Worker

比如在激活阶段,通常需要清理旧的缓存

self.addEventListener('activate', event => {
console.log('[Service Worker] Activate Event');
const cacheWhitelist = ['my-cache-v2'];

  caches.keys().then(cacheNames => {
    returnPromise.all(
      cacheNames.map(cacheName => {
        if (!cacheWhitelist.includes(cacheName)) {
          console.log(`[Service Worker] Deleting old cache: ${cacheName}`);
          return caches.delete(cacheName);
        }
      })
    );
  })
});

浏览器可能在异步缓存清理任务完成之前终止激活过程,导致旧缓存可能未被正确删除,使用 event.waitUntil 可以确保所有清理操作完成

self.addEventListener('activate', event => {console.log('[Service Worker] Activate Event');const cacheWhitelist = ['my-cache-v2'];  event.waitUntil(    caches.keys().then(cacheNames => {      returnPromise.all(        cacheNames.map(cacheName => {          if (!cacheWhitelist.includes(cacheName)) {            console.log(`[Service Worker] Deleting old cache: ${cacheName}`);            return caches.delete(cacheName);          }        })      );    })  );});

Stale-While-Revalidate 实现

1、创建目录结构

.
├── app
│   └── index.js
├── package.json
└── public
    ├── favicon.ico
    ├── index.html
    └── sw.js

因为 Service Worker 需要服务端配合,为了简单使用 express 演示

npm install --save express

2、提供 web 服务

修改 app/index.js,public 目录对外服务,为了演示缓存更新效果,添加了一个带有页面版本号的自定义响应头 x-page-version

const express = require('express');const path = require('path');const app = express();const port = 3000;app.use(express.static(path.join(__dirname, '../public'), {setHeaders: (res) => {    // mock,每隔 5s version 就发生变化    res.set('x-page-version', Math.ceil(Date.now() / 5000));  }}));// 启动服务器app.listen(port, () => {console.log(`Example app listening at http://localhost:${port}`);});

3、Service Worker 实现

首先是最基础的安装、激活,代码量并不大,主要是添加了很多 log 方便观测 Service Worker 执行过程

const CACHE_NAME = 'HOMEPAGE_CACHE_v1'; // 缓存 key,sw.js 更新了可以升级版本// 配置需要缓存的资源,demo 中只缓存主文档,静态资源浏览器自己就会缓存const urlsToCache = ['/',];// 安装事件:预缓存一些关键资源self.addEventListener('install', (event) => {console.log('[Service Worker] Install Event');  event.waitUntil(    caches.open(CACHE_NAME).then((cache) => {      console.log('[Service Worker] Caching pre-defined resources');      return cache.addAll(urlsToCache);    }).catch((error) => {      console.error('[Service Worker] Failed to cache resources during install:', error);    })  );});// 激活事件:清理旧版本的缓存self.addEventListener('activate', (event) => {console.log('[Service Worker] Activate Event');const cacheWhitelist = [CACHE_NAME];  event.waitUntil(    caches.keys().then((cacheNames) => {      returnPromise.all(        cacheNames.map((cacheName) => {          if (!cacheWhitelist.includes(cacheName)) {            console.log(`[Service Worker] Deleting old cache: ${cacheName}`);            return caches.delete(cacheName);          }        })      );    }).then(() => self.clients.claim()) // 确保 SW 控制所有客户端  );});

4、劫持页面请求

// 获取事件:实现 "Stale-While-Revalidate" 策略
self.addEventListener('fetch', (event) => {const requestUrl = new URL(event.request.url);// 仅处理需要缓存的请求if (!urlsToCache.includes(requestUrl.pathname)) return;// 处理 fetch 事件  event.respondWith(    caches.match(event.request).then((cachedResponse) => {      if (cachedResponse) {        // 如果缓存存在,立即返回缓存内容        console.log(`[Service Worker] Serving from cache: ${event.request.url}`);        // 后台发起网络请求以更新缓存        event.waitUntil(          fetch(event.request).then((networkResponse) => {            if (networkResponse && networkResponse.status === 200) {              return caches.open(CACHE_NAME).then((cache) => {                // 缓存最新内容,下次使用                cache.put(event.request, networkResponse.clone());                console.log(`[Service Worker] Fetched and cached (background): ${event.request.url}`);              });            }          }).catch((error) => {            console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);          })        );        return cachedResponse; // 立即返回缓存内容      }      // 如果缓存不存在,从网络获取最新资源      return fetch(event.request).catch((error) => {        console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);      });    })  );});

这样就基本实现了 Stale-While-Revalidate

5、注册 Service Worker

在主线程激活 Service Worker

if ("serviceWorker"in navigator) {
  navigator.serviceWorker.register("/sw.js").then(registration => {
    console.log(`Service Worker registered with scope: ${registration.scope}`);
  }).catch(error => {
    console.log(`Service Worker registration failed: ${error}`);
  });
}

更进一步

可以对上面 demo 改进一下,当获取到最新版本页面后和缓存对比,如果发现页面版本已更新,可以给主线程发送通知,让页面重新发请求,获取最新版本的缓存

更新 fetch 事件处理

// 获取事件:实现 "Stale-While-Revalidate" 策略
self.addEventListener('fetch', (event) => {const requestUrl = new URL(event.request.url);// 仅处理需要缓存的请求if (!urlsToCache.includes(requestUrl.pathname)) return;// 处理 fetch 事件  event.respondWith(    caches.match(event.request).then((cachedResponse) => {      if (cachedResponse) {        // 如果缓存存在,立即返回缓存内容        console.log(`[Service Worker] Serving from cache: ${event.request.url}`);        // 后台发起网络请求以更新缓存        event.waitUntil(          fetch(event.request).then((networkResponse) => {            if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {              // 获取缓存响应中的版本              const cachedVersion = cachedResponse.headers.get('x-page-version');              // 获取网络响应中的版本              const networkVersion = networkResponse.headers.get('x-page-version');              console.log(`[Service Worker] Cached Version: ${cachedVersion}`);              console.log(`[Service Worker] Network Version: ${networkVersion}`);              // 如果页面版本已更新              if (networkVersion !== cachedVersion) {                return caches.open(CACHE_NAME).then((cache) => {                  cache.put(event.request, networkResponse.clone());                  console.log(`[Service Worker] Fetched and cached (background): ${event.request.url}`);                  // 通知客户端刷新,展示最新内容                  return sendMessage({                    version: networkVersion,                    action: 'update',                    url: event.request.url,                  });                });              }            }          }).catch((error) => {            console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);          })        );        return cachedResponse; // 立即返回缓存内容      }      // 如果缓存不存在,从网络获取最新资源      return fetch(event.request).catch((error) => {        console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);      });    })  );});// 辅助函数:发送消息给客户端functionsendMessage(data) {return self.clients.matchAll().then((clients) => {    clients.forEach((client) => {      client.postMessage(data);    });  });}

更新主线程,添加接收来自 Service Worker 消息事件

navigator.serviceWorker.addEventListener("message", event => {
  console.log('Received a message from Service Worker:', event.data);
  if (event.data.action === "update") {
    if (event.data.url === window.location.href ) {
      console.log('load lasted version');
      location.href = event.data.url;
    }
  }
});

这就是 alibaba.com[2] 秒开的秘籍。

下载地址,见:

清华出的DeepSeek使用手册,104页,真的是太厉害了!(免费领取)

—  —

如喜欢本文,请点击右上角,把文章分享到朋友圈


1、2T架构师学习资料干货分享


2、10000+TB 资源,阿里云盘,牛逼!!

3、基本涵盖了Spring所有核心知识点总结

4、DeepSeek创始人梁文锋个人履历

逆天,面试被挂的原因竟然是,搜了女仆......

DeepSeek全方位指南:10分钟快速上手

字节跳动员工自爆工资收入明细,网友:离谱啊!

  · END ·

最后,关注公众号互联网架构师,在后台回复:2T,可以获取我整理的 Java 系列面试题和答案,非常齐全。

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描上方二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连点赞、转发、在看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值