Skip to content

开发思路

摘要

只要会编写 Vue 组件,那么就可以发挥您天马行空的想象力,来构建自己的主题。

2025-03-17 @Teek

本系列为 主题开发,主要介绍 Teek 的开发思路,当然这只是提供思路,不会细到每一个文件的逻辑讲解。

在阅读完本系列内容后,您可以去阅读 Teek 的源码,了解 Teek 的实现思路,也许对您的主题开发之路有些帮助。

基于 VitePress 开发一个主题是非常简单的,在 自定义主题拓展默认主题 已经详细的介绍了如何开发一个主题。

Layout 函数

VitePress 默认内置了一套主题,如果觉得内置主题的功能不满足需求或者想额外添加一些功能,可以编写组件来替换/拓展 VitePress 主题。

首先 VitePress 必须 需要接收一个 Layout 函数,该函数需要返回一个 vue 组件作为 入口组件

ts
import DefaultTheme from "vitepress/theme";

export default {
  extends: DefaultTheme,
  Layout: /* Vue 组件 */,
  enhanceApp({ app, router, siteData }) {},
};

Layout 实现一个组件主要有 2 种方式:

  1. h 函数 + .vue 组件
ts
import DefaultTheme from "vitepress/theme";
import MyComponent from "./MyComponent.vue";
import { h } from "vue";

export default {
  extends: DefaultTheme,
  Layout: () => h(MyComponent),
  enhanceApp({ app, router, siteData }) {},
};
  1. defineComponent 函数生成 vue 组件
ts
import DefaultTheme from "vitepress/theme";
import MyComponent from "./MyComponent.vue";
import { h } from "vue";

export default {
  extends: DefaultTheme,
  Layout: defineComponent({
    name: "ConfigProvider",
    setup(_, { slots }) {
      // 自定义一些全局逻辑

      return () => h(MyComponent, null, slots);
    },
  }),
  enhanceApp({ app, router, siteData }) {},
};

可以看到,defineComponent 函数的返回值还是使用了 h 函数 + .vue 组件,但是这样好处在于可以添加一些全局逻辑或往所有组件里注入常用数据,因为这是在所有组件加载前执行的逻辑。

比如 Teek 注入了文章信息数据、并开启一些监听器:

ts
import DefaultTheme from "vitepress/theme";
import MyComponent from "./MyComponent.vue";
import { h, type Component } from "vue";

const configProvider = (Layout: Component) => {
  return defineComponent({
    name: "ConfigProvider",
    setup(_, { slots }) {
      const { theme } = useData();
      // 往主题注入数据
      provide(postsContext, theme.value.posts);

      // 开启监听器
      usePermalink().startWatch();
      useAnchorScroll().startWatch();
      useViewTransition();

      return () => h(Layout, null, slots);
    },
  });
};

export default {
  extends: DefaultTheme,
  Layout: configProvider(MyComponent),
  enhanceApp({ app, router, siteData }) {},
};

相比较直接用 h 函数来构建组件,defineComponent 函数更灵活,多了一个中间层方便实现一些逻辑。

入口组件

阅读内容前,您需要了解 VitePress 提供了哪些插槽,请看 布局插槽

Teek 并不是完全脱离 VitePress 的主题,而是基于 VitePress 的主题开发,所以 Teek 在入口组件里继承 VitePress 的 Layout 组件,并通过 VitePress 提供的插槽来实现功能。

Teek 的入口文件伴随着迭代,已经有很多内容产出,这里给出 Teek 早期的模板:

vue
<script setup lang="ts" name="TeekLayout">
import DefaultTheme from "vitepress/theme";
import { useData } from "vitepress";

const { theme } = useData();
const { teekTheme = true } = theme.value;

// 维护已使用的插槽,防止外界传来的插槽覆盖已使用的插槽
const usedSlots = [
  "home-hero-before",
  "nav-bar-content-after",
  // 其他模板里已使用的插槽 ...
];
</script>

