跳至正文
来两杯美式
返回

VitePress 动态渲染全栈落地方案

By 来两杯美式
发布于更新于

思路二(客户端运行时动态请求接口) 的完整落地方案。

最核心的诉求:在用户访问页面时,实时去后端获取 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,字段类型强烈建议使用 LONGTEXTMEDIUMTEXT,以防文档过长被截断;另外建议加上特定的状态控制:

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 单页异步渲染)。

2. XSS 安全防护

由于使用了 v-html 来渲染数据库传过来的内容。如果数据库里的内容是普通用户可以自由投稿/评论输入的,必须在前端或后端引入 dompurify 来清洗 HTML,防止恶意脚本注入:

import DOMPurify from "dompurify";
const safeHtml = DOMPurify.sanitize(md.render(rawMarkdown));

如果只是公司内网几位技术专员/管理员在后台录入文档,则可以信任数据源,无需复杂清洗。


六、方案选型对照速查

维度思路二(客户端运行时)思路一(编译时连库)
内容更新实时生效,无需重新构建需要重新构建发布
SEO 友好度差(SPA 异步渲染)好(静态 HTML)
首屏速度略慢(需请求接口)快(已编译)
适用场景内网门户、管理后台对外官网、文档站
维护成本低(只维护数据库)中(需构建流水线)

分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
查看系列全部文章
  1. 01.T3 Stack 开发规范
  2. 02.Next.js 并行路由完全指南
  3. 03.Web 会话与身份验证完整指南
  4. 04.Auth Guard 与 tRPC 中间件
  5. 05.T3 Stack 会话认证指南
  6. 06.Next.js 子域名、反向代理与分享链路最佳实践
  7. 07.CORS 与 Web 安全实战白皮书
  8. 08.Web 多主题架构最佳实践
  9. 09.Vercel AI SDK 自定义 OpenAI 模型接入
  10. 10.AI 驱动动态看板架构指南
  11. 11.VitePress 动态渲染全栈落地方案

上一篇
微服务限流与容错:雪崩效应、熔断器与三种限流算法
下一篇
AI 驱动动态看板架构指南