Skip to content

前端 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/CSSVite/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 Workersw.js、workbox-*.js谨慎no-cacheSW 更新本身不能被长缓存挡住
PWA manifestmanifest.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.js
  • vendor.4c22ab.css
  • logo.3d8f01.svg
  • font.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.webmanifest
  • sw.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.png

4.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-StatusCloudflare 缓存状态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.production

src/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"

真实生产里建议分两步:

  1. 先上传新版本静态资源到 CDN 版本目录。
  2. 再发布 HTML 或 SSR 服务,使它引用新版本资源。

这样可以避免 HTML 已上线但 CDN 资源还没上传完成导致白屏。


6. Vite 中 external 第三方库到 CDN ​

6.1 什么时候 external ​

建议 external 的典型对象:

  • echarts
  • monaco-editor
  • 地图 SDK
  • PDF/Excel/图表等大体积低频依赖
  • 多个微前端共享且版本固定的基础库

不建议一开始就 external:

  • vue
  • vue-router
  • pinia
  • 频繁升级的业务组件库

因为 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.js

7.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.ts

8.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-cache

10.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-revalidate

10.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-cache

10.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.br
nginx
# 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.com

11.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 或 Vite assetFileNames 输出目录和上传目录不一致。

处理:

  • 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, immutable

14.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 分钟内定位:

  1. 确认现象范围:是所有用户还是个别用户?个别用户多半是本地缓存/代理;所有用户多半是配置或发布顺序问题。
  2. 确认请求链:打开 Network,看问题资源的完整 URL、状态码、响应头。
    • 404:路径不一致、上传遗漏、版本目录被清理。
    • 200 但内容是旧的:缓存头配置错误或边缘缓存未刷新。
    • CORS 报错:字体、模块脚本、fetch 类资源缺少 Access-Control-Allow-Origin。
  3. 绕过缓存验证:curl -I <资源URL> 直连源站和 CDN 各查一次,对比响应头。
    bash
    curl -I https://www.example.com/index.html
    curl -I https://cdn.example.com/frontend/shop-web/20260702-a1b2c3/assets/js/index.abc123.js
  4. 检查发布顺序:是不是 HTML 先上线、资源后上传?是不是删了旧版本资源?
  5. 检查 base / publicPath:构建产物里 index.html 引用的 URL 与上传路径是否完全一致(注意尾部 /)。
  6. 检查压缩与 MIME:Content-Type 错误(例如 .js 被当成 text/plain)会导致浏览器拒绝执行脚本;严格 MIME 检查(X-Content-Type-Options: nosniff)下尤其明显。
  7. 检查 CSP / SRI:控制台报 Refused to load 或 integrity 校验失败,说明白名单或哈希不匹配。
  8. 复现与回归:用无痕窗口 + 禁用缓存复现,修复后在真实用户环境(移动端、企业代理)再验证一次。

💡 排查时优先相信响应头而不是平台控制台里的配置页面:配置写了不等于生效,响应头才是最终事实。


15. 最小可用方案清单 ​

如果你要给一个 Vue/Vite 项目快速接入 CDN,最小方案是:

  1. 构建输出文件名带内容 hash。
  2. vite.config.ts 设置 base: CDN_BASE。
  3. src/assets 中的图片/字体通过 import 或 CSS url 进入构建。
  4. public 中不带 hash 的文件设置短缓存。
  5. 上传 dist/assets 到 CDN 版本目录。
  6. HTML 设置 no-cache 或短缓存。
  7. hash 资源设置 Cache-Control: public,max-age=31536000,immutable。
  8. 发布顺序是先资源、后 HTML。
  9. 保留旧版本资源至少 7 天。
  10. 用浏览器 Network 面板验证资源域名、状态码、缓存头和跨域头。

16. 学习路线 ​

建议按这个顺序练习:

  1. 先理解 HTTP 缓存:Cache-Control、ETag、max-age、immutable、no-cache。
  2. 用 Vite + Vue 做一个 demo,把 base 改成 CDN URL,观察构建产物。
  3. 把 src/assets、public、CSS font、动态 import 都放进 demo,比较它们的输出差异。
  4. 用 Webpack 5 写一个同等 demo,理解 output.publicPath。
  5. 做一次“错误发布”:删掉旧 chunk,复现 chunk 404,再用版本目录修复。
  6. 做 Nuxt SSR demo,理解 HTML 由服务返回、静态资源走 CDN 的关系。
  7. 试一次 external ECharts 到 CDN,再比较 bundle 体积、加载链路和安全成本。
  8. 写 CI/CD:先上传资源,再发布 HTML,最后做回滚。

17. 官方资料与参考链接 ​

基于 VitePress 构建的个人学习文档知识库