<template>
  <!-- 使用 Teek 主题 -->
  <template v-if="teekTheme">
    <DefaultTheme.Layout>
      <template #home-hero-before>
        <slot name="home-hero-before" />
        <!-- 自定义首页 -->
      </template>

      <!-- 在 VP 的不同插槽自定义不同内容 -->
      <template #xx></template>

      <!-- 未使用的其他 VP 插槽 -->
      <template
        v-for="name in Object.keys($slots).filter(name => !usedSlots.includes(name))"
        :key="name"
        #[name]="slotData"
      >
        <slot :name="name" v-bind="slotData"></slot>
      </template>
    </DefaultTheme.Layout>
  </template>

  <template v-else>
    <DefaultTheme.Layout>
      <template v-for="(_, name) in $slots" :key="name" #[name]="slotData">
        <slot :name="name" v-bind="slotData"></slot>
      </template>
    </DefaultTheme.Layout>
  </template>
</template>

Teek 从 theme 中取出一个配置项 teekTheme,如果为 true,则使用 Teek 主题,否则使用 VitePress 的默认主题。

提示

theme 为项目里 .vitepress/config.mts 里的 themeConfig 内容。

如果您完全不需要基于 VitePress 的主题开发,则不需要使用 DefaultTheme.Layout 组件,直接在该组件写入自己的内容即可,这也意味着您只是基于 Vite 环境构建您的专属风格,您将自己实现首页、侧边栏、导航栏、CSS 样式等,只有 Markdown 解析的内容不需要自己实现,VitePress 已经提供了全局组件 <Content /> 来渲染 Markdown 内容。

提示

如果想完全脱离 VitePress 主题,在 Layout 函数处不要添加 extends: DefaultTheme

在模板里可以看到这样两段代码:

vue
<template v-for="name in Object.keys($slots).filter(name => !usedSlot.includes(name))" :key="name" #[name]="slotData">
  <slot :name="name" v-bind="slotData"></slot>
</template>

<template v-for="(_, name) in $slots" :key="name" #[name]="slotData">
  <slot :name="name" v-bind="slotData"></slot>
</template>
  • 第一段代码:Teek 不仅自己使用 VitePress 的插槽,同时也允许用户使用 VitePress 的插槽,所以 Teek 先维护了已使用的插槽列表,然后通过了 v-for 遍历所有 未使用 VitePress 的插槽,并使用 #[name]="slotData" 将插槽内容传递给 VitePress
  • 第二段代码:当不使用 Teek 主题时,则加载 VitePress 的默认主题,并使用 v-for 遍历所有插槽,将插槽内容传递给 VitePress。

如果不通过 for 循环,那么就需要这样写:

