397 lines
11 KiB
Markdown
397 lines
11 KiB
Markdown
# 多 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. 配置自有域名(可选)
|
||
|
||
#### 方案 A:Cloudflare 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 → 缓存 → 返回
|
||
```
|
||
|
||
### 场景 2:unpkg 不可用
|
||
|
||
```
|
||
用户 → 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 部署详情
|