跳转到内容

自定义主题 ​

解析主题 ​

可以通过创建一个 .vitepress-react/theme/index.ts 文件(即“主题入口文件”)来启用自定义主题:

.
├─ docs                # 项目根目录
│  ├─ .vitepress-react
│  │  ├─ theme
│  │  │  └─ index.ts   # 主题入口
│  │  └─ config.ts     # 配置文件
│  └─ index.md
└─ package.json

当检测到存在主题入口文件时,VitePress 总会使用自定义主题而不是默认主题。但你可以扩展默认主题来在其基础上实现更高级的自定义。

本项目 的主题是 React

主题入口与组件是普通的 .tsx(React)文件,构建由 Vite 完成(TSX 自动 JSX runtime,无需额外插件)。不再有 .vue 文件或 Vue 应用实例。

主题接口 ​

VitePress 自定义主题是一个对象,该对象具有如下接口:

ts
import type { ComponentType } from 'react'
import type { Router, SiteData } from '@10coding/vitepress-react'

interface Theme {
  /** 每个页面的根布局组件(props 不限;框架以无参方式渲染) */
  Layout?: ComponentType
  /**
   * 在客户端增强应用(可异步;服务端阶段也会在 SSR 中运行,注意 `import.meta.env.SSR`)
   */
  enhanceApp?: (ctx: EnhanceAppContext) => Awaitable<void>
  /** 运行在根组件 effect 中的客户端逻辑(SSR 安全:内部请自行守卫 DOM) */
  setup?: () => void
  /** 扩展另一个主题:先执行其 enhanceApp/setup */
  extends?: Theme
}

interface EnhanceAppContext {
  router: Router // VitePress 路由实例
  siteData: SiteData // 站点级数据
}

主题入口文件需要将主题对象作为默认导出来导出。建议用 defineTheme() 包一层(或给对象字面量加 satisfies Theme)以获得类型约束——直接写裸对象只有字面量推断,拼错键名(如 compnents)、把 components 覆盖表写成非内部组件名、字段值类型不对,都不会报错:

.vitepress-react/theme/index.ts
.vitepress-react/theme/index.ts
ts
import { defineTheme } from '@10coding/vitepress-react'
import Layout from './Layout.tsx'

export default defineTheme({
  Layout,
  enhanceApp({ router }) {
    // ...
  }
})

两种等效写法:

ts
// 方式一:defineTheme(推荐;参数即 Theme,错误在编辑期提示)
export default defineTheme({ Layout, components: { VPNavBar: MyNavBar } })

// 方式二:satisfies Theme(需要 import type { Theme })
export default { Layout, components: { VPNavBar: MyNavBar } } satisfies Theme

默认导出是自定义主题的唯一方式;Layout 也是最常用的属性——从技术上讲,一个 VitePress 主题可以只是一个 React 布局组件。注意主题同样需要保证 SSR 兼容。

构建布局 ​

最基本的布局组件需要渲染 <Content />,它负责输出当前页面的 markdown 内容:

.vitepress-react/theme/Layout.tsx
.vitepress-react/theme/Layout.tsx
tsx
import { Content } from '@10coding/vitepress-react'

export default function Layout() {
  return (
    <div className="vp-layout">
      <h1>Custom Layout!</h1>
      <Content />
    </div>
  )
}

上面的布局只是把每个页面的 markdown 渲染为 HTML。我们添加的第一个改进是处理 404 错误:

.vitepress-react/theme/Layout.tsx
.vitepress-react/theme/Layout.tsx
tsx
import { Content, useData } from '@10coding/vitepress-react'

export default function Layout() {
  const { page } = useData()

  if (page.isNotFound) {
    return (
      <div className="vp-layout">
        <h1>Custom Layout!</h1>
        <p>Custom 404 page!</p>
      </div>
    )
  }
  return (
    <div className="vp-layout">
      <h1>Custom Layout!</h1>
      <Content />
    </div>
  )
}

useData() 提供了全部运行时数据(返回值是当前快照的普通值,不是响应式包装),方便根据条件渲染不同布局。另一个常用字段是当前页面的 frontmatter——借助它可以让用户通过 frontmatter 控制每页布局,例如标记某页使用特殊首页布局:

md
---
layout: home
---

主题据此分支渲染:

.vitepress-react/theme/Layout.tsx
.vitepress-react/theme/Layout.tsx
tsx
import { Content, useData } from '@10coding/vitepress-react'

export default function Layout() {
  const { page, frontmatter } = useData()

  if (page.isNotFound) {
    return (
      <div className="vp-layout">
        <h1>Custom Layout!</h1>
        <p>Custom 404 page!</p>
      </div>
    )
  }
  if (frontmatter.layout === 'home') {
    return (
      <div className="vp-layout">
        <h1>Custom Layout!</h1>
        <p>Custom home page!</p>
      </div>
    )
  }
  return (
    <div className="vp-layout">
      <h1>Custom Layout!</h1>
      <Content />
    </div>
  )
}

当然你可以把布局拆成多个组件:

.vitepress-react/theme/Layout.tsx
.vitepress-react/theme/Layout.tsx
tsx
import { useData } from '@10coding/vitepress-react'
import NotFound from './NotFound.tsx'
import Home from './Home.tsx'
import Page from './Page.tsx'