vue
<template>
  <Layout>
    <!-- layout -->
    <template #layout-top>
      <slot name="layout-top" />
    </template>
    <template #layout-bottom>
      <slot name="layout-bottom" />
    </template>
    <!-- navbar -->
    <template #nav-bar-title-before>
      <slot name="nav-bar-title-before" />
    </template>
    <template #nav-bar-title-after>
      <slot name="nav-bar-title-after" />
    </template>
    <template #nav-bar-content-before>
      <slot name="nav-bar-content-before" />
    </template>
    <template #nav-bar-content-after>
      <slot name="nav-bar-content-after" />
    </template>
    <template #nav-screen-content-before>
      <slot name="nav-screen-content-before" />
    </template>
    <template #nav-screen-content-after>
      <slot name="nav-screen-content-after" />
    </template>
    <!-- sidebar -->
    <template #sidebar-nav-before>
      <slot name="sidebar-nav-before" />
    </template>
    <template #sidebar-nav-after>
      <slot name="sidebar-nav-after" />
    </template>
    <!-- page -->
    <template #page-top>
      <slot name="page-top" />
    </template>
    <template #page-bottom>
      <slot name="page-bottom" />
    </template>
    <!-- 404 -->
    <template #not-found>
      <slot name="not-found" />
    </template>
    <!-- home -->
    <template #home-hero-info>
      <slot name="home-hero-info" />
    </template>
    <template #home-hero-image>
      <slot name="home-hero-image" />
    </template>
    <template #home-hero-before>
      <slot name="home-hero-before" />
    </template>
    <template #home-hero-after>
      <slot name="home-hero-after" />
    </template>
    <template #home-features-before>
      <slot name="home-features-before" />
    </template>
    <template #home-features-after>
      <slot name="home-features-after" />
    </template>
    <!-- doc -->
    <template #doc-footer-before>
      <slot name="doc-footer-before" />
    </template>
    <template #doc-before>
      <slot name="doc-before" />
    </template>
    <template #doc-after>
      <slot name="doc-after" />
    </template>
    <template #doc-top>
      <slot name="doc-top" />
    </template>
    <template #doc-bottom>
      <slot name="doc-bottom" />
    </template>
    <!-- aside -->
    <template #aside-top>
      <slot name="aside-top" />
    </template>
    <template #aside-bottom>
      <slot name="aside-bottom" />
    </template>
    <template #aside-outline-before>
      <slot name="aside-outline-before" />
    </template>
    <template #aside-outline-after>
      <slot name="aside-outline-after" />
    </template>
    <template #aside-ads-before>
      <slot name="aside-ads-before" />
    </template>
    <template #aside-ads-after>
      <slot name="aside-ads-after" />
    </template>
  </Layout>
</template>

可以看到 VitePress 提供的插槽非常多,这样写起来非常麻烦,因此建议使用 for 循环方式。

接下来就可以根据自己的需求来写组件,然后传入 VitePress 的插槽中,Teek 也是在模板里不断补充内容才达到现在的效果。

假设您已经自定义了首页和评论区组件,则需要传入 VitePress 的插槽中,内容如下:

vue
<script setup lang="ts" name="TeekLayout">
import DefaultTheme from "vitepress/theme";
import { useData } from "vitepress";
import HomePost from "./components/HomePost.vue"; 
import Comment from "./components/Comment.vue";

const { Layout } = DefaultTheme;
const { theme } = useData();
const { teekTheme = true } = theme.value;

// 维护已使用的插槽,防止外界传来的插槽覆盖已使用的插槽
const usedSlots = [
  "home-hero-before",
  "nav-bar-content-after",
  // 其他模板里已使用的插槽 ...
];
</script>

<template>
  <!-- 使用 Teek 主题 -->
  <template v-if="teekTheme">
    <Layout>
      <template #home-hero-before>
        <slot name="home-hero-before" />
        <!-- 自定义首页 -->
        <HomePost />
      </template>

      <template #doc-after>
        <slot name="doc-after" />
        <!-- 自定义评论区 -->
        <Comment />
      </template>

      <!-- 未使用的其他 VP 插槽 -->
      <template
        v-for="name in Object.keys($slots).filter(name => !usedSlots.includes(name))"
        :key="name"
        #[name]="slotData"
      >
        <slot :name="name" v-bind="slotData"></slot>
      </template>
    </Layout>
  </template>

  <template v-else>
    <Layout>
      <template v-for="(_, name) in $slots" :key="name" #[name]="slotData">
        <slot :name="name" v-bind="slotData"></slot>
      </template>
    </Layout>
  </template>
</template>

如果您不了解每个插槽分别作用于什么位置,可以在每个插槽里加一段文字如 <div>${插槽名}</div>,然后在页面查看输出的内容。

引入主题

假设您已经开发好了一个主题,则需要在项目的 .vitepress/theme/index.ts 文件中引入,如果没有请按照该路径依次创建。

ts
import Teek from "vitepress-theme-teek";

export default {
  extends: Teek,
};

此时项目成功使用你的自定义主题。

最近更新