chore: enhance CDN response validation and update version to 0.5.25
This commit is contained in:
1 parent
8bd7f30c19
commit
155ec00987
5 files changed
+129
-10
No files matched your search
@@ -2795,6 +2795,78 @@ if (cachedResponse) {
|
||||
- 在返回前验证 `blob.size > 0` 可以自动修复历史问题
|
||||
- 删除无效缓存后重新获取,确保用户看到正确的内容
|
||||
|
||||
### CDN 响应必须多重验证后才能缓存
|
||||
|
||||
**场景**: Service Worker 从 CDN 获取静态资源并缓存时
|
||||
|
||||
❌ **错误示例**:
|
||||
```typescript
|
||||
// 错误:只检查 response.ok,可能缓存 CDN 返回的 HTML 错误页面
|
||||
const response = await fetch(cdnUrl);
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
return response; // 可能是 404 页面被当作 JS 执行!
|
||||
}
|
||||
```
|
||||
|
||||
✅ **正确示例**:
|
||||
```typescript
|
||||
const response = await fetch(cdnUrl);
|
||||
if (response.ok) {
|
||||
// 1. Content-Type 验证
|
||||
const contentType = response.headers.get('Content-Type') || '';
|
||||
const isValidType = contentType.includes('javascript') ||
|
||||
contentType.includes('css') ||
|
||||
contentType.includes('json');
|
||||
if (!isValidType) continue; // 尝试下一个源
|
||||
|
||||
// 2. Content-Length 验证(排除空响应)
|
||||
const length = parseInt(response.headers.get('Content-Length') || '0', 10);
|
||||
if (length > 0 && length < 50) continue;
|
||||
|
||||
// 3. 内容采样验证(检测 HTML 错误页面)
|
||||
const sample = await response.clone().text().then(t => t.slice(0, 200));
|
||||
if (sample.includes('<!DOCTYPE') || sample.includes('Not Found')) {
|
||||
continue; // CDN 返回了 HTML 错误页面
|
||||
}
|
||||
|
||||
cache.put(request, response.clone());
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- CDN 可能返回 404 但 HTTP 状态码仍是 200(某些 CDN 的行为)
|
||||
- npm 包不存在时,CDN 返回 HTML 错误页面
|
||||
- 错误页面被当作 JS 执行会导致 React 多实例冲突,应用崩溃
|
||||
- 多重验证确保只缓存真正有效的资源
|
||||
|
||||
### CDN 请求应设置短超时快速回退
|
||||
|
||||
**场景**: Service Worker 实现 CDN 优先加载策略时
|
||||
|
||||
❌ **错误示例**:
|
||||
```typescript
|
||||
// 错误:超时太长,CDN 回源慢时用户等待时间过长
|
||||
const CDN_CONFIG = {
|
||||
fetchTimeout: 10000, // 10 秒超时
|
||||
};
|
||||
```
|
||||
|
||||
✅ **正确示例**:
|
||||
```typescript
|
||||
// 正确:短超时,CDN 缓存命中很快(<200ms),超时说明在回源
|
||||
const CDN_CONFIG = {
|
||||
fetchTimeout: 1500, // 1.5 秒超时,快速回退到服务器
|
||||
};
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- CDN 缓存命中通常 < 200ms,1.5s 足够
|
||||
- CDN 回源(首次请求)可能需要 3-5 秒,等待太久影响用户体验
|
||||
- 短超时后快速回退到服务器,保证首次加载速度
|
||||
- 用户请求会触发 CDN 缓存,后续访问自动加速
|
||||
|
||||
### 图像处理工具复用规范
|
||||
|
||||
**场景**: 需要对图片进行边框检测、去白边、裁剪等处理时
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@
|
||||
</script>
|
||||
<!-- base href 由构建工具处理,CDN 模式下使用相对路径 -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<meta name="app-version" content="0.5.24" />
|
||||
<meta name="app-version" content="0.5.25" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" sizes="16x16" href="./icons/favicon-16x16.svg" />
|
||||
<link rel="icon" type="image/svg+xml" sizes="32x32" href="./icons/favicon-32x32.svg" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "0.5.25",
|
||||
"buildTime": "2026-01-20T05:11:17.776Z",
|
||||
"buildTime": "2026-01-20T05:11:18.087Z",
|
||||
"gitCommit": "unknown",
|
||||
"changelog": []
|
||||
}
|
||||
@@ -234,9 +234,13 @@ export async function fetchFromCDNWithFallback(
|
||||
const response = await fetchWithTimeout(url);
|
||||
|
||||
if (response.ok) {
|
||||
// 验证响应类型,防止把 404 页面当作 JS 执行
|
||||
// ============================================
|
||||
// 多重验证:确保 CDN 返回的是有效资源
|
||||
// ============================================
|
||||
|
||||
// 1. Content-Type 验证
|
||||
const contentType = response.headers.get('Content-Type') || '';
|
||||
const isValidResponse =
|
||||
const isValidContentType =
|
||||
contentType.includes('javascript') ||
|
||||
contentType.includes('css') ||
|
||||
contentType.includes('json') ||
|
||||
@@ -245,10 +249,38 @@ export async function fetchFromCDNWithFallback(
|
||||
contentType.includes('woff') ||
|
||||
contentType.includes('application/octet-stream');
|
||||
|
||||
if (!isValidResponse) {
|
||||
console.warn(`[CDN Fallback] ${cdn.name} returned invalid Content-Type: ${contentType}`);
|
||||
if (!isValidContentType) {
|
||||
console.warn(`[CDN Fallback] ${cdn.name} invalid Content-Type: ${contentType}`);
|
||||
markCDNFailure(cdn.name);
|
||||
continue; // 尝试下一个 CDN
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Content-Length 验证(JS/CSS 文件不应该太小)
|
||||
const contentLength = parseInt(response.headers.get('Content-Length') || '0', 10);
|
||||
const isTextResource = contentType.includes('javascript') || contentType.includes('css') || contentType.includes('json');
|
||||
if (isTextResource && contentLength > 0 && contentLength < 50) {
|
||||
console.warn(`[CDN Fallback] ${cdn.name} response too small: ${contentLength} bytes`);
|
||||
markCDNFailure(cdn.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 内容采样验证(检测 HTML 错误页面)
|
||||
const clonedResponse = response.clone();
|
||||
try {
|
||||
const textSample = await clonedResponse.text().then(t => t.slice(0, 200));
|
||||
const looksLikeHtml = textSample.includes('<!DOCTYPE') ||
|
||||
textSample.includes('<html') ||
|
||||
textSample.includes('<HTML') ||
|
||||
textSample.includes('Not Found') ||
|
||||
textSample.includes('404');
|
||||
|
||||
if (isTextResource && looksLikeHtml) {
|
||||
console.warn(`[CDN Fallback] ${cdn.name} returned HTML instead of ${contentType}`);
|
||||
markCDNFailure(cdn.name);
|
||||
continue;
|
||||
}
|
||||
} catch (sampleError) {
|
||||
// 采样失败不阻止使用(可能是二进制文件)
|
||||
}
|
||||
|
||||
markCDNSuccess(cdn.name);
|
||||
|
||||
@@ -16,17 +16,27 @@ export const VersionUpdatePrompt: React.FC = () => {
|
||||
// Listen for custom event from main.tsx
|
||||
const handleUpdateAvailable = async (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
// console.log('[VersionUpdatePrompt] Update available:', customEvent.detail);
|
||||
|
||||
const version = customEvent.detail?.version;
|
||||
const newVersion = customEvent.detail?.version;
|
||||
|
||||
// 获取当前运行的版本(从 HTML meta 标签)
|
||||
const currentVersionMeta = document.querySelector('meta[name="app-version"]');
|
||||
const currentVersion = currentVersionMeta?.getAttribute('content');
|
||||
|
||||
try {
|
||||
// Fetch detailed version info (changelog)
|
||||
const res = await fetch(`/version.json?t=${Date.now()}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
|
||||
// 如果当前版本已经是最新版本,不显示更新提示
|
||||
if (currentVersion && data.version === currentVersion) {
|
||||
// console.log('[VersionUpdatePrompt] Already on latest version, skipping prompt');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use fetched data if versions match or if event didn't specify version
|
||||
if (!version || data.version === version) {
|
||||
if (!newVersion || data.version === newVersion) {
|
||||
setUpdateAvailable(data);
|
||||
return;
|
||||
}
|
||||
@@ -35,6 +45,11 @@ export const VersionUpdatePrompt: React.FC = () => {
|
||||
console.warn('Failed to fetch version.json:', error);
|
||||
}
|
||||
|
||||
// 如果无法获取 version.json,但事件带了版本号,检查是否相同
|
||||
if (currentVersion && newVersion && currentVersion === newVersion) {
|
||||
return; // 已经是最新版本
|
||||
}
|
||||
|
||||
// Fallback to event detail
|
||||
setUpdateAvailable(customEvent.detail);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user