返回

React Vite Service Worker 无法缓存 .tsx/.jsx 文件?原因及解决方案

javascript

Service Worker 在 React Vite 中无法缓存 .tsx/.jsx 文件的问题探究

使用 Service Worker 可以增强 Web 应用的离线能力和加载速度。但有时候,开发者可能会遇到 Service Worker 无法正确缓存 .tsx 或 .jsx 文件的情况,导致离线时页面无法正常显示。 这篇文章将深入分析问题根源,并提供切实可行的解决方案。

问题分析

通常,出现此类问题的主要原因是 Vite 的构建流程和 Service Worker 的工作方式存在不匹配。Vite 作为一款高速前端构建工具,默认对项目中的资源进行处理和优化,比如文件名添加 hash 值等操作。这与 Service Worker 预期缓存静态资源的方式不同。 当 Service Worker 尝试访问未经过特殊处理的原始文件路径时,将无法命中缓存。

一个常见的错误是将未构建的文件路径添加到 Service Worker 的 OFFLINE_ASSETS 列表中,如下面的代码示例,其中尝试缓存 /src/main.tsx,这是一个源代码路径,在部署中会被 Vite 处理,生成实际用于部署的js bundle。

const OFFLINE_ASSETS = [
  "/",
  "/src/main.tsx", // 此路径有问题,应该改为构建后的输出路径
  "/vite.svg",
  `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmCy16nhIbV3pI1qLYHMJKwbH2458oiC9EmA&s`,
];

因此,Service Worker 会因为在预期的位置找不到这些静态资源而失败。这与像 "/vite.svg" 或 远程图片可以被缓存形成鲜明对比,因为这些资源没有通过构建流程转化, 而是直接可以被 Service Worker 获取和缓存。

解决方案

下面介绍一些有效的方法来解决这个问题。

方案一:缓存构建后的文件

最直接的解决方案是确保 Service Worker 缓存的是 Vite 构建后的实际资源,而不是源代码路径。Vite 构建会将你的 main.tsx 以及其他的依赖打包成 .js 文件,以及其他静态资源文件。这些文件名可能会带有 hash 值。这些构建后的资源需要通过配置正确的路径加载和缓存,并且通过环境变量获取最终生成的输出文件名。

  1. 生成 manifest 文件:
    可以通过 Vite 插件,生成一个 manifest 文件 (例如 manifest.json), 此文件包含所有构建后的静态资源的对应关系(如输入文件与输出文件名),之后,service worker 读取此文件缓存对应路径。以下示例使用 vite-plugin-pwa,可配置输出 manifest.json:

    npm install vite-plugin-pwa -D
    

    vite.config.ts 添加以下内容:

    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    import { VitePWA } from 'vite-plugin-pwa'
    
    export default defineConfig({
      plugins: [
        react(),
        VitePWA({
          manifest:{
             //配置manifest文件相关信息
          },
            // PWA 相关的其他设置
          registerType: 'autoUpdate',
          workbox: {
            globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
          }
    
        })
      ],
    })
    

配置workbox,保证vite-plugin-pwa会缓存相关的文件,当然这里是根据文件名类型做了glob,也可以根据路径进行匹配。
2. 修改 Service Worker 代码: 读取 manifest.json 文件,使用文件中的构建后路径。需要获取构建输出的目录和资源路径映射。

// 假定构建后的静态资源位于 dist/assets 目录
async function loadManifest() {
    const response = await fetch('./manifest.json');
    const manifest = await response.json();
    const assets = []
    for(let k in manifest) {
        assets.push(manifest[k].file)
        assets.push(manifest[k].css)
    }
     console.log("manifest_assets", assets);
     return assets
 }

 const addResourcesToCache = async (resources) => {
  const cache = await caches.open("v1");
    const results = resources.filter(item => item) // 处理资源加载时候,有可能undefined情况出现
     await cache.addAll(results);
  };

 self.addEventListener("install", async (e) => {
  console.log("installing");
     const manifest_assets = await loadManifest()
    e.waitUntil(addResourcesToCache(manifest_assets));
 });
 这样Service Worker就会缓存Vite 构建后的静态资源,从而使你的 React 应用离线可用。

方案二:使用预缓存列表

一个可选的方法是在构建时生成预缓存列表。

  1. 编写构建脚本: 在 package.json 中,可以通过编写脚本在每次构建之前从vite.manifest生成可预缓存的文件列表:
{
 "scripts": {
     "build:manifest":"node ./scripts/build-manifest.js && vite build"
    }
}

在项目根目录下创建一个名为scripts/build-manifest.js 的文件, 代码如下:

    import {readFile,writeFile} from 'fs/promises'
  async function build_manifest() {

    const buildManifest = await readFile('./dist/manifest.json','utf8')
    const assets = JSON.parse(buildManifest)

     const targetAssetList= []

    for(let k in assets){
            targetAssetList.push(`/` + assets[k].file);

            if(assets[k].css){
                 targetAssetList.push(`/` + assets[k].css);
            }
     }

    let template =` const PRECACHE_ASSETS = ${JSON.stringify(targetAssetList)} ; export { PRECACHE_ASSETS }`

        await writeFile('./src/precache_asset_list.js', template, "utf-8")
}
  build_manifest();

通过 node 脚本解析dist/manifest.json 中的内容,将其路径提取出来,并生成 precache_asset_list.js 文件

  1. Service Worker 代码中导入 precache_asset_list.js, 进行使用 :
import { PRECACHE_ASSETS} from './precache_asset_list.js';

   const addResourcesToCache = async (resources) => {
    const cache = await caches.open("v1");
   const results = resources.filter(item => item)
     await cache.addAll(results);
   };

self.addEventListener("install", async (e) => {
  console.log("installing");
  e.waitUntil(addResourcesToCache(PRECACHE_ASSETS));
});

此方案核心也是需要获取构建后的资源路径,手动实现了一遍读取 manifest 并传递给 Service Worker 的过程,相较于第一种方案而言稍微繁琐些,但是它减少了对 vite-plugin-pwa 等工具的依赖。

其他安全建议

在处理缓存时需要注意缓存策略,确保在代码变更时 Service Worker 会更新,避免出现用户端始终是旧版本的问题。在注册和更新 Service Worker 时,可以适当增加调试信息。
通过合理的配置和策略,可以让 Service Worker 更好地为 React 应用服务。