基于 Nuxt 4 的现代 Headless CMS 前端:架构深度解析与二次开发指南
本文面向希望基于此项目进行二次开发的前端工程师,系统性地讲解项目的技术选型、架构设计与模块划分,并提供扩展开发的实操指引。
一、项目概览
本项目是一个面向内容管理的现代前端应用,使用 Nuxt 4(Vue 3)构建,支持 SSR/SSG 双模式部署,提供文章、分类、标签、评论等完整的内容管理功能,并内置多语言(中英文)和暗色模式支持。
核心特性一览
| 特性 | 技术方案 |
|---|---|
| 框架 | Nuxt 4(Vue 3.5+) |
| 样式 | Tailwind CSS v4 + CSS 变量主题系统 |
| UI 组件库 | shadcn-vue(基于 Reka UI) |
| 状态管理 | Pinia + 持久化插件 |
| 数据请求 | Axios + TanStack Vue Query |
| 国际化 | @nuxtjs/i18n(prefix 路由策略) |
| 内容渲染 | marked + Shiki + KaTeX + Mermaid |
| 富文本编辑 | Tiptap |
| API 协议 | Protobuf 生成 TypeScript HTTP 客户端 |
| 部署 | SSG 静态生成 + SPA fallback |
二、技术栈详解
2.1 Nuxt 4 + Vue 3
项目基于 Nuxt 4,采用文件系统路由(app/pages/),组件自动扫描注册,并通过 compatibilityDate 锁定行为一致性。SSR 模式开启,Nitro 引擎负责服务端渲染与静态生成。
// nuxt.config.ts 核心配置
export default defineNuxtConfig({
ssr: true,
nitro: {
prerender: {
routes: ['/'],
crawlLinks: true,
},
},
})
2.2 Tailwind CSS v4 + 语义化主题
项目使用 Tailwind CSS v4(通过 @tailwindcss/vite 插件集成),并采用 CSS 变量构建完整的语义化配色系统:
/* main.css 主题变量定义 */
:root {
--primary: 142.1 76.2% 36.3%; /* 极光电能绿 */
--accent: 217.2 91.2% 59.8%; /* 赛博科技蓝 */
--background: 210 40% 98%;
--foreground: 224 71.4% 4.1%;
/* ...更多语义 token */
}
.dark {
--primary: 142.1 86.2% 50.3%; /* 霓虹电能绿 */
--background: 224 45% 6%;
/* ... */
}
暗色模式通过 .dark class 精确切换,并设置了 @media (prefers-color-scheme: dark) 作为纯 CSS 后备方案,防止 JS 未执行时出现 FOUC(闪烁)问题。
关键设计决策:禁用了 Tailwind 的 dark: 变体写法,统一使用语义化 CSS 变量(如 hsl(var(--foreground)))控制样式,确保主题切换的一致性。
2.3 shadcn-vue 组件体系
UI 组件基于 shadcn-vue 构建,底层依赖 Reka UI(无头组件库)。项目在 app/components/ui/ 下维护了完整的组件集合:
ui/
├── button/ # 按钮
├── input/ # 输入框
├── select/ # 下拉选择
├── sheet/ # 侧边抽屉
├── dropdown-menu/ # 下拉菜单
├── pagination/ # 分页
├── skeleton/ # 骨架屏
├── sonner/ # Toast 通知
├── carousel/ # 轮播
├── avatar/ # 头像
├── switch/ # 开关
├── image/ # 图片(含加载失败占位)
└── ... # 更多
这些组件可以通过 npx shadcn-vue@latest add <component> 按需添加,遵循 shadcn 的 "拥有代码" 哲学——组件代码直接存在于你的项目中,可完全自定义。
三、架构设计
3.1 整体分层架构
┌───────────────────────────────────────────────────┐
│ Pages / Views │ 页面层
├───────────────────────────────────────────────────┤
│ Components (UI / Business) │ 组件层
├────────────┬────────────┬─────────────────────────┤
│ Stores │ Preferences│ Composables (Hooks) │ 状态/逻辑层
├────────────┴────────────┴─────────────────────────┤
│ API Layer (三层架构) │ 数据层
├───────────────────────────────────────────────────┤
│ Core (Transport / Storage) │ 基础设施层
└───────────────────────────────────────────────────┘
3.2 API 三层架构
API 模块是本项目最核心的架构设计之一,分为三层:
api/
├── generated/ ← 第一层:Protobuf 自动生成的 TypeScript 客户端
├── service/ ← 第二层:业务服务封装(单例 Client + 纯函数)
└── composables/ ← 第三层:Vue Query 集成(响应式 Hooks)
第一层:Generated(协议层)
由 protoc-gen-typescript-http 自动生成,定义了与后端 API 的类型契约。每个服务提供 createXxxServiceClient 工厂方法,接受一个 requestApi 函数作为 HTTP 适配器。
第二层:Service(服务层)
对生成的客户端进行封装,提供业务语义清晰的纯函数 API:
// service/post.ts
let _instance: ReturnType<typeof createPostServiceClient> | null = null;
export function getPostService() {
if (!_instance) {
_instance = createPostServiceClient(requestApi); // 注入 HTTP 适配器
}
return _instance;
}
export async function listPost(paging?, formValues?, fieldMask?, orderBy?, options?) {
// 组装查询参数,委托给生成的客户端
return await getPostService().List({ fieldMask, orderBy, query, page, pageSize });
}
第三层:Composables(组合式函数层)
集成 TanStack Vue Query,提供响应式数据管理:
// composables/post.ts
export function useListPost(options?) {
return useMutation({
mutationFn: (params) => {
const locale = getCurrentLocale(); // 自动注入当前语言
return listPost(params.paging, params.formValues, ...);
},
...options,
});
}
// 不带 Hook 的纯数据获取(供 Store / 非组件上下文使用)
export async function fetchListPost(params: ListPostParams) {
return queryClient.fetchQuery({
queryKey: ['listPost', params, locale],
queryFn: () => listPost(...),
});
}
这种分层设计的优势:
- 可替换性:Service 层屏蔽了协议细节,更换 API 协议只需重写 Service 层
- 灵活调用:组件中使用
useXxxHook,Store 中使用fetchXxx方法 - 语言感知:Composable 层自动注入当前 locale,组件无需关心
3.3 请求客户端(RequestClient)
core/transport/rest/ 实现了一个功能完备的 HTTP 客户端:
transport/
├── rest/
│ ├── request-client.ts # Axios 单例封装
│ ├── request-api.ts # Protobuf 适配器
│ ├── preset-interceptors.ts # 预置拦截器(401、错误消息)
│ ├── pagination.ts # 分页参数处理
│ ├── utils.ts # 查询字符串、排序等工具
│ └── modules/
│ ├── interceptor.ts # 拦截器管理器
│ ├── uploader.ts # 文件上传
│ └── downloader.ts # 文件下载
└── sse/
└── sse_client.ts # Server-Sent Events 客户端
内置拦截器链(按执行顺序):
- Token 注入:自动添加
Authorization: Bearer xxx - Request ID:注入
X-Request-ID和XMLHttpRequest标识 - Locale 注入:自动添加
Accept-Language请求头 - Auth 拦截器:401 时自动刷新 Token 或跳转登录页
- 响应解构:提取
response.data,简化调用方处理 - 错误消息:统一提取错误文本并通过回调通知
初始化时机:在 Nuxt 插件 01.init-client.ts 中完成初始化,注入 Token 获取、语言获取、Token 刷新、重新认证等回调:
// plugins/01.init-client.ts
export default defineNuxtPlugin(async (nuxtApp) => {
if (import.meta.server) return;
RequestClient.init(config.apiURL, {
getToken: () => accessStore.accessToken?.value ?? null,
getLocale: () => nuxtApp.$i18n?.locale?.value || 'zh-CN',
refreshToken: async () => { /* ... */ },
onReAuthenticate: async () => { /* ... */ },
});
});
3.4 状态管理
项目使用 Pinia 进行状态管理,按职责分为 core 和 app 两个模块:
stores/modules/
├── core/
│ ├── access.state.ts # 访问控制(Token、登录状态)
│ ├── user.state.ts # 用户信息
│ ├── navbar.state.ts # 导航栏状态
│ └── loading.state.ts # 全局加载状态
└── app/
└── auth.state.ts # 认证逻辑(登录、登出、Token 刷新)
认证流程(auth.state.ts):
登录 → API 登录 → 存储 AccessToken → 获取用户信息 → 跳转首页
↓
Token 过期 ← 401 拦截 → 自动刷新 Token → 失败则跳转登录页
密码在传输前通过 AES-CBC 加密(CryptoJS),密钥通过运行时配置注入。
3.5 偏好设置系统(Preferences)
core/preferences/ 实现了一套完整的用户偏好管理:
preferences/
├── preferences.ts # PreferenceManager 单例
├── use-preferences.ts # Vue Composable
├── update-css-variables.ts # CSS 变量同步
├── config/
│ └── default.ts # 默认偏好值
└── types/ # 类型定义
核心设计:
- PreferenceManager 是响应式单例,通过
reactive管理状态,readonly对外暴露 - 偏好变更自动同步到 CSS 变量(控制主题色、圆角等)和 localStorage
- 语言切换自动同步到
@nuxtjs/i18n - 支持主题模式:
light/dark/auto(跟随系统) - 使用
useDebounceFn防抖写入,避免频繁操作存储
// 组件中使用
const { isDark, theme, locale, toggleTheme, setLocale } = usePreferences();
3.6 存储管理器(StorageManager)
core/storage/ 提供了增强版的浏览器存储抽象:
- TTL 过期:每个存储项支持独立的过期时间
- 驱逐策略:支持 LRU / LFU / Hybrid 三种驱逐算法
- 批量操作:
getItems/setItems/removeItems,自动让出主线程避免阻塞 - 跨标签页同步:基于 BroadcastChannel API
- 监控埋点:命中率、过期次数、错误数等指标
- 配额管理:自动清理过期项,处理
QuotaExceededError
3.7 国际化方案
使用 @nuxtjs/i18n 模块,采用 prefix 路由策略(所有路由带语言前缀):
i18n: {
locales: [
{ code: 'zh-CN', file: 'zh-CN/index.ts' },
{ code: 'en-US', file: 'en-US/index.ts' },
],
defaultLocale: 'zh-CN',
strategy: 'prefix', // /zh-CN/、/en-US/
}
翻译文件按模块拆分:
locales/zh-CN/
├── app.json # 应用全局
├── authentication.json # 认证相关
├── cms.json # 内容管理
├── comment.json # 评论
├── common.json # 通用
├── navbar.json # 导航栏
├── page.json # 页面
└── ...
语言感知 API 调用:通过 getCurrentLocale() 工具函数统一获取当前语言,确保 API 请求的语言参数与 UI 语言同步:
// utils/locale.ts
export function getCurrentLocale(): SupportedLanguagesType {
const nuxtApp = useNuxtApp();
const locale = nuxtApp.$i18n?.locale?.value;
// zh → zh-CN, en → en-US
return map[locale] || 'zh-CN';
}
四、核心功能模块
4.1 内容渲染引擎(ContentViewer)
components/content/Viewer.vue 是一个功能丰富的 Markdown / HTML 渲染器:
| 能力 | 实现 |
|---|---|
| Markdown 解析 | marked(自定义 Renderer) |
| 代码高亮 | Shiki(双主题 light/dark 切换) |
| 数学公式 | KaTeX(行内 $...$,块级 $$...$$) |
| 流程图 | Mermaid.js |
| HTML 消毒 | DOMPurify(白名单过滤) |
| 图片处理 | figure/figcaption 语义化包裹 |
渲染流程:Markdown → marked 解析 → KaTeX 公式处理 → DOMPurify 消毒 → HTML 输出
4.2 富文本评论编辑器
components/comment/RichTextEditor.vue 基于 Tiptap 构建,支持:
- 格式化:加粗、斜体、删除线、代码块、无序列表
- 字数统计:实时显示,超出限制时警告
- 快捷键:
Ctrl + Enter快速提交 - 双向绑定:
v-model集成
4.3 页面路由结构
pages/
├── index.vue # 首页(Hero + 精选文章 + 分类 + 标签 + 最新文章)
├── login.vue # 登录页
├── register.vue # 注册页
├── about.vue # 关于页
├── contact.vue # 联系页
├── settings.vue # 设置页
├── user.vue # 用户中心
├── post/
│ ├── index.vue # 文章列表
│ └── [id].vue # 文章详情
├── category/
│ ├── index.vue # 分类列表
│ └── [id].vue # 分类详情
└── tag/
├── index.vue # 标签列表
└── [id].vue # 标签详情
首页使用了 IntersectionObserver + MutationObserver 实现滚动渐显动画,性能开销极低。
五、环境配置与构建
5.1 多环境配置
通过 .env 文件管理不同环境的配置:
# .env.development
NUXT_PUBLIC_API_BASE_URL=http://localhost:6700
NUXT_PUBLIC_ENABLE_MOCK=false
NUXT_PUBLIC_AES_KEY=
# .env.production
NUXT_PUBLIC_API_BASE_URL=https://api.example.com
NUXT_PUBLIC_ENABLE_MOCK=false
NUXT_PUBLIC_AES_KEY=your-secret-key
5.2 构建命令
pnpm dev # 开发服务器(加载 .env.development)
pnpm build # 服务端构建(加载 .env.production)
pnpm generate # 静态站点生成(SSG)
pnpm preview # 预览构建结果
5.3 SSG 部署策略
项目支持静态站点生成(SSG)部署:
- 预渲染:Nitro 引擎预渲染
/首页,并自动爬取内部链接 - 动态路由:
/post/:id、/category/:id等动态路由由客户端渲染(SPA fallback) - 根路径重定向:构建后自动生成
index.html,将根路径/重定向到/zh-CN/
// nuxt.config.ts — 构建后钩子
nitro: {
hooks: {
'prerender:done'() {
writeFileSync(resolve(outputDir, 'index.html'), `
<meta http-equiv="refresh" content="0;url=/zh-CN/">
<script>location.replace("/zh-CN/"+location.search+location.hash)</script>
`);
},
},
}
配合 Nginx 的 try_files $uri $uri/ /index.html 即可完成 SPA fallback。
六、二次开发指南
6.1 新增业务模块
以添加一个「产品」模块为例:
第一步:确认 API 类型定义
在 api/generated/ 中确保后端已生成对应的 TypeScript 客户端(如 createProductServiceClient)。
第二步:编写 Service 层
// api/service/product.ts
import { createProductServiceClient } from '@/api/generated/app/service/v1';
import { requestApi } from '@/core/transport/rest';
let _instance: ReturnType<typeof createProductServiceClient> | null = null;
export function getProductService() {
if (!_instance) {
_instance = createProductServiceClient(requestApi);
}
return _instance;
}
export async function listProduct(paging?, formValues?, options?) {
return await getProductService().List({ ... });
}
export async function getProduct(id: number) {
return await getProductService().Get({ id });
}
第三步:编写 Composable 层
// api/composables/product.ts
import { useMutation } from '@tanstack/vue-query';
import { listProduct, getProduct } from '@/api/service/product';
import { getCurrentLocale } from '@/utils/locale';
export function useListProduct(options?) {
return useMutation({
mutationFn: (params) => {
const locale = getCurrentLocale();
return listProduct(params.paging, params.formValues, { locale });
},
...options,
});
}
export async function fetchProduct(id: number) {
const locale = getCurrentLocale();
return queryClient.fetchQuery({
queryKey: ['getProduct', id, locale],
queryFn: () => getProduct(id, locale),
});
}
第四步:注册导出
在 api/service/index.ts 和 api/composables/index.ts 中添加:
export * from './product';
第五步:创建页面
在 pages/product/ 下创建路由页面,使用 Composable 获取数据。
6.2 新增 UI 组件
项目使用 shadcn-vue,可通过 CLI 添加标准组件:
npx shadcn-vue@latest add dialog
自定义业务组件放在 components/ 对应的目录中,Nuxt 会自动扫描注册。注意:组件只扫描 .vue 文件,index.ts 文件会被忽略,避免命名冲突。
6.3 新增多语言支持
- 在
locales/下创建新的语言目录(如ja-JP/) - 复制现有语言文件并翻译
- 在
nuxt.config.ts的i18n.locales中注册新语言 - 在
core/preferences/types.ts的SupportedLanguagesType中添加新语言类型
6.4 自定义主题
主题通过 CSS 变量控制,修改 assets/css/main.css 中的变量即可:
:root {
--primary: 210 100% 50%; /* 改为蓝色主题 */
--accent: 280 80% 60%; /* 改为紫色点缀 */
}
如需添加更多预设主题,可在 core/preferences/config/ 中扩展。
6.5 扩展请求拦截器
在需要自定义请求/响应处理时,可通过 RequestClient 的拦截器 API:
const client = RequestClient.getInstance();
// 添加请求拦截器
client.addRequestInterceptor({
fulfilled: (config) => {
config.headers['X-Custom-Header'] = 'value';
return config as never;
},
});
// 添加响应拦截器
client.addResponseInterceptor({
fulfilled: (response) => {
// 自定义响应处理
return response;
},
});
6.6 关键注意事项
| 场景 | 注意事项 |
|---|---|
| Composable 函数命名 | 需精确匹配单复数(如 useListPosts vs useListPost) |
| 组件中使用 i18n | 需使用 useI18n() composable,不要直接引用 $t |
| 路由导航 | 使用 navigateTo() 而非 useRouter().push() |
| 语言切换 | 通过 switchLocalePath() 并传入配置的 locale 联合类型 |
| Store 中导航 | 使用 navigateTo() 替代 useRouter()(Pinia 中无法使用路由 composables) |
| 暗色模式样式 | 使用 CSS 变量 hsl(var(--foreground)) 而非 Tailwind dark: 变体 |
| 图片占位 | 使用 <UiImage> 公共组件,自动处理加载失败 |
| Dev server 缓存 | 新增组件后需重启 dev server 刷新自动导入缓存 |
| JSON 翻译文件 | 禁止使用裸露花括号 {},需使用 {'@'} 转义特殊字符 |
七、项目结构速查
app/
├── api/ # API 三层架构
│ ├── generated/ # Protobuf 生成代码
│ ├── service/ # 业务服务封装
│ └── composables/ # Vue Query Hooks
├── assets/css/ # 全局样式 + 主题变量
├── components/ # Vue 组件
│ ├── auth/ # 认证组件(登录/注册)
│ ├── category/ # 分类组件
│ ├── comment/ # 评论组件
│ ├── content/ # 内容渲染(Viewer)
│ ├── home/ # 首页区块
│ ├── layout/ # 布局组件(Header/Footer/Nav)
│ ├── post/ # 文章组件
│ └── ui/ # 基础 UI 组件(shadcn-vue)
├── constants/ # 常量定义
├── core/ # 核心基础设施
│ ├── preferences/ # 偏好设置系统
│ ├── storage/ # 存储管理器
│ └── transport/ # HTTP / SSE 传输层
├── hooks/ # 通用 Composables
├── layouts/ # 页面布局
├── pages/ # 路由页面
├── plugins/ # Nuxt 插件
├── stores/ # Pinia 状态管理
├── utils/ # 工具函数
└── typings/ # 全局类型定义
locales/ # 国际化翻译文件
├── zh-CN/
└── en-US/
八、总结
本项目采用清晰的分层架构,将 协议层 → 服务层 → 组合函数层 解耦,使每一层都可以独立演进和替换。核心基础设施(Transport、Storage、Preferences)高度模块化,可方便地扩展或替换实现。
对于二次开发者而言,最常见的扩展模式是:新增 Service → 新增 Composable → 创建页面组件。整个流程有清晰的模板可遵循,大部分情况下无需触碰基础设施代码。
项目在暗色模式、国际化、SSR/SSG 兼容性等方面积累了大量工程实践细节,建议开发者在深入开发前完整阅读 core/ 目录下的实现代码,理解整体设计意图后再进行扩展。
快速开始:
pnpm install && pnpm dev,打开http://localhost:3000即可运行。
- Github 开源仓库:github.com/tx7do/go-wi…
- Gitee 开源仓库:gitee.com/tx7do/go-wi…
- 在线演示地址:cms.gowind.cloud