Service Worker 提高首页的打开速度

目录

实战分享:利用 Service Worker 实现首页秒开

demo目录

实现

1. 缓存配置

2. 安装事件

3. 激活事件

4. 拦截请求事件

5. 辅助函数:发送消息给客户端

结果


实战分享:利用 Service Worker 实现首页秒开

大家好!在前端开发的过程中,我们都在不断追求网页性能的优化,期望给用户带来更流畅、快速的浏览体验。今天,我想和大家分享一个我在实践中探索的成果 —— 使用 Service Worker 让首页秒开的实战案例。

在这个项目中,我将详细展示从搭建基础环境到逐步实现 Service Worker 缓存策略的全过程,包括如何处理页面和接口请求的缓存,如何应对版本更新带来的变化,以及在这个过程中遇到的各种棘手问题,像资源路径错误导致的缓存失败、接口请求出现 404 找不到资源的情况、缓存状态无法正确获取和显示等,我都将一一为大家呈现是如何解决的。

这不仅仅是一个简单的代码示例,更是一个充满实战经验和解决方案的分享。无论你是初涉前端领域的新手,还是已经有一定经验的开发者,相信都能从这个案例中获得启发和帮助,让我们一起深入探究如何借助 Service Worker 提升网页的性能表现吧!

demo目录

// demo 目录
.
├── app
│   └── index.js
├── package.json
└── public
    ├── index.html
    └── sw.js

实现

package.json里我没有家很多东西,只是加了一个启用服务端的命令和三个 服务端需要用到的

{
  "name": "service_worker",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node app/index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "path": "^0.12.7"
  }
}

因为这是一个demo, 所以index.js 文件是用express临时写的一个服务端,

const express = require('express');
const path = require('path');
const app = express();
const cors = require('cors');
const port = 3000;

// 模拟接口数据
const apiData = {
  message: "这是接口返回的数据",
  version: 1
};

app.use(cors());


// 处理接口请求,返回模拟的JSON数据,并设置页面版本头信息
app.get('/api/data', (req, res) => {
  // 这里模拟接口数据版本变化,可根据实际情况调整逻辑
  apiData.version = Math.ceil(Date.now() / 5000);
  res.set('x-page-version', apiData.version);
  res.json(apiData);
});


app.use(express.static(path.join(__dirname, '../public'), {
  setHeaders: (res) => {
    // 这里模拟页面版本变化,每隔5秒version就发生变化
    res.set('x-page-version', Math.ceil(Date.now() / 5000));
  }
}));

// 启动服务器
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Service Worker Demo</title>
</head>

<body>
  <h1>Service Worker Demo</h1>
  <p id="cache-status">缓存状态:未获取</p>
  <p id="page-version">页面版本:未获取</p>
  <button id="fetchDataButton">获取接口数据</button>
  <div id="data-display"></div>
  <script>
    // 注册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}`);
      });
    }

    // 处理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.reload(true);
        }
      }
      // 接收并处理初始状态消息
      if (event.data.action === 'setInitialStatus') {
        updateCacheStatus(event.data.cacheStatus, event.data.pageVersion);
      }
    });

    const fetchDataButton = document.getElementById('fetchDataButton');
    const dataDisplay = document.getElementById('data-display');

    fetchDataButton.addEventListener('click', () => {
      // 发起接口请求
      fetchData();
    });

    async function fetchData() {
      try {
        const response = await fetch('http://127.0.0.1:3000/api/data');
        if (response.ok) {
          const data = await response.json();
          dataDisplay.textContent = JSON.stringify(data);
        } else {
          console.error('接口请求失败,状态码:', response.status);
        }
      } catch (error) {
        console.error('接口请求出现错误:', error);
      }
    }

    // 更新页面上显示的缓存状态和版本信息
    function updateCacheStatus(status, version) {
      document.getElementById('cache-status').textContent = `缓存状态:${status}`;
      document.getElementById('page-version').textContent = `页面版本:${version}`;
    }
  </script>
</body>

</html>

接下来是最主要的也是最核心的代码sw.js

1. 缓存配置

const CACHE_NAME = 'HOMEPAGE_CACHE_v1';
const urlsToCache = ['/'];
  • CACHE_NAME:定义了缓存的名称,每次更新Service Worker时可以改变这个名称来触发缓存更新。
  • urlsToCache:定义了需要缓存的资源列表,这里只缓存了主页。

2. 安装事件

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(urlsToCache);
    }).catch((error) => {
      console.error('[Service Worker] Failed to cache resources during install:', error);
    })
  );
});
  • 当Service Worker安装时,会触发install事件。这里使用event.waitUntil确保缓存操作完成后再进行下一步。
  • caches.open(CACHE_NAME)打开缓存,cache.addAll(urlsToCache)将预定义的资源添加到缓存中。

