From c26bd23da435ad1e60e29b7eceaba0bb262e8f8d Mon Sep 17 00:00:00 2001 From: liquidliang Date: Wed, 21 Jan 2026 10:01:59 +0800 Subject: [PATCH] =?UTF-8?q?docs(claude):=20=E6=B7=BB=E5=8A=A0=20Service=20?= =?UTF-8?q?Worker=20=E9=A2=84=E7=BC=93=E5=AD=98=E6=9C=BA=E5=88=B6=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 记录了 Precache Manifest 机制的工作原理: - 构建时生成资源清单 - SW 安装时预缓存所有资源 - 增量更新跳过未变化文件 --- CLAUDE.md | 57 +++++++++++++++++++ .../src/components/ttd-dialog/ttd-dialog.tsx | 5 +- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d5df8fb9..7ce5a14e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -587,6 +587,63 @@ taskQueueService.observeTaskUpdates().subscribe((event) => { }); ``` +### Service Worker 预缓存机制 (Precache Manifest) + +项目使用 **Precache Manifest** 机制确保版本更新时用户能快速加载新版本: + +**问题**:如果 SW 只预缓存少量基础文件(如 index.html),版本更新后用户首次访问需要从网络下载所有 JS/CSS,导致加载慢。 + +**解决方案**: + +1. **构建时生成资源清单** (`vite.config.ts`): +```typescript +// Vite 插件在构建完成后扫描 dist 目录 +function precacheManifestPlugin(): Plugin { + return { + name: 'precache-manifest', + closeBundle: { + async handler() { + // 扫描所有静态资源,生成 precache-manifest.json + // 包含 URL 和文件哈希(用于增量更新) + manifest.push({ url: '/assets/xxx.js', revision: 'a1b2c3d4' }); + } + } + }; +} +``` + +2. **SW 安装时预缓存所有资源** (`sw/index.ts`): +```typescript +sw.addEventListener('install', (event) => { + event.waitUntil( + (async () => { + const files = await loadPrecacheManifest(); // 读取 manifest + await precacheStaticFiles(cache, files); // 并发预缓存 + })() + ); +}); +``` + +3. **增量更新**:通过 `x-sw-revision` 头比较文件哈希,跳过未变化的文件 + +**工作流程**: +``` +版本更新发布 → 用户访问 → 新 SW 安装 + ↓ + 读取 precache-manifest.json + ↓ + 并发预缓存所有资源(6个并发) + ↓ + 预缓存完成 → 通知用户更新 + ↓ + 用户确认 → 激活新 SW → 缓存优先秒开 +``` + +**相关文件**: +- `apps/web/vite.config.ts` - `precacheManifestPlugin()` 生成 manifest +- `apps/web/src/sw/index.ts` - `loadPrecacheManifest()` 和 `precacheStaticFiles()` +- `dist/apps/web/precache-manifest.json` - 构建产物 + ### 编辑器插件系统 ``` Drawnix (主编辑器) diff --git a/packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx b/packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx index e62d9042..66d7fdca 100644 --- a/packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx +++ b/packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx @@ -4,6 +4,7 @@ import { DialogType, useDrawnix } from '../../hooks/use-drawnix'; import MarkdownToDrawnix from './markdown-to-drawnix'; import AIImageGeneration from './ai-image-generation'; import AIVideoGeneration from './ai-video-generation'; +import type { ReferenceImage } from './shared/ReferenceImageUpload'; import { useI18n } from '../../i18n'; import { useBoard } from '@plait-board/react-board'; import React, { useState, useEffect, useRef, memo, useCallback, lazy, Suspense } from 'react'; @@ -83,7 +84,7 @@ const TTDDialogComponent = ({ container }: { container: HTMLElement | null }) => // AI 图片生成的初始数据 const [aiImageData, setAiImageData] = useState<{ initialPrompt: string; - initialImages: (File | { url: string; name: string })[]; + initialImages: ReferenceImage[]; selectedElementIds: string[]; // 保存选中元素的IDs initialResultUrl?: string; // 初始结果URL,用于显示预览 }>({ @@ -224,7 +225,7 @@ const TTDDialogComponent = ({ container }: { container: HTMLElement | null }) => const processedContent = await processSelectedContentForAI(board, selectedElementIds); // 准备图片列表 - const imageItems: (File | { url: string; name: string })[] = []; + const imageItems: ReferenceImage[] = []; // 1. 先添加剩余的图片(非重叠的图片) processedContent.remainingImages.forEach(image => {