Service Worker在电子菜单中的实际应用

一、分享背景与业务场景

针对门店菜单电子屏长时间开机、网络差、易白屏的痛点,通过 Service Worker 实现前端离线缓存与请求拦截,确保断网仍可正常展示菜单,提升顾客体验与门店运营稳定性,同时沉淀可复用的离线前端方案。

1.1 业务场景

菜单电子屏是一套面向门店的 Vue 3 前端应用,运行在店内大屏、平板等设备上,核心用于展示实时菜单、热销商品、多时段菜单等内容。

门店设备通常处于长时间开机 状态,且网络环境不稳定 ,无法依赖稳定网络支撑应用运行,因此对离线可用缓存策略有强需求,需保障核心功能在断网 / 弱网下正常使用。


1.2 为什么使用Service Worker

Service Worker 是浏览器后台运行的代理脚本 ,独立于页面线程,具备拦截所有网络请求、管理本地缓存、实现离线业务逻辑的核心能力,其价值与菜单电子屏业务高度匹配。

离线可用:断网不影响核心功能

Service Worker 在安装阶段预缓存、首次加载后懒缓存菜单基础资源(Vue 框架、静态样式、菜单模板、默认菜品图片);当设备断网时,自动拦截网络请求,直接从本地缓存返回资源,确保菜单大屏仍展示最后一次同步的菜单数据,不出现白屏 / 报错。

业务价值:

门店断网、网络波动时,顾客仍可正常查看菜单、热销品等核心信息,不影响门店基础运营,提升顾客体验、降低运营投诉。


1.3 实际效果

因此,选用Service Worker 作为离线与缓存方案的核心,并结合业务做了策略细化与联动。


二、Service Worker 详解

2.1 什么是 Service Worker

Service Worker 基于浏览器独立线程 运行,不与页面主线程耦合,遵循:install → activate → active 生命周期。

  • 通过浏览器原生 Cache API 管理本地缓存
  • 通过拦截 fetch 请求 实现请求重定向与缓存返回
  • 是前端 PWA 离线能力的核心技术

2.2 生命周期

Service Worker 从"被注册"到"真正接管页面请求",会经历几个状态,理解它们有助于排查"为什么没生效""为什么还是旧缓存"。

暂时无法在飞书文档外展示此内容

要点

  • Installing :脚本首次或更新后解析成功,触发 install。这里常做"预缓存"(把关键 HTML/JS/CSS 提前放进 Cache)。
  • Waiting :如果当前已有旧版 SW 在控制页面,新 SW 会停在这里,直到旧 SW 控制的页面全部关闭,或主线程调用了 skipWaiting()
  • Activating :新 SW 开始接管,触发 activate。这里常做"清理旧版本用的 Cache 名字"。
  • Activated :之后页面发出的、落在 SW 作用域内的 fetch 才会被 fetch 事件拦截。
  • Idle / Terminated:一段时间没有 fetch,浏览器可能把 SW 挂起或终止,下次有请求再拉起来。

我们项目里用了 skipWaiting: trueclientsClaim: true,所以新 SW 会尽快激活并立刻接管所有客户端,不用等用户关掉所有标签页。


2.3 作用域(scope)与注册

Service Worker 只对自己作用域下 的请求生效。作用域由注册时传入的 path 决定,默认是 sw.js 所在目录。

例如:

  • sw.jshttps://example.com/static/sw.js,默认 scope 为 https://example.com/static/
  • 只有该路径及其子路径 下的页面(如 /static/app/)发出的请求会被这个 SW 拦截;/other/ 下的页面不会。

注册方式(主线程):

javascript 复制代码
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', { scope: '/' })
  .then(reg => console.log('SW 注册成功', reg.scope))
    .catch(err => console.error('SW 注册失败', err))
}

2.4 拦截请求:fetch 事件

