硅基俊宁.
Skills · 技能体系 ↗
← 返回文章

为什么选 Fumadocs:Node.js 项目接入开箱即用文档站

俊宁 · 2026-07-08
迁自历史博客,保留原文、署名、引用与原始日期;文中的版本、成果与性能结论不代表重新完成了当前验收。
查看原始出处 ↗ · 查看迁移清单
为什么选 Fumadocs:Node.js 项目接入开箱即用文档站封面

后端服务到了一定规模,文档就成了刚需。不是给外部用户的 README,而是给自己人的:新成员怎么跑起来、接口入参是什么、数据库里几张表是怎么关联的。

我负责的这个 Node.js 后端服务跑着一套自动化平台,随着接口越来越多,"靠脑子记"已经不够了。于是我决定正经做一个内嵌文档站,要求只有一个:好看,且不要花时间调样式。

一句话总结

Fumadocs 是目前我见过最开箱即用的 Node.js 文档框架——接入 OpenAPI 两行命令就能出接口文档,颜值不需要额外调,跟 Midway.js / Egg.js 静态托管的结合也很自然。


选型过程:为什么不选其他的

我评估了以下几个方案:

框架技术栈OpenAPI 支持颜值开箱备注
VitePressVue 3无官方插件中等适合纯文档,定制自由度高但要花时间
DocusaurusReactdocusaurus-plugin-openapi-docs一般生态成熟,但 OpenAPI 插件渲染效果不够精致
NextraNext.js有,需要配置中等和 Next.js 绑定深,学习成本适中
MintlifySaaS一流最好看需要联网托管,私有项目不适合
FumadocsNext.jsfumadocs-openapi,官方维护一流可自部署,颜值对标 Mintlify

Mintlify 的 OpenAPI 渲染是业内标杆,但它是 SaaS——文档得推到他们的服务器,对内部项目不合适。

Fumadocs 是我找到的唯一一个可以自部署、OpenAPI 一流、不需要调样式就好看的方案。


Fumadocs 的颜值从何而来

设计系统内置

Fumadocs UI 本身是 Tailwind CSS v4 驱动的设计系统,所有颜色、排版、间距都基于 CSS 变量。导入一行 CSS 就能用:

/* app/globals.css */
@import 'fumadocs-ui/style.css';
@import 'tailwindcss';

不需要写主题配置,深色模式自动支持,字体和间距比例都对。

文档布局开箱可用

侧边栏、TOC(目录)、面包屑、代码高亮——这些在大多数框架里都要自己拼,Fumadocs 全部内置:

// app/docs/layout.tsx
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { source } from '@/lib/source';

export default function Layout({ children }) {
  return (
    <DocsLayout tree={source.pageTree} nav={{ title: '我的项目' }}>
      {children}
    </DocsLayout>
  );
}

这就是文档布局的全部代码。侧边栏自动从 content/docs/ 的目录结构生成,meta.json 控制排序和分组。

MDX 组件

<Cards>、<Card>、<Callout>、<Steps> 这些常见 MDX 组件全部内置,写文档时直接用,不需要自己实现。


fumadocs-openapi:两行命令出接口文档

这是我最看重的功能。维护一份 openapi.yaml,然后:

# 安装
npm install fumadocs-openapi

# 从 YAML 生成 MDX 页面
npx fumadocs-openapi generate -i openapi/my-api.yaml -o content/docs/api

生成的每个接口都是一个独立的 MDX 页面,包含:

效果对标 Mintlify 和 Swagger UI Pro,但完全自部署。

OpenAPI spec 的骨架是这样的:

# openapi/my-api.yaml
openapi: 3.0.3
info:
  title: My API
  version: 1.0.0
paths:
  /api/skills:
    get:
      summary: 获取技能列表
      tags: [Skills]
      parameters:
        - name: projectKey
          in: query
          schema:
            type: string
      responses:
        '200':
          description: 技能列表
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Skill'

写一次 spec,既能生成文档,将来也能用于接口测试和 Mock Server——比手写 Markdown 维护成本低。


怎么和 Midway.js / Egg.js 结合

