CDN 部署
Vistack 的前端(web-client / web-admin)是 Vite 构建的纯静态产物,可以部署到任意 CDN / 静态托管,与后端完全解耦。事实上在线 Demo https://vistack.pages.dev 就是 Cloudflare Pages(全球边缘 CDN) 部署的。
为什么支持 CDN?
| 能力 | 实现 | CDN 友好度 |
|---|---|---|
| 静态产物 | Vite 构建输出 build/web-client / build/web-admin,无服务端渲染依赖 | ✅ 任意 CDN 可直接托管 |
| API 地址 | 编译时注入 VITE_API_BASE(默认相对路径 /api) | ✅ 相对路径可同域反代,无需知道后端域名 |
| 媒体地址 | [minio].public_endpoint 独立配置,预签名 URL 基于它生成 | ✅ 可指向对象存储 CDN |
| 鉴权 | JWT 位于请求头(Authorization: Bearer),无 Cookie 会话 | ✅ CDN 透传即可 |
| 直播 | WebRTC 直连 SFU,不经前端 CDN | ✅ 不受影响 |
前置准备
1. 构建前端产物
cd web
pnpm install
pnpm run build
# 产物:build/web-client(用户端)、build/web-admin(管理端)2. 确定 API 地址(VITE_API_BASE)
前端通过 web/ui/src/api/axios.ts 读取 VITE_API_BASE(默认 /api):
export const baseURL = (import.meta as any).env.VITE_API_BASE ?? '/api'| 部署形态 | VITE_API_BASE | 说明 |
|---|---|---|
| 同域反代 | /api(默认,无需配置) | CDN 域名上把 /api 反代到后端 |
| 前后端分离 | https://api.example.com/api/v1 | 绝对地址,需后端开启 CORS |
修改方式(任选):
# 方式一:构建时注入
VITE_API_BASE=https://api.example.com/api/v1 pnpm run build
# 方式二:写进 .env 文件(如 web/.env.production)
echo "VITE_API_BASE=https://api.example.com/api/v1" > web/.env.production
pnpm run build3. 后端配置(CORS + MinIO 公网地址)
CORS:前后端不同域时,conf/app.toml 的 [cors].allow_origins 需加入前端 CDN 域名:
[cors]
enable = true
allow_origins = ["https://static.example.com", "https://vistack.pages.dev"]
allow_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
allow_headers = ["Content-Type", "Authorization", "X-Requested-With"]
allow_credentials = trueMinIO 公网地址:播放 / 封面的预签名 URL 基于 [minio].public_endpoint 生成,必须可被浏览器访问:
[minio]
public_endpoint = "https://media.example.com" # 或 MinIO/CDN 公网地址
secure = true形态一:同域反代(推荐)
前端与 API 同域名,CDN 节点直接返回静态文件,/api 路径回源到后端网关:
用户 ──▶ CDN 边缘节点(static.example.com)
├─ /api/* ──▶ 回源后端(Traefik 网关 / Nginx)
├─ /admin/* ──▶ 管理端静态(SPA 回退)
└─ 其余 ──▶ 用户端静态(SPA 回退)Nginx 示例:
server {
listen 443 ssl;
server_name static.example.com;
# 用户端产物
root /var/www/vistack/web-client;
# API 反向代理到后端网关
location /api/ {
proxy_pass http://backend-gateway:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 管理端(独立产物目录)
location /admin {
alias /var/www/vistack/web-admin;
try_files $uri $uri/ /admin/index.html; # SPA 回退
}
# 用户端 SPA 回退
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源长缓存(文件名带 hash,可放心缓存)
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}Vite 构建产物文件名带内容 hash(如
index-DvqF6fSJ.js),更新发布无需手动刷新缓存。
形态二:前后端分离域名
前端 CDN 与 API 分属不同域名,构建时注入 API 绝对地址,后端开 CORS:
static.example.com(CDN 前端) ──▶ api.example.com/api/v1(后端,CORS 允许 static.example.com)Cloudflare Pages 示例
创建项目:Cloudflare Dashboard → Workers & Pages → Create → Pages → 连接 Git 仓库(或上传产物);
构建配置(连接 Git 方式):
- Build command:
cd web && pnpm install && pnpm run build - Build output directory:
build/web-client - Environment variables:
VITE_API_BASE=https://api.example.com/api/v1
- Build command:
SPA 回退:Cloudflare Pages 对未匹配路径默认返回 404,需添加
_redirects文件让前端路由回退到index.html(部署到产物根目录):/* /index.html 200管理端:单独建一个 Pages 项目(输出
build/web-admin,环境变量加VITE_BASE=/admin/),或与用户端合并托管(见形态一 Nginx 的/admin配置)。
形态三:媒体加速(对象存储 CDN)
播放 DASH 分片与封面体积大,可前置 CDN 缓存,降低 MinIO 出口带宽:
[minio]
public_endpoint = "https://media-cdn.example.com" # 对象存储 CDN 域名
secure = true预签名 URL 将指向 CDN 域名;CDN 首次回源 MinIO 后缓存分片。
媒体 CDN 缓存策略(DASH 分片)
签名 URL 与 CDN 的两个关键点
预签名 URL(SigV4)形如:
https://media-cdn.example.com/vistack/dash/42/chunk-00001.m4s?X-Amz-Algorithm=...&X-Amz-Date=...&X-Amz-Signature=...&X-Amz-Expires=3600CDN 缓存它有两个坑:
- 签名包含 Host:SigV4 签名校验包含请求的
Host头。public_endpoint必须是浏览器实际访问的域名(即 CDN 域名),且 CDN 回源 MinIO 时保持 Host 头不变(回源 HOST = public_endpoint 域名),否则 MinIO 校验签名返回403 SignatureDoesNotMatch; - 缓存 key 污染:同一分片每次生成的
X-Amz-Signature/X-Amz-Date都不同,若 CDN 按完整 URL 做缓存 key,同一对象会被缓存成多份,命中率趋近于 0。需要让 CDN 忽略签名相关查询参数。
缓存 key 策略:忽略签名参数
| 平台 | 配置 |
|---|---|
| Cloudflare | Cache Rules → 自定义缓存 key:Query string → Ignore all(或只保留需要的参数),并设置 Edge TTL |
| 阿里云 CDN | 缓存配置 → 过滤参数:打开"忽略 URL 参数"(过滤 ? 后全部参数) |
| 腾讯云 CDN | 缓存配置 → 忽略参数:全忽略 |
| AWS CloudFront | Cache policy → Query strings:None(忽略全部) |
注意
忽略参数仅用于媒体路径(如 /vistack/dash/*、/vistack/covers/*)。若同一 CDN 域名还承载带参业务(如 API),请用路径级规则只对媒体生效。
缓存 TTL 建议
| 对象 | 内容特征 | TTL 建议 | 说明 |
|---|---|---|---|
dash/{id}/chunk-*.m4s | 转码产物,不可变 | 1 年(immutable) | 文件名含视频 ID + 序号,重转码才会变化 |
dash/{id}/init-*.m4s | 转码产物,不可变 | 1 年(immutable) | 同上 |
dash/{id}/manifest.mpd | 档位清单,重转码才变 | 1 天 | 码率档位列表更新后最多延迟 1 天 |
covers/{id}.jpg | 封面,可被用户更换 | 1 天 | 换封面后需主动刷新或等 TTL 过期 |
签名有效期 vs 缓存 TTL
- CDN 命中缓存时不校验签名,签名过期不影响已缓存内容继续被服务;
- 但回源时若签名已过期,MinIO 返回 403 → 前端播放失败。因此:
- 签名有效期(
X-Amz-Expires)应 ≥ CDN TTL,保证 TTL 内回源可成功; - 前端播放器遇到 403 时应重新请求播放地址(重新签名)再试。
- 签名有效期(
一键发布脚本
仓库内置 deploy/cdn-publish.sh,一条命令完成「构建前端 → 合并布局(管理端并入 /admin)→ 生成 SPA 回退规则 → 上传 CDN」:
# 对象存储(S3 兼容:MinIO / OSS / COS)—— 默认
VITE_API_BASE=/api ./deploy/cdn-publish.sh s3
# Cloudflare Pages
PROVIDER=pages CF_PROJECT_NAME=vistack ./deploy/cdn-publish.sh pages
# 仅构建 + 合并布局,不上传(预检产物是否正确)
./deploy/cdn-publish.sh dry工作流程
构建:
web-client(根路径)与web-admin(VITE_BASE=/admin/,资源前缀/admin/assets/*);合并布局:把 web-admin 产物复制到 web-client 产物的
admin/子目录——单个桶 / 单个 Pages 项目同时承载两端;SPA 回退:生成
_redirects(Pages 模式):/admin /admin/index.html 200 /admin/* /admin/index.html 200 /* /index.html 200上传:
s3模式:mc mirror(需先mc alias set local <ENDPOINT> <AK> <SK>)或aws s3 sync;pages模式:wrangler pages deploy(需安装 wrangler)。
环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
VITE_API_BASE | /api | 前端 API 地址(同域反代保持默认;分离域名填绝对地址) |
VITE_BASE | /admin/ | 管理端 base |
CDN_BUCKET | - | s3 模式必填:桶名 |
CF_PROJECT_NAME | - | pages 模式必填:项目名 |
S3/OSS 静态网站
s3 模式上传后,若使用静态网站托管(非 CDN 直接回源),需额外把 Error Document / 404 规则 指向 index.html 实现 SPA 回退(脚本已提示)。
常见问题
Q1:刷新 /video/123 返回 404?
前端是 SPA,CDN 需配置路由回退:所有未匹配路径返回 index.html(Cloudflare Pages 用 _redirects,Nginx 用 try_files)。注意 /api/* 不要回退到 index.html,应反代到后端。
Q2:接口报 CORS 错误?
前后端不同域时,确认:
- 后端
[cors].allow_origins包含前端域名; - 预检(OPTIONS)请求被放行(
allow_methods含 OPTIONS,中间件已处理)。
Q3:视频播放不出来?
预签名 URL 由 [minio].public_endpoint 生成——检查该地址是否公网可达,且与前端同域策略一致(不同域时确认 MinIO 侧 CORS 或改用代理播放)。
Q4:更新前端后用户看到旧版本?
Vite 产物文件名带 hash,/assets/ 长缓存即可自动更新;但 index.html 本身不要长缓存(默认即可),否则新版本入口文件不生效。
Q5:Docker 模式与 CDN 冲突吗?
不冲突。Docker 模式(api 内置托管)适合单机;CDN 模式(独立静态托管)适合多地域加速。两者可并存:api 容器仍提供 API,前端产物单独部署 CDN 并指向同一后端即可。
部署形态对比
| 形态 | 适合场景 | 前端入口 | 需要配置 |
|---|---|---|---|
| Docker 内置托管 | 单机 / 内网 | http://localhost | 无(开箱即用) |
| 同域反代 + CDN | 单一域名、全国加速 | https://static.example.com | 反代规则 + 回退规则 |
| 前后端分离 | 多环境、独立扩展 | https://static.example.com | CORS + VITE_API_BASE |
| 媒体 CDN | 高带宽播放场景 | 同上 | MinIO public_endpoint + CDN 缓存 |