csharp 复制代码
简化版手写示例:
self.addEventListener('fetch', (event) => {
  const url = event.request.url
  // 只处理同源或指定的接口/资源
  if (!url.includes('/api/menu')) return
  event.respondWith(
    fetch(event.request)
      .then(res => {
        const clone = res.clone()
        caches.open('menu-api-cache').then(cache => cache.put(event.request, clone))
        return res
      })
      .catch(() => caches.match(event.request))
  )
}) 

2.5 Cache API 与 Cache Storage

Service Worker 用的"缓存"不是 localStorage,而是 Cache API (浏览器里常叫 Cache Storage)。

  • caches :全局对象,类似 caches.open('my-cache-name') 得到一个 Cache 对象。
  • 每个 Cache 里存的是 Request → Response 的键值对,键是请求对象,值是响应对象。
  • 同一个域名下可以有多个 Cache(例如我们项目里的 storemenu-api-cachestoremenu-pic-cachejs-cache),互不覆盖,便于按"接口 / 图片 / 静态资源"分开管理和过期。

常用方法:

csharp 复制代码
 // 打开(或创建)一个命名缓存
const cache = await caches.open('storemenu-api-cache')

// 存:请求 + 响应
await cache.put(request, response)

// 取:只根据 request 查,返回 response 或 undefined
const response = await cache.match(request)

// 删
await cache.delete(request)

// 列出该 Cache 里所有 request
const keys = await cache.keys()

三、技术架构概览

3.1 技术栈与缓存策略

  • 构建: Vite 3
  • PWA/Service Worker : vite-plugin-pwa + Workbox(runtimeCaching)
  • 前端: Vue 3 + Pinia + Vue Router
php 复制代码
  VitePWA({
      registerType: 'autoUpdate', //生成的注册脚本会自动检查SW更新,发现新版本时在后台下载并切换
      srcDir: './', //指定 Service Worker 源文件所在目录。
      filename: 'sw.js', //生成的 SW 文件名(最后会出现在站点根目录,例如 https://xxx/sw.js)。
      includeAssets: ['favicon.ico'],//指定额外要预缓存的静态资源(通常不在 Vite 打包产物里),比如站点图标。
      injectRegister: 'auto', //让插件 自动在入口文件里注入 SW 注册代码,不用你手写 navigator.serviceWorker.register(...)。
      workbox: {
        cacheId: 'E-menu-cache', //给当前这套 Service Worker 的缓存起一个"前缀 ID"。
        cleanupOutdatedCaches: true, //自动清理 已经不再被当前 SW 配置使用的旧 cache。
        skipWaiting: true, //对应生命周期里的 skipWaiting()。
        clientsClaim: true, //对应生命周期里的 clients.claim()。
        runtimeCaching: [
          {
            urlPattern: /.*/(storemenu-api).*/,//用正则或字符串匹配要处理的 URL
            handler: 'NetworkFirst', //使用的策略,如 'NetworkFirst'、'CacheFirst' 等
            options: {
              cacheName: 'storemenu-api-cache', //对应的 cache 名字
              cacheableResponse: {
                statuses: [200],//只有状态码在这个列表里的响应才会被写进缓存
              },
              expiration: {
                maxEntries: 1, // 只保留一份接口缓存
                maxAgeSeconds: 7 * 24 * 60 * 60, // 缓存7天
              },
            },
          },
        ],
      },
    }),

PWA(Progressive Web App)通常包含:

  • HTTPS 部署(SW 只工作在安全源下)
  • Service Worker 做离线与缓存
  • Web App Manifest(图标、名称、主题色、是否全屏等)

Service Worker 是 PWA 能"离线用、秒开"的核心;没有 SW,就只是普通网页。我们项目通过 vite-plugin-pwa 同时生成了 manifest 和基于 Workbox 的 sw.js,所以既满足"可安装到桌面",又满足"菜单电子屏离线可用"。


3.2 常见缓存策略