3. 激活事件

self.addEventListener('activate', (event) => {
  const cacheWhitelist = [CACHE_NAME];
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (!cacheWhitelist.includes(cacheName)) {
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => self.clients.claim())
  );
});
  • 当Service Worker激活时,会触发activate事件。这里使用event.waitUntil确保清理旧缓存操作完成后再进行下一步。
  • caches.keys()获取所有缓存名称,cacheNames.map遍历所有缓存,如果不在白名单中则删除。

4. 拦截请求事件

self.addEventListener('fetch', (event) => {
  const requestUrl = new URL(event.request.url);
  if (urlsToCache.includes(requestUrl.pathname)) {
    event.respondWith(
      caches.match(event.request).then((cachedResponse) => {
        if (cachedResponse) {
          const cachedVersion = cachedResponse.headers.get('x-page-version');
          event.waitUntil(
            fetch(event.request).then((networkResponse) => {
              if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
                const networkVersion = networkResponse.headers.get('x-page-version');
                if (networkVersion !== cachedVersion) {
                  return caches.open(CACHE_NAME).then((cache) => {
                    cache.put(event.request, networkResponse.clone());
                    if (requestUrl.pathname === '/') {
                      return sendMessage({
                        version: networkVersion,
                        action: 'update',
                        url: requestUrl.href
                      });
                    }
                  });
                }
              }
            }).catch((error) => {
              console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);
            })
          );
          return cachedResponse;
        }
        return fetch(event.request).then((networkResponse) => {
          if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
            const networkVersion = networkResponse.headers.get('x-page-version');
            if (requestUrl.pathname === '/') {
              return sendMessage({
                action: 'setInitialStatus',
                cacheStatus: '从网络获取并缓存',
                pageVersion: networkVersion
              }).then(() => networkResponse);
            }
            return networkResponse;
          }
        }).catch((error) => {
          console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);
        });
      })
    );
  } else {
    event.respondWith(fetch(event.request));
  }
});
  • 当有网络请求时,会触发fetch事件。
  • 如果请求的资源在urlsToCache中,则尝试从缓存中获取资源。
    • 如果缓存中有资源,则返回缓存内容,并在后台发起网络请求以更新缓存。
    • 如果缓存中没有资源,则从网络获取最新资源。
  • 对于不在urlsToCache中的请求,直接转发请求。

5. 辅助函数:发送消息给客户端

function sendMessage(data) {
  return self.clients.matchAll().then((clients) => {
    clients.forEach((client) => {
      client.postMessage(data);
    });
  });
}
  • sendMessage函数用于向所有客户端发送消息,这里用于通知客户端缓存状态或页面版本更新。

完整代码: 

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) => {
      return Promise.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 控制所有客户端
  );
});

// 拦截页面请求,实现Stale-While-Revalidate策略
self.addEventListener('fetch', (event) => {
  const requestUrl = new URL(event.request.url);
  // 处理首页及接口请求的缓存逻辑
  if (urlsToCache.includes(requestUrl.pathname)) {
    event.respondWith(
      caches.match(event.request).then((cachedResponse) => {
        if (cachedResponse) {
          // 如果缓存存在,立即返回缓存内容,并在后台更新缓存(发起请求)
          console.log(`[Service Worker] Serving from cache: ${event.request.url}`);
          const cachedVersion = cachedResponse.headers.get('x-page-version');
          event.waitUntil(
            fetch(event.request).then((networkResponse) => {
              if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
                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}`);
                    // 通知客户端刷新,展示最新内容(如果是首页),或者更新缓存数据(如果是接口请求)
                    if (requestUrl.pathname === '/') {
                      return sendMessage({
                        version: networkVersion,
                        action: 'update',
                        url: requestUrl.href
                      });
                    }
                  });
                }
              }
            }).catch((error) => {
              console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);
            })
          );
          return cachedResponse;
        }
        // 如果缓存不存在,从网络获取最新资源(发起请求)
        return fetch(event.request).then((networkResponse) => {
          if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
            const networkVersion = networkResponse.headers.get('x-page-version');
            // 向客户端发送初始状态消息,并返回网络响应
            if (requestUrl.pathname === '/') {
              return sendMessage({
                action: 'setInitialStatus',
                cacheStatus: '从网络获取并缓存',
                pageVersion: networkVersion
              }).then(() => networkResponse);
            }
            return networkResponse;
          }
        }).catch((error) => {
          console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);
        });
      })
    );
  } else {
    // 对于其他非缓存资源请求,直接转发请求,不做额外处理
    event.respondWith(fetch(event.request));
  }
});

// 辅助函数:发送消息给客户端
function sendMessage(data) {
  return self.clients.matchAll().then((clients) => {
    clients.forEach((client) => {
      client.postMessage(data);
    });
  });
}

结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值