开发思路
摘要
只要会编写 Vue
组件,那么就可以发挥您天马行空的想象力,来构建自己的主题。
2025-03-17 @Teek
本系列为 主题开发,主要介绍 Teek 的开发思路,当然这只是提供思路,不会细到每一个文件的逻辑讲解。
在阅读完本系列内容后,您可以去阅读 Teek 的源码,了解 Teek 的实现思路,也许对您的主题开发之路有些帮助。
基于 VitePress 开发一个主题是非常简单的,在 自定义主题 和 拓展默认主题 已经详细的介绍了如何开发一个主题。
Layout 函数
VitePress 默认内置了一套主题,如果觉得内置主题的功能不满足需求或者想额外添加一些功能,可以编写组件来替换/拓展 VitePress 主题。
首先 VitePress 必须 需要接收一个 Layout
函数,该函数需要返回一个 vue
组件作为 入口组件:
import DefaultTheme from "vitepress/theme";
export default {
extends: DefaultTheme,
Layout: /* Vue 组件 */,
enhanceApp({ app, router, siteData }) {},
};
在 Layout
实现一个组件主要有 2 种方式:
h
函数 +.vue
组件
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 }) {},
};
defineComponent
函数生成vue
组件
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 注入了文章信息数据、并开启一些监听器:
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 早期的模板:
<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
在模板里可以看到这样两段代码:
<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
循环,那么就需要这样写:
<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 的插槽中,内容如下:
<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
文件中引入,如果没有请按照该路径依次创建。
import Teek from "vitepress-theme-teek";
export default {
extends: Teek,
};
此时项目成功使用你的自定义主题。