策略本质是"先网络还是先缓存、失败时怎么回退"。下面用一句话 + 我们项目里的用法概括:

策略 逻辑(一句话) 典型用途 本项目
NetworkFirst 先请求网络,成功则返回并写入缓存;失败则用缓存 希望尽量新、又要离线兜底 菜单 API、图片、JS/CSS
CacheFirst 先查缓存,有则返回;没有再请求网络并写入缓存 版本化静态资源、不常变的图片 未用(图片我们改为 NetworkFirst 以便更新)
StaleWhileRevalidate 先返缓存(若有),同时后台请求网络,下次用新响应 首屏要快、数据可略旧一版 未用
NetworkOnly 只走网络,不写缓存 必须实时的接口 未用
CacheOnly 只读缓存,没有就失败 预缓存好的 App Shell 未用

我们清一色用 NetworkFirst,保证在线时拿到最新数据/图片,离线时再回退到 Cache,和"菜单要新、又要抗断网"的需求一致。


3.3 多时段菜单 + 图片预加载与离线切换

场景:早/午/晚等多时段菜单,每个时段有不同背景图。若只缓存"当前时段"的图,离线切换到其他时段会缺图。

做法

  1. 一次拉全量 :菜单 API 返回所有时段的 menuTimePeriodDisplayVO,前端缓存在内存(如 cachedMenuData)。
  2. 预加载所有时段图片 :在线拿到数据后,对每个时段的 backgroundUrl 以及公共图(如 discountImgUrlhotImgUrl 等)做预加载。
  3. 通过 fetch 写入 SW 缓存 :预加载时用 new Image() 触发加载,再对同一 URL 执行 fetch(url, { cache: 'reload' }),让 SW 的 storemenu-pic-cache 写入该图片。
  4. 离线切换 :时段切换仅改前端展示的数据和图片 URL,不再发请求;图片从 storemenu-pic-cache 读取,避免 no-response
javascript 复制代码
// 预加载单张图片并触发 SW 缓存(先对比缓存,避免重复下载)
const cacheImage = async (imageUrl, imageName = '') => {
  const cachedUrl = await getCachedImageUrl(imageUrl) // 从 storemenu-pic-cache keys 对比
  if (cachedUrl === imageUrl) return

  const img = new Image()
  img.src = imageUrl
  img.onload = async () => {
    await fetch(imageUrl, { cache: 'reload' })  // 写入 storemenu-pic-cache
  }
}

// 预加载所有时段背景图 + 公共图
const preloadTimePeriodImages = async () => {
  // 公共图
  if (cachedMenuData.value?.discountImgUrl) await cacheImage(cachedMenuData.value.discountImgUrl, 'discountImg')
  // ...
  // 各时段背景图
  for (const menu of cachedMenuData.value.menuTimePeriodDisplayVO || []) {
    if (menu.backgroundUrl) await cacheImage(menu.backgroundUrl, `timePeriod_${menu.timePeriodId}`)
  }
}

作用

  • 预加载前用 getCachedImageUrl 对比当前缓存,避免重复下载。
  • 多时段数据 + 多时段图片全部进缓存,离线可按时段自动切换且无缺图。

3.4 资源缓存策略

目标:既保证离线可用,又避免无用图片长期占用(如门店更换菜单后旧图仍占缓存)。

实现

  1. SW 层storemenu-pic-cache 使用 expiration.maxEntriesmaxAgeSecondspurgeOnQuotaError 做自动淘汰。

  2. 应用层 :在预加载或数据更新后,执行 cleanupUnusedImageCache()

    1. 根据当前 cachedMenuData 汇总"当前仍需要的图片 URL"(含各时段背景、公共图等)。
    2. 打开 storemenu-pic-cache,遍历 keys(),删除不在上述集合内且属于 storemenu-pic 的请求。
