Files
TrueGrowth/docs/CDN_DEPLOYMENT.md

397 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 多 CDN 智能部署方案
## 概述
本文档描述 Opentu 的多 CDN 智能部署策略,实现:
1. **Service Worker 缓存优先** - 最快加载速度
2. **多 CDN 自动回退** - 高可用性
3. **本地服务器兜底** - 最终保障
4. **零服务器流量成本** - 利用免费 CDN
5. **安全混合部署** - HTML 在自有服务器,静态资源在 CDN
## 安全混合部署(推荐)
### 为什么需要混合部署?
- **HTML 文件**可能包含用户配置、API 密钥等敏感信息
- CDN 是公开的,任何人都可以访问
- 将 HTML 保留在自有服务器,只将静态资源放到 CDN
### 架构
```
用户访问 your-domain.com
自有服务器(完整副本)
├── HTML 文件(安全)
└── 全部静态资源(兜底)
HTML 加载资源(路径指向 CDN
┌──────┼──────┐
↓ ↓ ↓
unpkg jsdelivr 服务器
(优先) (备用) (兜底)
```
### 使用方法
```bash
# 一键部署:构建 + 发布 CDN + 部署服务器
pnpm run deploy --otp=123456
# 预览模式(不实际执行)
pnpm run deploy:dry
# 只发布到 CDN跳过服务器部署
pnpm run deploy:cdn-only --otp=123456
# 只部署到服务器(跳过 npm 发布)
pnpm run deploy:server-only
```
### 服务器配置
`.env` 文件中添加:
```bash
# 服务器信息
DEPLOY_HOST=your-server.com
DEPLOY_USER=username
DEPLOY_PORT=22
DEPLOY_WEB_DIR=/var/www/aitu
# SSH 认证(二选一)
DEPLOY_SSH_KEY=~/.ssh/id_rsa
# 或
DEPLOY_SSH_PASSWORD=your-password
```
### 文件分布
| 文件类型 | CDN | 服务器 | 说明 |
|---------|:---:|:------:|------|
| `*.html` | ❌ | ✅ | 安全:不在 CDN 公开 |
| `sw.js` | ❌ | ✅ | Service Worker 必须同源 |
| `init.json` | ❌ | ✅ | 初始化配置 |
| `assets/*.js` | ✅ | ✅ | CDN 优先,服务器兜底 |
| `assets/*.css` | ✅ | ✅ | CDN 优先,服务器兜底 |
| `icons/*` | ✅ | ✅ | CDN 优先,服务器兜底 |
| `manifest.json` | ✅ | ✅ | CDN 优先,服务器兜底 |
### 加载顺序
1. **Service Worker 缓存** - 最快,离线可用
2. **CDN unpkg** - 优先加载,节约服务器流量
3. **CDN jsdelivr** - unpkg 失败时备用
4. **自有服务器** - 所有 CDN 失败时兜底
## 架构图
```
用户请求
┌─────────────────────────────────────────────────┐
│ Service Worker │
│ ┌─────────────────────────────────────────┐ │
│ │ 1. 检查本地缓存 (Cache Storage) │ │
│ │ ↓ 缓存命中 → 直接返回 │ │
│ │ ↓ 缓存未命中 │ │
│ │ │ │
│ │ 2. 尝试 CDN 1: unpkg.com │ │
│ │ ↓ 成功 → 缓存并返回 │ │
│ │ ↓ 失败 │ │
│ │ │ │
│ │ 3. 尝试 CDN 2: jsdelivr.net │ │
│ │ ↓ 成功 → 缓存并返回 │ │
│ │ ↓ 失败 │ │
│ │ │ │
│ │ 4. 回退本地服务器 │ │
│ │ ↓ 成功 → 缓存并返回 │ │
│ │ ↓ 失败 → 显示离线页面 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
## CDN 源对比
| CDN | 免费额度 | 特点 | 访问地址 |
|-----|----------|------|----------|
| unpkg | 无限 | npm 官方托管,全球节点 | `unpkg.com/aitu-app@{version}/` |
| jsdelivr | 无限 | 国内访问快,有缓存 | `cdn.jsdelivr.net/npm/aitu-app@{version}/` |
| 本地服务器 | 自有流量 | 完全可控,最终兜底 | 自有域名 |
## 部署流程
### 方式 1安全混合部署推荐
```bash
# 步骤 1升级版本
pnpm run version:patch
# 步骤 2构建混合部署包
pnpm run cdn:build
# 步骤 3发布静态资源到 npm
pnpm run cdn:publish --skip-build --otp=123456
# 步骤 4部署 HTML 到自有服务器
scp -r dist/deploy/server/* user@your-server:/path/to/web/
```
**CDN 上没有 HTML 文件**,用户信息安全。
### 方式 2完整发布到 npm包含 HTML
```bash
# 升级版本并发布
pnpm run version:patch
pnpm run npm:publish --otp=123456
```
发布后CDN 自动可用:
- unpkg: `https://unpkg.com/aitu-app@{version}/index.html`
- jsdelivr: `https://cdn.jsdelivr.net/npm/aitu-app@{version}/index.html`
> ⚠️ 注意:此方式 HTML 文件也在 CDN 上公开
### 2. 配置自有域名(可选)
#### 方案 ACloudflare Pages推荐
1. 登录 [Cloudflare Pages](https://dash.cloudflare.com/pages)
2. 创建项目,连接 GitHub 仓库
3. 配置构建:
```
Build command: pnpm run build:web
Output directory: dist/apps/web
Environment: NODE_VERSION=20
```
4. 添加自定义域名
#### 方案 B自有服务器 + CDN 代理
1. 部署到自有服务器(使用现有脚本):
```bash
pnpm run deploy:package
pnpm run deploy:upload
```
2. 配置 Cloudflare CDN 代理:
- 将域名 DNS 指向 Cloudflare
- 开启橙色云朵(代理模式)
- 静态资源自动缓存
### 3. Service Worker 配置
项目已内置多 CDN 回退逻辑,关键文件:
- `apps/web/src/sw/cdn-fallback.ts` - CDN 回退策略
- `apps/web/public/cdn-config.js` - CDN 选择器(可选)
## 核心代码说明
### CDN 回退策略 (`cdn-fallback.ts`)
```typescript
// CDN 源配置
const CDN_SOURCES = [
{
name: 'unpkg',
urlTemplate: 'https://unpkg.com/aitu-app@{version}/{path}',
priority: 1,
},
{
name: 'jsdelivr',
urlTemplate: 'https://cdn.jsdelivr.net/npm/aitu-app@{version}/{path}',
priority: 2,
},
];
// 智能回退逻辑
async function fetchFromCDNWithFallback(resourcePath, version, localOrigin) {
// 1. 尝试所有 CDN
for (const cdn of getAvailableCDNs()) {
const response = await tryFetch(cdn, resourcePath, version);
if (response.ok) return response;
}
// 2. 回退本地服务器
return fetch(localOrigin + resourcePath);
}
```
### 健康检测机制
- 自动检测 CDN 可用性
- 失败超过 3 次自动降级
- 1 分钟后自动恢复尝试
```typescript
// 标记 CDN 失败
markCDNFailure(cdnName);
// 检查 CDN 是否可用
if (isCDNAvailable(cdnName)) {
// 尝试使用
}
```
## 缓存策略
| 资源类型 | 策略 | Cache-Control |
|----------|------|---------------|
| HTML | Network First | max-age=0, must-revalidate |
| JS/CSS | Cache First | max-age=31536000, immutable |
| 图片 | Cache First | max-age=31536000, immutable |
| Service Worker | Network First | max-age=0, must-revalidate |
| version.json | Network First | max-age=0, must-revalidate |
## 使用场景
### 场景 1正常访问CDN 可用)
```
用户 → SW 缓存(命中)→ 立即返回
用户 → SW 缓存(未命中)→ unpkg → 缓存 → 返回
```
### 场景 2unpkg 不可用
```
用户 → SW 缓存(未命中)→ unpkg失败→ jsdelivr → 缓存 → 返回
```
### 场景 3所有 CDN 不可用
```
用户 → SW 缓存(未命中)→ unpkg失败→ jsdelivr失败→ 本地服务器 → 返回
```
### 场景 4完全离线
```
用户 → SW 缓存(命中)→ 返回(离线可用)
```
## 调试命令
```javascript
// 在浏览器控制台执行
// 查看 CDN 状态
__OPENTU_CDN_API__.sources
// 强制重新选择 CDN
__OPENTU_CDN_API__.reselectCDN()
// 清除 CDN 缓存
__OPENTU_CDN_API__.clearCDNCache()
> 兼容说明:运行时仍保留 `__AITU_CDN__` / `__AITU_CDN_API__` 旧别名读取能力,用于兼容历史缓存与调试入口,但新代码应统一使用 `__OPENTU_CDN__` / `__OPENTU_CDN_API__`。
```
## 监控与告警
建议接入监控:
1. **CDN 可用性监控**:定期检测各 CDN 节点
2. **加载性能监控**:记录资源加载时间
3. **错误率监控**:统计各源失败率
可通过 PostHog 自定义事件实现:
```typescript
// 记录 CDN 加载事件
posthog.capture('cdn_load', {
source: 'unpkg',
resource: 'index.js',
latency: 150,
success: true,
});
```
## 成本对比
| 方案 | 月流量成本 | 可用性 | 速度 |
|------|-----------|--------|------|
| 纯自有服务器 | ~$50-200/100GB | 99.9% | 一般 |
| Cloudflare Pages | $0 | 99.99% | 快 |
| npm CDN 回退 | $0 | 99.999% | 快 |
| 本方案(组合) | ~$0 | 99.999%+ | 最快 |
## 开发模式
本地开发时(`localhost` / `127.0.0.1`CDN 逻辑会自动跳过:
### 自动跳过的逻辑
| 组件 | 开发模式行为 |
|------|-------------|
| `cdn-config.js` | 跳过 CDN 检测,直接设置 `cdn: 'local'` |
| `cdn-fallback.ts` | `fetchFromCDNWithFallback()` 直接返回 `null` |
| Service Worker | 跳过 CDN 回退,直接使用本地服务器 |
### 开发模式判断
```javascript
// 以下情况被识别为开发模式:
const isDevelopment =
location.hostname === 'localhost' ||
location.hostname === '127.0.0.1' ||
location.hostname.endsWith('.localhost');
```
### 开发时的资源加载流程
```
开发模式:
用户请求 → SW 缓存(可选)→ 本地 Vite 服务器 → 返回
↓ 失败
显示错误(不尝试 CDN
```
### 强制测试 CDN 回退
如果需要在本地测试 CDN 回退逻辑,可以:
1. **使用 `ngrok` 或类似工具**:将本地服务暴露为公网域名
2. **修改 hosts 文件**:将测试域名指向 127.0.0.1
3. **构建后预览**`pnpm run build:web && pnpm run preview`preview 模式不被识别为开发模式)
## 常见问题
### Q: CDN 缓存更新延迟怎么办?
A:
- unpkg通常 1-5 分钟
- jsdelivr可能需要手动刷新缓存
- 使用版本号指定(`@0.5.16`)确保获取正确版本
### Q: 如何强制刷新 CDN 缓存?
A:
```bash
# jsdelivr 刷新
curl -X PURGE https://purge.jsdelivr.net/npm/aitu-app@0.5.16/
# unpkg 自动同步,无需手动刷新
```
### Q: 国内访问慢怎么办?
A:
1. jsdelivr 通常国内访问较快
2. 考虑添加国内 CDN 源(如 bootcdn、cdnjs
3. 使用 Cloudflare 中国节点
## 相关文件
- `/scripts/publish-npm.js` - npm 发布脚本
- `/apps/web/src/sw/cdn-fallback.ts` - CDN 回退策略
- `/apps/web/public/cdn-config.js` - CDN 选择器
- `/apps/web/public/_headers` - 缓存头配置
- `/docs/CFPAGE-DEPLOY.md` - Cloudflare Pages 部署
- `/docs/NPM_CDN_DEPLOY.md` - npm CDN 部署详情