思路二(客户端运行时动态请求接口) 的完整落地方案。
最核心的诉求:在用户访问页面时,实时去后端获取 Markdown 文本并完美渲染,同时保留 VitePress 标志性的侧边栏、导航栏以及文档样式(包括代码高亮等)。
下面梳理一套完整的全栈落地方案,包含前端配置、路由处理、Markdown 深度解析(支持代码高亮)以及后端接口规范。
一、整体架构设计
由于是动态读取,不需要为每篇文章建一个 .md 文件,而是利用 VitePress 的动态路由(Dynamic Routing),让所有文章共享一个通用的模板页面(例如:/posts/[id])。
docs/
├── .vitepress/
│ └── config.js # VitePress 配置
├── posts/
│ └── [id].md # 通用动态渲染模板
└── index.md # 门户首页
二、前端代码实现
1. 安装必要的解析依赖
在客户端动态解析 Markdown,需要 markdown-it,以及用来让代码块自带语法高亮的 shiki(VitePress 官方同款高亮引擎)。
pnpm add markdown-it shiki
2. 编写动态模板页面 docs/posts/[id].md
这个文件虽然是 .md 结尾,但直接在里面写 Vue 3 的 Composition API 代码,通过 useData() 获取 URL 中的 id 参数。
---
# 告诉 VitePress 这是一个动态路由页面,告诉它有哪些合法的 id(哪怕先传空,运行时再接管)
paths:
paths() {
return [{ params: { id: 'dynamic' } }]
}
---
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useData } from 'vitepress'
import MarkdownIt from 'markdown-it'
import { codeToHtml } from 'shiki'
// 获取当前路由参数,例如 /posts/123 -> params.value.id 为 123
const { params } = useData()
const htmlContent = ref('')
const loading = ref(true)
const error = ref(null)
// 1. 初始化 Markdown 解析器
const md = new MarkdownIt({
html: true, // 支持原生 HTML 标签
linkify: true, // 自动将 URL 转换为链接
})
// 2. 异步配置 Shiki 语法高亮(复用 VitePress 体验的关键)
async function initMarkdownRenderer() {
// 自定义 markdown-it 的代码块渲染逻辑
md.options.highlight = (code, lang) => {
try {
// 运行时动态高亮代码
return codeToHtml(code, {
lang: lang || 'txt',
theme: 'github-dark' // 可以根据当前的主题切换,这里固定一个暗色主题
})
} catch (e) {
return `<pre><code>${md.utils.escapeHtml(code)}</code></pre>`
}
}
}
// 3. 请求后端网关/API 获取数据
async function fetchArticleData(articleId) {
if (!articleId || articleId === 'dynamic') return
loading.value = true
error.value = null
try {
// 替换为真实后端 Gateway 接口地址
const response = await fetch(`https://api.yourcompany.com/sms-gateway/portal/articles/${articleId}`)
if (!response.ok) throw new Error('文章获取失败')
const result = await response.json()
// 假设后端返回格式为 { code: 200, data: { content: "## Markdown 内容" } }
const rawMarkdown = result.data.content
// 解析为 HTML 并赋值
htmlContent.value = md.render(rawMarkdown)
} catch (err) {
console.error(err)
error.value = '无法加载该文档,请稍后再试或联系系统管理员。'
} finally {
loading.value = false
}
}
// 4. 生命周期与监听
onMounted(async () => {
await initMarkdownRenderer()
fetchArticleData(params.value.id)
})
// 监听路由 id 变化(当用户在侧边栏切换文章时触发)
watch(() => params.value.id, (newId) => {
fetchArticleData(newId)
})
</script>
<!-- 骨架屏 / 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="spinner"></div>
<p>正在从云端安全加载文档...</p>
</div>
<!-- 错误处理 -->
<div v-else-if="error" class="error-container">
<p class="error-text">{{ error }}</p>
</div>
<!-- 核心:内容渲染区域 -->
<!-- class="vp-doc" 是灵魂,它能让动态生成的 HTML 完美继承 VitePress 的所有排版、字体和间距样式 -->
<div v-else class="vp-doc" v-html="htmlContent"></div>
<style scoped>
.loading-container, .error-container {
padding: 40px 0;
text-align: center;
color: var(--vp-c-text-2);
}
.error-text {
color: var(--vp-c-danger-1);
font-weight: 500;
}
.spinner {
border: 3px solid var(--vp-c-bg-elv);
border-top: 3px solid var(--vp-c-brand-1);
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
三、配置侧边栏动态路由
为了让用户能点击左侧菜单切换这些数据库文章,需要修改 .vitepress/config.js。因为是运行时方案,侧边栏导航指向的链接格式必须固定:
// .vitepress/config.js
export default {
title: "企业内部通用门户",
description: "基于 VitePress 的动态内容管理系统",
themeConfig: {
nav: [{ text: "首页", link: "/" }],
sidebar: [
{
text: "数据库动态文档",
items: [
// 这里的 id 对应数据库里的主键,点击时会触发上面页面的 watch 监听并实时拉取
{ text: "网关通用接入规范", link: "/posts/1" },
{ text: "消息中心每日快照说明", link: "/posts/2" },
{ text: "Keycloak 私有化部署指南", link: "/posts/3" },
],
},
],
},
};
💡 进阶技巧:如果左侧菜单列表本身也想从数据库动态读取,可以在
.vitepress/config.js里利用 Node.js 的fs或发送一个同步/异步的fetch请求去连中间件,在打包时把菜单生出来。这样只需要维护数据库,客户端菜单和内容就全活了。
四、后端标准接口规范
为了让前端平稳拿到数据,后端的接口通常需要注意跨域(CORS)问题以及数据格式。
1. 推荐的 JSON 返回格式
{
"code": 200,
"msg": "success",
"data": {
"id": 1,
"title": "网关通用接入规范",
"updatedAt": "2026-07-11 16:00:00",
"content": "# 网关接入规范\n\n### 1. 认证安全\n系统基于 **Keycloak** 进行统一认证...\n\n```javascript\nconst token = await getAuthToken();\n```"
}
}
2. 数据库设计要点(MySQL 示例)
在数据库中存 Markdown,字段类型强烈建议使用 LONGTEXT 或 MEDIUMTEXT,以防文档过长被截断;另外建议加上特定的状态控制:
CREATE TABLE `portal_articles` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL COMMENT '文章标题',
`content` longtext NOT NULL COMMENT '存储原生的 Markdown 字符串',
`status` tinyint DEFAULT '1' COMMENT '1:发布, 0:草稿',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
五、避坑指南与优化建议
1. 白屏与 SEO 问题
采用这种方案后,由于内容是浏览器运行后才去捞的,查看源代码时内容区域是一片空白(因为它是标准的 SPA 单页异步渲染)。
- 企业内网系统、管理后台、门户网站:完全没问题。
- 需要搞百度/谷歌 SEO 的对外官网:千万别用思路二,老老实实用思路一(编译时连库)。
2. XSS 安全防护
由于使用了 v-html 来渲染数据库传过来的内容。如果数据库里的内容是普通用户可以自由投稿/评论输入的,必须在前端或后端引入 dompurify 来清洗 HTML,防止恶意脚本注入:
import DOMPurify from "dompurify";
const safeHtml = DOMPurify.sanitize(md.render(rawMarkdown));
如果只是公司内网几位技术专员/管理员在后台录入文档,则可以信任数据源,无需复杂清洗。
六、方案选型对照速查
| 维度 | 思路二(客户端运行时) | 思路一(编译时连库) |
|---|---|---|
| 内容更新 | 实时生效,无需重新构建 | 需要重新构建发布 |
| SEO 友好度 | 差(SPA 异步渲染) | 好(静态 HTML) |
| 首屏速度 | 略慢(需请求接口) | 快(已编译) |
| 适用场景 | 内网门户、管理后台 | 对外官网、文档站 |
| 维护成本 | 低(只维护数据库) | 中(需构建流水线) |