javascript 复制代码
const cleanupUnusedImageCache = async () => {
  const currentImageUrls = new Set()
  // 收集 公共图 + 所有时段 backgroundUrl
  // ...
  const picCache = await caches.open('storemenu-pic-cache')
  const cachedRequests = await picCache.keys()
  for (const request of cachedRequests) {
    if (isStoremenuPic(request.url) && !currentImageUrls.has(request.url)) {
      await picCache.delete(request)
    }
  }
}

效果:缓存内容与当前菜单配置对齐,存储可控,离线仍能正常显示当前及多时段所需图片。

3.5调试与注意点

  • Chrome DevTools → Application → Service Workers:查看当前页面的 SW 状态(installing/waiting/activated)、可 Unregister、Update、勾选 Update on reload 方便开发时每次刷新都更新 SW。
  • Application → Cache Storage:看各 Cache 里的 Request/Response,可手动删某条或清空,用来验证"单份""清理"是否生效。
  • Application → Storage → Clear site data:会清掉 SW 和所有 Cache,适合做"首次访问"测试。
  • 开发时若改了 vite.config.js 里 workbox 配置,需要重新 build 才会生成新的 sw.jsregisterType: 'autoUpdate' 会在下次打开页面时自动用新 SW。

四、整体请求与缓存流程

  1. 首次在线访问

    1. 页面加载 → SW 注册并接管 fetch。
    2. 请求菜单 API → SW NetworkFirst → 网络成功 → 写入 storemenu-api-cache(仅 1 条)。
    3. 前端保存 cachedMenuData,执行 cleanupApiCache + ensureApiCache(apiUrl)
    4. 若在线,执行 preloadTimePeriodImages(),所有时段背景图及公共图通过 fetch 进入 storemenu-pic-cache;再执行 cleanupUnusedImageCache()
  2. 在线再次访问 / 定时刷新

    1. 菜单 API 再次请求 → 网络成功 → SW 更新同一条 API 缓存;应用层再次清理并 ensure 当前 URL。
    2. 预加载与清理同上,保证缓存与当前配置一致。
  3. 离线访问

    1. 菜单 API 请求 → SW NetworkFirst 网络失败 → 从 storemenu-api-cache 返回唯一一条缓存。
    2. 前端用该数据填充 cachedMenuData,按当前时间筛选时段并展示;图片请求由 SW 从 storemenu-pic-cache 返回,多时段切换无需网络。

五、小结

在菜单电子屏项目中,Service Worker 不仅提供了离线可用能力,还通过 API 单份、多时段图片预加载、应用层 + SW 双端清理 等设计,在"数据尽量新"和"离线稳定展示"之间取得平衡,并把不同类型资源拆分到独立缓存桶中,同时缓存体量可控、不再无限膨胀。上述模式可直接复用到其他需要离线优先、多版本/多时段资源的大屏或 PWA 场景中。

相关推荐
前端Hardy2 小时前
NW.js v0.109.1 最新稳定版发布:被遗忘的桌面开发神器?启动快 3 倍,内存省 70%!
前端·javascript·vue.js
Kath2 小时前
[归档][2022-05-16]opensumi看码记录
前端
清风徐来QCQ2 小时前
跨域问题(CORS-Cross-Origin Resource Sharing跨域资源共享)
前端
DanCheOo2 小时前
我写了一个 AI 代码质量流水线,一行命令搞定 Review + 修复 + 测试 + 报告
前端·ai编程
yaaakaaang2 小时前
(六)前端,如此简单!--- 三类通讯
前端
Jinuss3 小时前
源码分析之React中副作用Effect全流程
前端·javascript·react.js
踩着两条虫3 小时前
VTJ.PRO 在线应用开发平台的低代码引擎与DSL系统
前端·低代码·ai编程
Yiyaoshujuku3 小时前
医院API接口,从医院真实世界数据HIS、LJS、EMR、PACS系统到医院药品流向数据....
大数据·前端·人工智能
Shirley~~3 小时前
力扣hot100:相交链表
前端·算法