export default function Layout() {
  const { page, frontmatter } = useData()
  return (
    <div className="vp-layout">
      <h1>Custom Layout!</h1>
      {page.isNotFound ? (
        <NotFound />
      ) : frontmatter.layout === 'home' ? (
        <Home />
      ) : (
        <Page />
      )}
    </div>
  )
}
.vitepress-react/theme/Page.tsx
.vitepress-react/theme/Page.tsx
tsx
import { Content } from '@10coding/vitepress-react'

export default function Page() {
  return <Content />
}

请查看运行时 API 参考获取主题组件中所有可用的内容。此外,可以利用构建时数据加载生成数据驱动布局——例如,一个列出当前项目中所有文章入口的页面。

基于默认主题布局组合 ​

不必从零自绘:默认主题的 Layout 是自包含的普通 React 组件(内部自己读取数据并渲染 SkipLink/导航/侧栏/内容/页脚),可以直接从 @10coding/vitepress-react/theme 导入,在自己的主题 Layout 里按条件整页复用,只对特定页面走自定义分支。

.vitepress-react/theme/Layout.tsx
.vitepress-react/theme/Layout.tsx
tsx
import Theme from '@10coding/vitepress-react/theme'
import { Content, useData } from '@10coding/vitepress-react'

export default function CustomLayout() {
  const { frontmatter } = useData()

  // 未打标的页面 → 整套复用默认主题布局(home / doc / layout:false 等
  // 分流默认主题已内部处理,不需要重复实现)
  if (frontmatter.layout !== 'custom') {
    return <Theme.Layout />
  }

  // frontmatter 打上 layout: custom 的页面 → 自绘
  return (
    <div className="vp-layout">
      <h1>Custom Layout!</h1>
      <Content />
    </div>
  )
}

对应页面在 frontmatter 里标记:

md
---
layout: custom
---

接线时用 extends 继承默认主题的其余能力,再覆盖 Layout:

.vitepress-react/theme/index.ts
.vitepress-react/theme/index.ts
ts
import { defineTheme } from '@10coding/vitepress-react'
import Theme from '@10coding/vitepress-react/theme'
import CustomLayout from './Layout.tsx'

export default defineTheme({
  extends: Theme, // 继承默认主题其余字段;enhanceApp 会 base-first 链式执行
  Layout: CustomLayout
})

几个注意点:

  • 导入来源:Layout 不在 @10coding/vitepress-react 根导出里(那里只有 useData/Content 等);默认主题要写 @10coding/vitepress-react/theme。
  • 组合粒度:默认 Layout 是自包含组件、不接受 children,但它提供与 Vue 上游对齐的具名插槽 props(camelCase,如 asideOutlineBefore/layoutTop),可不动默认结构在指定位置注入内容;默认主题内部组合树还可用 Theme.components 注册表按名覆盖(含叶子组件)。两者详见扩展默认主题。若连插槽与注册表都不够(要改变整体骨架),再整页自绘或复制一份布局组件自己拼(上面的构建布局列了全部可拆分部件思路)。
  • 在默认布局外面再包一层(如全站顶部横幅)是允许的:把 <Theme.Layout /> 放进自己的容器即可。
  • 404:本项目 已废弃 Theme.NotFound,按 page.isNotFound 分支(参考构建布局里的 404 处理);不特殊处理时让它走 <Theme.Layout /> 也可以。

分发自定义主题 ​

分发自定义主题最简单的方式是将其作为 GitHub 模版仓库。

如果希望将主题作为 npm 包分发,请按下面的步骤:

  1. 在包入口把主题对象作为默认导出(文件为 .ts/.tsx)。

  2. 如果合适,把主题配置类型作为 ThemeConfig 导出。

  3. 如果主题需要调整 VitePress 配置,请在包的子路径下(例如 my-theme/config)导出该配置,以便用户扩展。

  4. 记录主题配置选项(配置文件与 frontmatter 两处)。

  5. 提供清晰的使用说明(见下节)。

使用自定义主题 ​

要使用外部主题,请导入它并重新导出:

.vitepress-react/theme/index.ts
.vitepress-react/theme/index.ts
ts
import Theme from 'awesome-vitepress-theme'

export default Theme

如果主题需要扩展:

.vitepress-react/theme/index.ts
.vitepress-react/theme/index.ts
ts
import Theme from 'awesome-vitepress-theme'

export default {
  ...Theme,
  enhanceApp(ctx) {
    // ...
  }
}

注意:本项目的主题对象用对象展开/覆盖组合(extends 主题字段也可用,语义见主题接口),不是 Vue 的“extends 组件再包装”那套写法。

如果主题需要特殊的 VitePress 配置,在站点配置中扩展它:

.vitepress-react/config.ts
.vitepress-react/config.ts
ts
import baseConfig from 'awesome-vitepress-theme/config'

export default {
  extends: baseConfig
}

如果主题提供了 ThemeConfig 类型:

.vitepress-react/config.ts
.vitepress-react/config.ts
ts
import baseConfig from 'awesome-vitepress-theme/config'
import { defineConfig } from '@10coding/vitepress-react'
import type { ThemeConfig } from 'awesome-vitepress-theme'

export default defineConfig({
  extends: baseConfig,
  themeConfig: {
    // 类型为 ThemeConfig
  } as ThemeConfig
})