这里有个有意思的架构问题:文档站是 Next.js 项目,服务端是 Midway.js(Egg.js 底层)。两个进程,怎么变成"一个项目"?

静态导出 + egg-static 托管

Next.js 支持 output: 'export',构建后输出纯静态文件:

// client/next.config.mjs
const config = {
  output: 'export',      // 输出静态 HTML/JS/CSS,不需要 Node.js 运行时
  images: { unoptimized: true },
};

Egg.js 内置 egg-static 插件,开启后可以托管静态目录:

// src/config/config.default.ts
import { join } from 'path';

static: {
  prefix: '/',
  dir: [join(appInfo.appDir, 'client', 'out')],  // Next.js 导出目录
  dynamic: true,
  preload: false,
},
// src/config/plugin.ts
static: {
  enable: true,
  package: 'egg-static',
},

请求流程:

HTTP 请求
  ↓
egg-static(命中 client/out/ 静态文件 → 直接返回 HTML/JS/CSS)
  ↓
Midway 路由(没有匹配的静态文件 → 走 API 控制器)

也就是说,GET / 返回文档站首页,GET /api/skills 走 Midway 控制器——静态文件和 API 共用同一个端口,完全无感。

本地开发:一个命令同时跑两个进程

生产环境用静态导出。本地开发时,Next.js 需要一个 dev server 做热更新,不能用静态文件。用 concurrently 解决:

// package.json(项目根目录)
{
  "scripts": {
    "dev": "concurrently --names 'server,docs' --prefix-colors 'cyan,magenta' --kill-others-on-fail 'npm run dev:server' 'npm run dev:docs'",
    "dev:server": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js",
    "dev:docs": "npm --prefix client run dev"
  }
}

npm run dev 之后,终端里:

Home controller 做一个简单的判断:本地环境没有 client/out/,就重定向到 :3000;其他环境如果 build 缺失就显示一个保底状态页。

// src/controller/home.ts
const CLIENT_INDEX = join(__dirname, '../../client/out/index.html');

@Get('/')
async home() {
  if (existsSync(CLIENT_INDEX)) {
    this.ctx.type = 'html';
    return readFileSync(CLIENT_INDEX, 'utf-8');
  }
  if (this.ctx.app.config.env === 'local') {
    this.ctx.redirect('http://localhost:3000');
    return;
  }
  // 返回保底状态页
}

目录结构

my-server/
├── src/              # Midway.js 后端
│   ├── controller/
│   ├── service/
│   └── model/
├── client/           # Fumadocs 文档站(Next.js)
│   ├── app/
│   ├── content/docs/ # MDX 文档内容
│   ├── openapi/      # OpenAPI spec
│   └── out/          # next build 产物(被 egg-static 服务)
└── package.json      # dev/build 统一入口

版本对齐的坑

Fumadocs 各包的 peer dependency 比较严格,截至写作时:

包版本备注
fumadocs-core^16.9.0
fumadocs-mdx^15.0.0注意不是 ^12,v15 才支持 core v16
fumadocs-ui^16.0.0
fumadocs-openapi^10.0.0要求 core ^16.9、ui ^16.9
tailwindcss^4.0.0v16 ui 内置 Tailwind v4,不再用 v3 preset

fumadocs-mdx 对 Next.js 16 有 optional peer dep,安装时加 --legacy-peer-deps 即可,不影响 Next.js 15 运行。

CSS 配置从 Tailwind v3 的 tailwind.config.ts 改成 v4 的 @import 方式:

@import 'fumadocs-ui/style.css';  /* Fumadocs 设计系统 + Tailwind v4 */
@import 'tailwindcss';             /* 补充 utility classes */
@source '../app/**/*.{ts,tsx}';
@source '../content/**/*.{md,mdx}';

要不要用 / 我的判断框架

适合用 Fumadocs 的场景:

不适合的场景:

我的判断:值得上。特别是如果你的项目已经在写 OpenAPI spec,fumadocs-openapi 自动生成这一步能省很多时间,比手写 Markdown 维护成本低,比 Swagger UI 好看一个量级。