外观
前端 CDN 技术学习文档
更新日期:2026-07-02
适用范围:Vue 3、Nuxt 4、Vite、Webpack 5、Vue CLI 存量项目、静态站点、SSR/SSG 前端项目。
目标:理解前端 CDN 的工程价值,并能在真实项目里完成资源拆分、构建配置、上传发布、缓存控制、回滚和排障。
1. 先建立正确的 CDN 工程模型
CDN(Content Delivery Network,内容分发网络)在前端项目里不是“把文件放到一个更快的服务器”这么简单。它真正解决的是:把可以公开缓存的静态资源放到距离用户更近的边缘节点,并通过构建产物 hash、HTTP 缓存头、发布版本目录、资源路径重写和回源策略,让用户稳定拿到正确版本的 JS、CSS、图片、字体、视频等资源。
一个现代前端项目通常会有这些资源:
| 资源类型 | 常见来源 | 是否适合 CDN | 推荐缓存策略 | 说明 |
|---|---|---|---|---|
| HTML 入口 | index.html、SSR 返回页面 | 谨慎 | no-cache 或短缓存 | HTML 负责引用当前版本资源,不能长期缓存,否则容易引用到旧 chunk |
| 入口 JS/CSS | Vite/Webpack 生成 | 适合 | max-age=31536000, immutable | 文件名必须包含内容 hash |
| 异步 chunk | 路由懒加载、动态 import | 适合 | max-age=31536000, immutable | 必须保留旧版本一段时间,避免旧 HTML 加载不到旧 chunk |
| 图片/字体/音视频 | src/assets、CSS url、组件 import | 适合 | hash 文件长期缓存 | 大图、字体、媒体文件是 CDN 收益最高的资源 |
public/ 目录资源 | favicon、robots、固定下载文件 | 视情况 | 有版本名则长期缓存;无版本名则短缓存 | public/logo.png 这类不带 hash 的文件不能随意长期缓存 |
| sourcemap | *.map | 通常不公开 | 私有或不上 CDN | 可上传到 Sentry 等平台,不建议公开暴露源码 |
| Service Worker | sw.js、workbox-*.js | 谨慎 | no-cache | SW 更新本身不能被长缓存挡住 |
| PWA manifest | manifest.webmanifest | 谨慎 | 短缓存 | 应跟 HTML 一样可及时更新 |
| API 响应 | /api/** | 看业务 | 公共 GET 可缓存;私有数据不缓存 | 不要把用户隐私数据误缓存到边缘 |
| 第三方库 | Vue、ECharts、Monaco 等 | 可选 | 锁版本 + SRI | 只有体积大、变动少、跨项目复用强时才值得 external 到 CDN |
前端 CDN 落地的核心公式是:
text
稳定入口 HTML + 带 hash 的静态资源 + 正确 publicPath/base + 可回滚版本目录 + 可验证缓存头如果只配置了 CDN 域名,却没有 hash、缓存头、旧版本保留和发布流程,线上迟早会遇到“白屏”“chunk 404”“用户拿到旧代码”“刷新才正常”等问题。
2. 前端 CDN 的几种常见落地方式
2.1 静态资源 CDN
这是最常见、最推荐的方式。HTML 仍由业务域名或 SSR 服务返回,JS/CSS/图片/字体等静态资源从 CDN 域名加载。
text
https://www.example.com/ -> 返回 HTML
https://cdn.example.com/app/20260702/ -> JS/CSS/chunk/assets
https://cdn.example.com/static/ -> 图片、字体、下载文件适合:
- Vue SPA
- Vite 构建项目
- Webpack 构建项目
- Nuxt SSG 站点
- Nuxt SSR 但静态构建资源独立上 CDN
2.2 整站 CDN
HTML、静态资源、API 网关都经过 CDN。Cloudflare Pages、Netlify、Vercel、CloudFront + S3 都属于常见方案。
适合:
- 静态站点
- 文档站
- 营销页
- SSG 博客
- 可被边缘缓存的 SSR 页面
注意:
- HTML 不能用和 hash 资源一样的长期缓存策略。
- 登录态页面、用户私有数据页面要谨慎边缘缓存。
- SSR 项目需要区分页面缓存、接口缓存和静态资源缓存。
2.3 第三方依赖 CDN external
把 vue、vue-router、echarts、monaco-editor 等依赖不打进 bundle,而是在 HTML 里通过 CDN script 或 import map 加载。
适合:
- 后台管理系统里体积很大的低频库,例如 ECharts、Monaco、地图 SDK
- 多个微前端共享同一套大依赖
- 老项目需要快速减小首包
不适合:
- 小项目无脑 external Vue/React
- 需要强 tree-shaking 的项目
- 网络环境不可控、内网或离线场景
- 对 CSP/SRI/供应链安全要求很高但没有配套治理的项目
工程上更稳的建议是:优先把自己项目的 hash 静态资源放 CDN;第三方依赖 external 是进阶优化,不是默认动作。
3. CDN 域名、路径和版本目录设计
推荐使用独立静态资源域名:
text
业务域名:https://www.example.com
静态域名:https://cdn.example.com
版本路径:https://cdn.example.com/frontend/shop-web/20260702-abc123/3.1 为什么推荐版本目录
内容 hash 文件名可以解决单个文件缓存问题,版本目录可以解决发布和回滚问题。
text
cdn.example.com/shop-web/20260701-a1b2c3/assets/index.8a7fd9.js
cdn.example.com/shop-web/20260702-d4e5f6/assets/index.19b0aa.js这样发布新版本时不需要删除旧资源,只需要让 HTML 指向新版本目录。一旦回滚,只要 HTML 指回旧版本目录即可。
3.2 旧版本要保留多久
建议保留至少 7 到 30 天,取决于你的 HTML 缓存时间、用户会话长度和发布频率。
原因:
- 用户打开页面时拿到旧 HTML,过一会儿路由懒加载触发旧 chunk。
- 如果旧 chunk 已被清理,用户会遇到
Loading chunk failed或白屏。 - 移动端 WebView、企业内网代理、浏览器缓存都可能延迟更新 HTML。
3.3 文件命名原则
推荐:
text
assets/js/[name].[hash].js
assets/css/[name].[hash].css
assets/img/[name].[hash][extname]
assets/fonts/[name].[hash][extname]不推荐:
text
assets/index.js
assets/app.css
logo.png没有 hash 的文件不能放心长期缓存。
4. HTTP 缓存策略
4.1 hash 静态资源
http
Cache-Control: public, max-age=31536000, immutable适用于:
index.8a7fd9.jsvendor.4c22ab.csslogo.3d8f01.svgfont.6aa912.woff2
immutable 的含义是:资源 URL 不变时,浏览器可以认为内容不会变。前提是文件名确实带内容 hash,且发布后不覆盖同名文件。
4.2 HTML、manifest、Service Worker
http
Cache-Control: no-cache或:
http
Cache-Control: public, max-age=0, must-revalidate适用于:
index.html- SSR 页面 HTML
manifest.webmanifestsw.js- 运行时配置文件,例如
/config/runtime.json
4.3 public 目录里的无 hash 文件
例如:
text
public/favicon.ico
public/robots.txt
public/logo.png
public/download/template.xlsx这些文件如果 URL 不带版本号,不建议长期缓存。可以使用:
http
Cache-Control: public, max-age=300如果一定要长期缓存,路径或文件名要带版本:
text
/downloads/template-v20260702.xlsx
/brand/logo-v3.png4.4 CDN 和浏览器缓存的分层
很多 CDN 支持分别设置:
- 浏览器缓存 TTL:浏览器保存多久。
- 边缘缓存 TTL:CDN 节点保存多久。
- 回源缓存策略:CDN 没命中时如何向源站请求。
常见做法:
text
hash 资源:浏览器 1 年,边缘 1 年,不覆盖同名文件
HTML:浏览器 no-cache,边缘短缓存或不缓存
API 公共 GET:按业务 TTL 缓存,必须区分登录态4.5 缓存是否真的生效:验证方法
配置完不等于生效。上线前后都应该用响应头验证:
bash
# 查看响应头,重点看 Cache-Control、Age、缓存状态头
curl -I https://cdn.example.com/frontend/shop-web/20260702-a1b2c3/assets/js/index.abc123.js重点字段:
| 响应头 | 含义 | 期望值 |
|---|---|---|
Cache-Control | 浏览器与中间缓存的缓存指令 | hash 资源为 public, max-age=31536000, immutable;HTML 为 no-cache 或短缓存 |
Age | 该资源在当前缓存中已存在多少秒 | 数值持续增大说明命中了缓存 |
ETag / Last-Modified | 协商缓存校验器 | 配合 no-cache 使用,允许缓存但每次验证 |
CF-Cache-Status | Cloudflare 缓存状态 | HIT 表示命中;MISS、DYNAMIC 表示未命中 |
X-Cache | 部分 CDN / S3+CloudFront 使用 | Hit from cloudfront / Miss from cloudfront |
浏览器侧验证:
- Network 面板查看
Size列:显示disk cache/memory cache表示没发起网络请求。 - 勾选
Disable cache再刷新,确认资源本身可访问、响应头正确。 - 对比
Age和Date,判断用户拿到的是不是陈旧副本。
no-cache 与 no-store 的区别经常被搞混:
| 指令 | 行为 |
|---|---|
no-store | 完全不缓存,每次重新下载 |
no-cache | 可以缓存,但每次使用前必须向服务器验证(配合 ETag 可返回 304,节省流量) |
max-age=0, must-revalidate | 缓存立即过期,使用前必须验证,语义接近 no-cache |
💡 对 HTML 用
no-cache而不是no-store,可以在内容没变时通过 304 复用缓存,兼顾“及时更新”和“省流量”。
5. Vite + Vue 3 项目落地
5.1 目录示例
text
shop-web/
index.html
src/
main.ts
router.ts
assets/
logo.svg
fonts/
brand.woff2
public/
favicon.ico
robots.txt
vite.config.ts
.env.productionsrc/assets 中通过 import、CSS url()、Vue 模板引用的资源,会进入构建流程并通常带 hash。public 中的资源会原样复制,通常不自动加 hash。
5.2 环境变量
ini
# .env.production
VITE_CDN_BASE=https://cdn.example.com/frontend/shop-web/20260702-a1b2c3/
VITE_ROUTER_BASE=/注意:VITE_ 前缀变量会暴露到浏览器端,不要放密钥。
5.3 Vite 配置 CDN base
当前 Vite 文档中的构建底层正在向 Rolldown 迁移。较新项目可以使用 rolldownOptions,存量 Vite 5/6/7 项目通常仍能看到 rollupOptions。下面示例以较新的 rolldownOptions 写法为主,如果你的项目还在旧版 Vite,可把 rolldownOptions 换成 rollupOptions。
ts
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const cdnBase = env.VITE_CDN_BASE || '/'
return {
plugins: [vue()],
base: cdnBase,
build: {
outDir: 'dist',
assetsDir: 'assets',
manifest: true,
sourcemap: 'hidden',
cssCodeSplit: true,
assetsInlineLimit: 4096,
rolldownOptions: {
output: {
entryFileNames: 'assets/js/[name].[hash].js',
chunkFileNames: 'assets/js/[name].[hash].js',
assetFileNames: (assetInfo) => {
const name = assetInfo.names?.[0] || assetInfo.name || ''
if (/\.(css)$/.test(name)) return 'assets/css/[name].[hash][extname]'
if (/\.(png|jpe?g|gif|svg|webp|avif)$/.test(name)) return 'assets/img/[name].[hash][extname]'
if (/\.(woff2?|ttf|otf|eot)$/.test(name)) return 'assets/fonts/[name].[hash][extname]'
return 'assets/[name].[hash][extname]'
},
advancedChunks: {
groups: [
{ name: 'vue', test: /[\\/]node_modules[\\/](vue|vue-router)[\\/]/ },
],
},
},
},
},
}
})构建后,Vite 会把 HTML、JS、CSS 中能识别的静态资源引用改成 base 下的 URL,例如:
html
<script type="module" crossorigin src="https://cdn.example.com/frontend/shop-web/20260702-a1b2c3/assets/js/index.abc123.js"></script>
<link rel="stylesheet" crossorigin href="https://cdn.example.com/frontend/shop-web/20260702-a1b2c3/assets/css/index.def456.css">5.4 Vue Router 不要误用 CDN base
很多 Vite + Vue 项目会这样写:
ts
createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})如果 base 是 CDN 完整 URL,这会把路由基础路径也变成 CDN 域名,明显不对。建议把资源 base 和路由 base 分开:
ts
// src/router.ts
import { createRouter, createWebHistory } from 'vue-router'
export const router = createRouter({
history: createWebHistory(import.meta.env.VITE_ROUTER_BASE || '/'),
routes: [
{ path: '/', component: () => import('./pages/Home.vue') },
{ path: '/orders', component: () => import('./pages/Orders.vue') },
],
})5.5 public/ 资源的处理
Vite 的 public 目录适合放不会被源码 import 的文件,例如 favicon.ico、robots.txt。但这些文件通常不会被内容 hash 命名。
在 Vue 组件里引用 public 文件时,不要硬编码 CDN 域名:
vue
<script setup lang="ts">
const base = import.meta.env.BASE_URL
</script>
<template>
<img :src="`${base}brand/logo-v3.png`" alt="Brand logo">
</template>如果 brand/logo-v3.png 会变化,文件名要带版本;否则浏览器和 CDN 可能继续使用旧文件。
5.6 上传 CDN 的发布脚本
示例:把构建产物上传到对象存储或 CDN 源站的版本目录。
bash
npm ci
npm run build
# 示例:上传 hash 静态资源,真实项目替换为你的 S3/OSS/COS/R2 命令
aws s3 sync dist/ s3://my-cdn-bucket/frontend/shop-web/20260702-a1b2c3/ \
--cache-control "public,max-age=31536000,immutable" \
--exclude "index.html" \
--exclude "manifest.webmanifest" \
--exclude "sw.js"
# HTML 单独上传到业务源站或短缓存位置
aws s3 cp dist/index.html s3://my-origin-bucket/shop-web/index.html \
--cache-control "public,max-age=0,must-revalidate"真实生产里建议分两步:
- 先上传新版本静态资源到 CDN 版本目录。
- 再发布 HTML 或 SSR 服务,使它引用新版本资源。
这样可以避免 HTML 已上线但 CDN 资源还没上传完成导致白屏。
6. Vite 中 external 第三方库到 CDN
6.1 什么时候 external
建议 external 的典型对象:
echartsmonaco-editor- 地图 SDK
- PDF/Excel/图表等大体积低频依赖
- 多个微前端共享且版本固定的基础库
不建议一开始就 external:
vuevue-routerpinia- 频繁升级的业务组件库
因为 external 会牺牲打包器的 tree-shaking、版本一致性和离线可控性。
6.2 ESM CDN + import map 方案
index.html:
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<script type="importmap">
{
"imports": {
"vue": "https://esm.sh/vue@3.5.17",
"vue-router": "https://esm.sh/vue-router@4.5.1"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>vite.config.ts:
ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rolldownOptions: {
external: ['vue', 'vue-router'],
},
},
})优点:
- 浏览器原生 ESM,语义清楚。
- Vite 输出的裸模块导入由 import map 解析。
注意:
- 必须锁定具体版本,不要使用
latest。 - 要评估目标浏览器对 import maps 的支持。
- CDN 供应链风险要纳入安全治理。
6.3 UMD 全局变量方案
index.html:
html
<script
src="https://cdn.jsdelivr.net/npm/vue@3.5.17/dist/vue.global.prod.js"
integrity="sha384-替换为真实hash"
crossorigin="anonymous">
</script>Vite 对应用模式不是天然偏向 UMD global external。这个方案在 Webpack 里更常见。如果 Vite 项目要大量使用 global external,建议谨慎评估插件维护质量和构建结果,不要只为了减少几百 KB 引入额外不确定性。
7. Webpack 5 项目落地
7.1 典型目录
text
legacy-admin/
public/
index.html
favicon.ico
src/
main.ts
assets/
logo.png
fonts/
admin.woff2
webpack.prod.js7.2 output.publicPath 配置 CDN
js
// webpack.prod.js
const path = require('node:path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const cdnBase = process.env.CDN_BASE || '/'
module.exports = {
mode: 'production',
entry: {
app: path.resolve(__dirname, 'src/main.ts'),
},
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: cdnBase,
filename: 'assets/js/[name].[contenthash:8].js',
chunkFilename: 'assets/js/[name].[contenthash:8].js',
assetModuleFilename: 'assets/media/[name].[contenthash:8][ext][query]',
clean: true,
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.(png|jpe?g|gif|svg|webp|avif)$/i,
type: 'asset/resource',
generator: {
filename: 'assets/img/[name].[contenthash:8][ext]',
},
},
{
test: /\.(woff2?|ttf|otf|eot)$/i,
type: 'asset/resource',
generator: {
filename: 'assets/fonts/[name].[contenthash:8][ext]',
},
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'assets/css/[name].[contenthash:8].css',
chunkFilename: 'assets/css/[name].[contenthash:8].css',
}),
new HtmlWebpackPlugin({
template: './public/index.html',
inject: 'body',
}),
],
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10,
},
},
},
},
}publicPath 会影响:
- 入口 JS URL
- 异步 chunk 加载路径
- CSS 中抽取出来的资源路径
- asset modules 生成的图片、字体、媒体路径
- Web Worker、WASM 等由 Webpack runtime 加载的资源路径
7.3 Webpack 动态 publicPath
如果 CDN 地址要运行时决定,可以在入口最前面设置:
ts
// src/public-path.ts
declare let __webpack_public_path__: string
__webpack_public_path__ =
window.__ASSET_BASE_URL__ || 'https://cdn.example.com/frontend/admin/latest/'入口文件必须先 import 它:
ts
// src/main.ts
import './public-path'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')HTML:
html
<script>
window.__ASSET_BASE_URL__ = 'https://cdn.example.com/frontend/admin/20260702-a1b2c3/'
</script>注意:__webpack_public_path__ 必须在任何异步 import、CSS、资源加载之前设置。
7.4 Webpack external Vue 到 CDN
HTML 模板:
html
<!-- public/index.html -->
<div id="app"></div>
<script
src="https://cdn.jsdelivr.net/npm/vue@3.5.17/dist/vue.global.prod.js"
integrity="sha384-替换为真实hash"
crossorigin="anonymous">
</script>Webpack 配置:
js
module.exports = {
externals: {
vue: 'Vue',
},
}业务代码仍然可以写:
ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')Webpack 会把 vue 模块请求转成使用全局变量 Vue。这种方式适合存量后台项目和多项目共享大依赖,但要配合:
- 固定版本号
- SRI
crossorigin="anonymous"- CDN 降级方案
- 监控 script 加载失败
7.5 Webpack public 资源
Webpack 本身不等同于 Vite 的 public 目录。通常用 HtmlWebpackPlugin 处理 HTML,用 CopyWebpackPlugin 复制静态文件。
js
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'public/favicon.ico', to: 'favicon.ico' },
{ from: 'public/robots.txt', to: 'robots.txt' },
{ from: 'public/downloads', to: 'downloads' },
],
}),
]复制出来且不带 hash 的文件不要设置一年缓存,除非文件名自带版本。
8. Nuxt 4 项目落地
Nuxt 4 默认把 Vue 应用代码放在 app/ 目录,server/、public/、shared/、nuxt.config.ts 仍在项目根目录。
text
nuxt-shop/
app/
app.vue
pages/
components/
server/
api/
public/
favicon.ico
nuxt.config.ts8.1 app.cdnURL 与 buildAssetsDir
Nuxt 提供 app.cdnURL 用于生产环境从 CDN 服务 public 文件夹内容;app.buildAssetsDir 是构建资源目录,默认通常是 /_nuxt/。
ts
// nuxt.config.ts
export default defineNuxtConfig({
app: {
baseURL: '/',
buildAssetsDir: '/_nuxt/',
cdnURL: process.env.NUXT_APP_CDN_URL || '',
},
})环境变量:
ini
NUXT_APP_CDN_URL=https://cdn.example.com/frontend/nuxt-shop/20260702-a1b2c3/构建后,Nuxt 注入到 HTML 里的客户端资源会指向 CDN 地址。
8.2 Nuxt SSR + CDN 静态资源
适合需要 SSR SEO 的站点:
text
用户请求 https://www.example.com/products/1
-> SSR 服务返回 HTML
-> HTML 引用 https://cdn.example.com/frontend/nuxt-shop/20260702-a1b2c3/_nuxt/xxx.js配置:
ts
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
app: {
cdnURL: process.env.NUXT_APP_CDN_URL || '',
buildAssetsDir: '/_nuxt/',
},
routeRules: {
'/_nuxt/**': {
headers: {
'cache-control': 'public,max-age=31536000,immutable',
},
},
'/**': {
headers: {
'cache-control': 'public,max-age=0,must-revalidate',
},
},
},
})如果 /_nuxt/** 已经从外部 CDN 域名加载,routeRules 只会影响 Nuxt 服务自己返回的响应,不会自动影响对象存储或 CDN 上的资源。真正线上还要在 CDN 源站或平台 headers 里配置缓存。
8.3 Nuxt 静态生成 + CDN
适合文档站、博客、营销页:
bash
npm ci
npm run generate输出目录:
text
.output/public/可以把 .output/public/ 整体上传到静态托管平台或 CDN 源站。
如果使用版本目录:
bash
aws s3 sync .output/public/ s3://my-cdn-bucket/frontend/nuxt-shop/20260702-a1b2c3/但如果整站静态 HTML 也放在版本目录里,你还需要一个稳定入口,例如 CDN 规则、对象存储重定向或业务域名指向当前版本。
8.4 Nuxt Hybrid Rendering 与 CDN
Nuxt 可以用 routeRules 按路由控制渲染和缓存:
ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/about': { prerender: true },
'/blog/**': { isr: 3600 },
'/products/**': { swr: true },
'/admin/**': { ssr: false },
'/api/**': { cors: true },
},
})实践建议:
- 首页、关于页、文档页:适合预渲染并放 CDN。
- 商品详情、博客详情:可用 ISR/SWR,边缘或平台缓存要看部署平台支持。
- 后台管理页:通常 CSR + 静态资源 CDN 即可。
- API:只缓存公共 GET,不缓存用户私有数据。
8.5 Nuxt 部署到 Cloudflare Pages
Cloudflare Pages 支持 Nuxt,通常用 Nitro preset:
bash
NITRO_PRESET=cloudflare-pages npm run build或在配置中:
ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages',
},
})Cloudflare Pages 的 headers 可以放在 public/_headers,构建后会复制到输出目录。
text
# public/_headers
/_nuxt/*
Cache-Control: public, max-age=31536000, immutable
/*.html
Cache-Control: public, max-age=0, must-revalidate
/sw.js
Cache-Control: no-cache如果资源已经迁到独立 CDN 域名,这个 _headers 文件要作用在实际托管这些资源的平台上。
9. Vue CLI 存量项目落地
Vue CLI 已经不是新 Vue 项目的首选,但很多企业项目仍在使用。
9.1 vue.config.js
js
// vue.config.js
const cdnBase = process.env.CDN_BASE || '/'
module.exports = {
publicPath: cdnBase,
outputDir: 'dist',
assetsDir: 'assets',
productionSourceMap: false,
crossorigin: 'anonymous',
integrity: true,
configureWebpack: {
output: {
filename: 'assets/js/[name].[contenthash:8].js',
chunkFilename: 'assets/js/[name].[contenthash:8].js',
},
},
}publicPath 对应的是 Vue CLI 对 Webpack output.publicPath 的封装。
9.2 Vue CLI external Vue
js
module.exports = {
configureWebpack: {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
},
},
chainWebpack: (config) => {
config.plugin('html').tap((args) => {
args[0].cdn = {
js: [
'https://cdn.jsdelivr.net/npm/vue@3.5.17/dist/vue.global.prod.js',
'https://cdn.jsdelivr.net/npm/vue-router@4.5.1/dist/vue-router.global.prod.js',
],
}
return args
})
},
}public/index.html:
html
<% if (htmlWebpackPlugin.options.cdn) { %>
<% htmlWebpackPlugin.options.cdn.js.forEach(function (url) { %>
<script src="<%= url %>" crossorigin="anonymous"></script>
<% }) %>
<% } %>
<div id="app"></div>注意:上面没有 SRI hash,只是展示机制。生产要补真实 integrity,并且要锁定版本。
10. Nginx / 对象存储 / 静态平台缓存配置
10.1 Nginx 静态资源缓存
nginx
server {
listen 443 ssl http2;
server_name cdn-origin.example.com;
root /var/www/frontend/shop-web/current;
location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|webp|avif|woff2?|ttf|otf)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
try_files $uri =404;
}
location ~* \.(?:html|webmanifest)$ {
add_header Cache-Control "public, max-age=0, must-revalidate" always;
try_files $uri =404;
}
location = /sw.js {
add_header Cache-Control "no-cache" always;
try_files $uri =404;
}
}10.2 Netlify _headers
text
/assets/*
Cache-Control: public, max-age=31536000, immutable
/_nuxt/*
Cache-Control: public, max-age=31536000, immutable
/index.html
Cache-Control: public, max-age=0, must-revalidate
/sw.js
Cache-Control: no-cache10.3 Cloudflare Pages _headers
text
/assets/*
Cache-Control: public, max-age=31536000, immutable
/_nuxt/*
Cache-Control: public, max-age=31536000, immutable
/*.html
Cache-Control: public, max-age=0, must-revalidate10.4 S3 / CloudFront 思路
常见做法:
bash
# hash 资源长期缓存
aws s3 sync dist/assets/ s3://bucket/frontend/shop-web/20260702-a1b2c3/assets/ \
--cache-control "public,max-age=31536000,immutable"
# HTML 单独短缓存
aws s3 cp dist/index.html s3://bucket/frontend/shop-web/index.html \
--cache-control "public,max-age=0,must-revalidate" \
--content-type "text/html; charset=utf-8"CloudFront 可以按路径行为设置缓存策略:
text
/frontend/shop-web/*/assets/* -> long TTL
/frontend/shop-web/index.html -> no-cache10.5 压缩与传输优化:gzip 与 Brotli
CDN 的价值不止缓存,还包括压缩传输。文本类资源(JS、CSS、HTML、SVG、JSON)压缩后体积通常能减少 60%–80%。
两种主流算法:
| 算法 | 压缩率 | 兼容性 | 说明 |
|---|---|---|---|
gzip | 基准 | 几乎所有客户端 | 必配的保底方案 |
br(Brotli) | 比 gzip 高约 15%–25% | 现代浏览器均支持(要求 HTTPS) | 优先方案 |
浏览器通过 Accept-Encoding 请求头声明支持的算法,服务端/CDN 返回 Content-Encoding 说明实际使用的算法。
Nginx 示例(对已有内容做动态压缩):
nginx
http {
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
gzip_vary on;
}更推荐“预压缩”:构建时生成 .gz / .br 文件,服务器直接发送,避免实时压缩消耗 CPU。
以 Vite 为例,需要安装压缩插件(如 vite-plugin-compression2),并在配置中启用:
ts
// vite.config.ts
import { defineConfig } from 'vite'
import { compression } from 'vite-plugin-compression2'
export default defineConfig({
plugins: [
vue(),
compression({ algorithm: 'gzip' }),
compression({ algorithm: 'brotliCompress' }),
],
})text
# 构建产物示例
dist/assets/index.abc123.js
dist/assets/index.abc123.js.gz
dist/assets/index.abc123.js.brnginx
# Nginx 静态预压缩(需要 gzip_static 模块)
gzip_static on;注意:
- 图片(JPEG/PNG/WebP/AVIF)、woff2 字体、视频本身已经压缩过,再走 gzip/br 几乎无收益,不要浪费 CPU。
- 预压缩文件必须和原文件一起上传到 CDN,并保持同名(
.js.gz、.js.br)。 - S3/对象存储方案通常需要在上传时设置
Content-Encoding,或通过 CDN 的边缘函数/转换规则返回正确头。 - 校验:响应头里看到
Content-Encoding: br或gzip,且体积明显变小,才算生效。
11. 安全与跨域
11.1 crossorigin
当 JS/CSS 从 CDN 域名加载时,建议设置:
html
<script src="https://cdn.example.com/app/index.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.example.com/app/index.css" crossorigin="anonymous">收益:
- 能让跨域脚本错误上报更完整。
- 配合 SRI 时通常需要。
- 字体、WASM、模块脚本等跨域加载更可控。
CDN 响应头一般需要:
http
Access-Control-Allow-Origin: *如果资源只给指定站点使用,也可以配置:
http
Access-Control-Allow-Origin: https://www.example.com11.2 SRI
SRI(Subresource Integrity)用于让浏览器校验 CDN 脚本或样式内容是否被篡改。
html
<script
src="https://cdn.jsdelivr.net/npm/vue@3.5.17/dist/vue.global.prod.js"
integrity="sha384-替换为真实hash"
crossorigin="anonymous">
</script>适合:
- 第三方 CDN 上的 Vue、ECharts、地图 SDK
- 不由你完全控制的 CDN 资源
不适合:
- 每次构建都会变化的业务 JS,如果没有自动注入 SRI 的构建流程,会增加发布复杂度。
11.3 CSP
如果项目启用了 Content Security Policy,需要把 CDN 域名加入白名单:
http
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://cdn.example.com;
img-src 'self' data: https://cdn.example.com;
font-src 'self' https://cdn.example.com;
connect-src 'self' https://api.example.com;不要为了省事直接放开:
http
script-src *这会扩大 XSS 和供应链风险。
12. 图片、字体、视频等资源怎么上 CDN
12.1 图片
推荐:
- 小图标用 SVG 或 icon font,注意缓存。
- 业务图片用对象存储 + CDN。
- 商品图、头像、文章封面用图片 CDN,支持 resize、webp/avif、质量压缩。
- 源图路径包含版本或内容 hash。
Vue 示例:
vue
<script setup lang="ts">
import logoUrl from '@/assets/logo.svg'
</script>
<template>
<img :src="logoUrl" alt="Logo">
</template>构建后 logoUrl 会变成 CDN 资源 URL。
12.2 字体
CSS:
css
@font-face {
font-family: "Brand";
src: url("./assets/fonts/brand.woff2") format("woff2");
font-display: swap;
}注意:
- 字体跨域加载需要 CORS。
- 优先使用
woff2。 - 使用
font-display: swap降低字体阻塞渲染的影响。 - 字体文件带 hash 后可以长期缓存。
12.3 视频和大文件
视频、大文件下载不要简单塞进前端构建产物:
- 放对象存储。
- CDN 开启 Range Requests。
- 大视频考虑 HLS/DASH 切片。
- 下载文件名带版本。
- 私有下载用签名 URL,别把鉴权文件公开长期缓存。
13. 项目级发布流程示例
13.1 CI/CD 流程
text
1. 生成版本号:20260702-gitsha
2. 安装依赖:npm ci
3. 构建时注入 CDN base
4. npm run build
5. 上传 dist/assets 或 _nuxt 到 CDN 版本目录
6. 验证 CDN 资源 200、缓存头正确、gzip/br 正常
7. 发布 HTML/SSR 服务,让它引用新版本 CDN
8. 灰度观察错误率、chunk 加载失败率
9. 保留旧版本资源 7-30 天13.2 GitHub Actions 示例
yaml
name: Deploy Frontend
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Build
env:
VERSION: ${{ github.run_number }}-${{ github.sha }}
VITE_CDN_BASE: https://cdn.example.com/frontend/shop-web/${{ github.run_number }}-${{ github.sha }}/
run: npm run build
- name: Upload static assets
run: |
aws s3 sync dist/ s3://my-cdn-bucket/frontend/shop-web/${{ github.run_number }}-${{ github.sha }}/ \
--cache-control "public,max-age=31536000,immutable" \
--exclude "index.html" \
--exclude "sw.js"
- name: Upload HTML
run: |
aws s3 cp dist/index.html s3://my-origin-bucket/shop-web/index.html \
--cache-control "public,max-age=0,must-revalidate"13.3 回滚流程
如果版本 20260702-d4e5f6 出问题:
text
1. 不删除 CDN 新版本资源。
2. 把 HTML 或 SSR 配置指回 `20260701-a1b2c3`。
3. 清理 HTML 缓存或让短缓存自然过期。
4. 观察错误率恢复。
5. 保留问题版本资源,便于复盘和错误栈 sourcemap 匹配。14. 常见线上问题排查
14.1 页面白屏,控制台提示 chunk 404
常见原因:
- HTML 缓存太久,引用旧 chunk。
- 发布时先更新 HTML,后上传 CDN 资源。
- 清理了旧版本 chunk。
publicPath或base配错。
处理:
- HTML 设置
no-cache或短缓存。 - 先上传资源,再发布 HTML。
- 版本目录保留 7-30 天。
- 检查 Network 中 chunk 请求 URL 是否指向正确 CDN 路径。
14.2 本地正常,线上图片/字体 404
常见原因:
base或publicPath少了尾部/。public目录文件引用路径写死。- Webpack
assetModuleFilename或 ViteassetFileNames输出目录和上传目录不一致。
处理:
- CDN base 统一以
/结尾。 - 用构建产物里的
index.html和 manifest 检查实际路径。 - 上传目录与构建输出路径保持一致。
14.3 字体跨域失败
常见原因:
- CDN 没有
Access-Control-Allow-Origin。 - CSS 从业务域名加载,字体从 CDN 加载,浏览器要求 CORS。
处理:
http
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=31536000, immutable14.4 第三方 CDN 脚本加载失败
处理建议:
- 锁定版本,不用
latest。 - 加 SRI 和
crossorigin。 - 对关键业务不要依赖不可控第三方 CDN。
- 必要时把第三方库同步到自有 CDN。
14.5 用户总是看到旧页面
常见原因:
- HTML 被 CDN 长缓存。
- Service Worker 缓存策略错误。
- 中间代理缓存了入口文件。
处理:
- HTML、SW、manifest 设置
no-cache。 - SW 发布要有更新策略和回滚策略。
- 在响应头里检查真实生效的
Cache-Control、Age、CF-Cache-Status或平台等价头。
14.6 系统性排查清单
线上 CDN 问题不要只靠“猜”,按下面顺序走一遍,绝大多数问题能在 10 分钟内定位:
- 确认现象范围:是所有用户还是个别用户?个别用户多半是本地缓存/代理;所有用户多半是配置或发布顺序问题。
- 确认请求链:打开 Network,看问题资源的完整 URL、状态码、响应头。
- 404:路径不一致、上传遗漏、版本目录被清理。
- 200 但内容是旧的:缓存头配置错误或边缘缓存未刷新。
- CORS 报错:字体、模块脚本、fetch 类资源缺少
Access-Control-Allow-Origin。
- 绕过缓存验证:
curl -I <资源URL>直连源站和 CDN 各查一次,对比响应头。bashcurl -I https://www.example.com/index.html curl -I https://cdn.example.com/frontend/shop-web/20260702-a1b2c3/assets/js/index.abc123.js - 检查发布顺序:是不是 HTML 先上线、资源后上传?是不是删了旧版本资源?
- 检查 base / publicPath:构建产物里
index.html引用的 URL 与上传路径是否完全一致(注意尾部/)。 - 检查压缩与 MIME:
Content-Type错误(例如.js被当成text/plain)会导致浏览器拒绝执行脚本;严格 MIME 检查(X-Content-Type-Options: nosniff)下尤其明显。 - 检查 CSP / SRI:控制台报
Refused to load或 integrity 校验失败,说明白名单或哈希不匹配。 - 复现与回归:用无痕窗口 + 禁用缓存复现,修复后在真实用户环境(移动端、企业代理)再验证一次。
💡 排查时优先相信响应头而不是平台控制台里的配置页面:配置写了不等于生效,响应头才是最终事实。
15. 最小可用方案清单
如果你要给一个 Vue/Vite 项目快速接入 CDN,最小方案是:
- 构建输出文件名带内容 hash。
vite.config.ts设置base: CDN_BASE。src/assets中的图片/字体通过 import 或 CSS url 进入构建。public中不带 hash 的文件设置短缓存。- 上传
dist/assets到 CDN 版本目录。 - HTML 设置
no-cache或短缓存。 - hash 资源设置
Cache-Control: public,max-age=31536000,immutable。 - 发布顺序是先资源、后 HTML。
- 保留旧版本资源至少 7 天。
- 用浏览器 Network 面板验证资源域名、状态码、缓存头和跨域头。
16. 学习路线
建议按这个顺序练习:
- 先理解 HTTP 缓存:
Cache-Control、ETag、max-age、immutable、no-cache。 - 用 Vite + Vue 做一个 demo,把
base改成 CDN URL,观察构建产物。 - 把
src/assets、public、CSS font、动态 import 都放进 demo,比较它们的输出差异。 - 用 Webpack 5 写一个同等 demo,理解
output.publicPath。 - 做一次“错误发布”:删掉旧 chunk,复现 chunk 404,再用版本目录修复。
- 做 Nuxt SSR demo,理解 HTML 由服务返回、静态资源走 CDN 的关系。
- 试一次 external ECharts 到 CDN,再比较 bundle 体积、加载链路和安全成本。
- 写 CI/CD:先上传资源,再发布 HTML,最后做回滚。
17. 官方资料与参考链接
- Vite 静态资源处理:https://vite.dev/guide/assets
- Vite 配置
base:https://vite.dev/config/shared-options.html#base - Vite 构建配置:https://vite.dev/config/build-options.html
- Vite 环境变量:https://vite.dev/guide/env-and-mode
- Vite 部署静态站点:https://vite.dev/guide/static-deploy.html
- Webpack
output.publicPath:https://webpack.js.org/configuration/output/#outputpublicpath - Webpack Public Path 指南:https://webpack.js.org/guides/public-path/
- Webpack Asset Modules:https://webpack.js.org/guides/asset-modules/
- Webpack Externals:https://webpack.js.org/configuration/externals/
- Webpack 缓存指南:https://webpack.js.org/guides/caching/
- Vue CLI 配置:https://cli.vuejs.org/config/
- Nuxt 4 配置:https://nuxt.com/docs/4.x/getting-started/configuration
- Nuxt 4
nuxt.config:https://nuxt.com/docs/4.x/api/nuxt-config - Nuxt 渲染模式:https://nuxt.com/docs/guide/concepts/rendering
- Nuxt 部署:https://nuxt.com/docs/getting-started/deployment
- Nitro 部署:https://nitro.build/deploy
- Cloudflare Pages Nuxt 部署:https://developers.cloudflare.com/pages/framework-guides/deploy-a-nuxt-site/
- Cloudflare Pages Headers:https://developers.cloudflare.com/pages/configuration/headers/
- Netlify Headers:https://docs.netlify.com/manage/routing/headers/
- AWS CloudFront 缓存行为:https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistValuesCacheBehavior.html
- MDN Cache-Control:https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
- MDN Subresource Integrity:https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
- MDN import maps:https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap
- MDN
crossorigin:https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin