How to write a theme?
TIP提示
Valaxy 与 Vite/Vue 的生态完全兼容,因此你在编写主题时,可以任意使用第三方的 Vite
/Vue
插件。
Valaxy 主题无需预编译,直接发布源文件即可。
撰写中…
作为 Valaxy 作者,我可以很轻松的实现自己的主题。 但也因此,我可能很难了解真正主题开发者的需求。
因此,如果你有任何开发主题的相关问题, 可前往 QQ 频道「云乐坊」 或发起 Discussions 与我交流,我将会为您提供尽可能的帮助,并针对泛化的问题撰写文档。
对了,由于目前的主题并不多,主题作者可以在这里发现一些来自云游君私人的奖励。
主题示例
- valaxy-theme-starter: Valaxy 主题开发模版
- valaxy-theme-yun: valaxy-theme-yun 一个更完善的主题示例
- valaxy-theme-press: valaxy-theme-press 当前文档主题示例
创建主题模板
# 使用 valaxy-theme-starter 模版
pnpm create valaxy
# choose Theme
在动手之前,我们先来了解一下一个 Valaxy 主题的基础结构,它与正常的用户目录结构也十分相似。
以 valaxy-theme-yun 为例:
尽管它们看起来很多,但是大部分都是可选的,你可以根据主题的需求按需编写。
App.vue
: 主题的入口文件,用于挂载全局的主题组件README.md
: 主题的说明文档(毫无疑问,这是必不可少的 😛)client
:主题所暴露给用户的客户端辅助函数index.ts
: 主题的客户端辅助函数入口文件
components
: 主题的组件ValaxyMain.vue
: 主题的文章渲染组件YunSidebar.vue
: 主题的侧边栏组件YunSponsor.vue
: 主题的赞助组件YunWaline.vue
: 第三方评论 Waline 适配组件
composables
: 辅助的 Composition APIconfig.ts
: 主题的配置文件helper.ts
: 主题的辅助函数index.ts
: 主题的 Composition API 入口文件post.ts
: 主题的文章相关的辅助函数
docs
: 主题的文档(自由用你喜欢的结构组织并展示吧!)出于定制化与 DogFooding 的考虑,Valaxy 的文档采用自身制作,并制作了一个文档主题 valaxy-theme-press,如果你只是想要一个简单轻量的文档站点,Vitepress 是个不错的选择。(valaxy-theme-starter 在未来也许会内置该示例模版。)
en-US
: 英文文档zh-CN
: 中文文档
features
: 主题特色功能,一些不依赖于 Vue Composition API 的功能(区别于composables
)fireworks.ts
: 烟花点击效果
layouts
: 主题的布局(扩展更多布局)default.vue
: 默认布局home.vue
: 首页布局layout.vue
: 文章列表布局post.vue
: 文章布局(放置于pages/posts/
文件夹下的文章默认为 `post 布局)tags.vue
: 标签布局
locales
: 主题的多语言支持en.yml
: 英文语言文件zh-CN.yml
: 中文语言文件
node_modules
: 主题的依赖(请勿提交至仓库)node
: 主题的 Node 端逻辑package.json
: 主题的相关信息与依赖pages
: 主题的默认页面(扩展更多页面)index.vue
: 首页page
: 普通页[page].vue
: 文章列表页,动态路由,如/page/2
setup
: 主题的入口文件(可注册 Vue 插件等)main.ts
: 主入口文件defineAppSetup
stores
: 主题的状态管理app.ts
: 全局状态管理文件
styles
: 主题的样式(css-vars.scss
与index.scss
将会被自动引入)css-vars.scss
: 主题的 CSS 变量index.scss
: 主题的样式入口文件
tsconfig.json
: 主题的 TypeScript 配置types
: 主题的类型声明index.d.ts
: 主题的类型声明入口文件
unocss.config.ts
: 主题的 unocss 配置utils
: 主题的工具函数valaxy.config.ts
: 主题的配置文件
APIs
我们提供了一个扩展函数,以供你快速扩展页面信息。
你也可以直接扩展 unplugin-vue-router 插件中的 extendRoute
。
https://github.com/posva/unplugin-vue-router/issues/43#issuecomment-1433140464
// valaxy.config.ts
import { defineTheme } from 'valaxy'
export default defineTheme({
router: {
extendRoute(route) {
// want to get component absolute paths?
// const path = route.components.get('default')
console.log(route)
},
},
extendMd(ctx) {
console.log(ctx.path)
},
})
import type { EditableTreeNode } from 'unplugin-vue-router'
// provided by valaxy, just as a tip
export interface ValaxyConfig {
vue?: Parameters<typeof Vue>[0]
components?: Parameters<typeof Components>[0]
unocss?: UnoCSSConfig
pages?: Parameters<typeof Pages>[0]
extendMd?: (ctx: {
route: EditableTreeNode
data: Readonly<Record<string, any>>
excerpt?: string
path: string
}) => void
}
TIP提示
data
解析自 Markdown frontmatter,为原始数据(不可变),将会被合并至 route.meta.frontmatter
中。
Client
Toggle Dark
切换亮暗模式
The following variables are stored in global state, which you can get through useAppStore
.
isDark
: Whether dark mode is enabledthemeColor
: Theme color (followsisDark
)toggleDark
: Toggle dark modetoggleDarkWithTransition
: Toggle dark mode with transition
以下变量被存储在全局状态中,你可以通过 useAppStore
获取。
isDark
: 是否启用了暗黑模式themeColor
: 主题色(可跟随 isDark 变化)toggleDark
: 切换暗黑模式toggleDarkWithTransition
: 带有过渡效果的切换暗黑模式
<script lang="ts" setup>
import { useAppStore } from 'valaxy'
const appStore = useAppStore()
</script>
<template>
<button class="yun-icon-btn" @click="app.toggleDarkWithTransition">
<div i="ri-sun-line dark:ri-moon-line" />
</button>
</template>
You can configure dark mode related options through
themeConfig.valaxyDarkOptions
.
你可以通过
themeConfig.valaxyDarkOptions
来配置暗黑模式的相关选项。
Default Theme Config.valaxyDarkOptions
import type { UseDarkOptions } from '@vueuse/core'
// eslint-disable-next-line ts/no-namespace
export namespace DefaultTheme {
export interface Config {
valaxyDarkOptions?: {
/**
* Options for `useDark`
* disableTransition default is `true`
* Its options are not computed, init when loaded.
* @see https://vueuse.org/core/useDark
* @url https://paco.me/writing/disable-theme-transitions
*
* @zh `useDark` 的选项
* disableTransition 默认为 `true`,不会进行渐变过渡,这是 VueUse 的默认行为
*/
useDarkOptions?: UseDarkOptions
/**
* Enable circle transition when toggling dark mode
* Then use `toggleDarkWithTransition` instead of `toggleDark`
* @zh 启用圆形过渡切换暗黑模式
*/
circleTransition?: boolean
/**
* Theme color
* @zh 主题色
*/
themeColor?: {
/**
* Theme color for light mode
* @zh 亮色主题色
*/
light?: string
/**
* Theme color for dark mode
* @zh 暗色主题色
*/
dark?: string
}
}
/**
* Custom header levels of outline in the aside component.
*
* @default 2
*/
outline?: number | [number, number] | 'deep' | false
}
}
Node
Hooks
开始编写
App.vue
你的入口文件
譬如我想要为主题添加一个全局的 Loading 页面。
你可以从 valaxy 导入全局状态 useAppStore
,记录 showLoading
来实现。
你也可以使用你自己的全局状态管理。参见 状态管理。
<!-- valaxy-theme-yun/App.vue -->
<script lang="ts" setup>
import { useHead } from '@unhead/vue'
import { useAppStore } from 'valaxy'
import { onMounted } from 'vue'
// ...
const app = useAppStore()
onMounted(() => {
app.showLoading = false
})
</script>
<template>
<!-- ... -->
<!-- 添加 Loading 组件,components/YunLoading.vue -->
<!-- https://github.com/YunYouJun/valaxy/blob/main/packages/valaxy-theme-yun/components/YunLoading.vue -->
<Transition name="fade">
<YunLoading v-if="app.showLoading" />
</Transition>
</template>
ValaxyMain
你需要自定义一个 ValaxyMain
组件来决定主题的文章渲染部分。
你可以从
ValaxyMain
的props
中获取frontmatter
与pageData
。
<script lang="ts" setup>
import type { PageData, Post } from 'valaxy'
defineProps<{
frontmatter: Post
data?: PageData
}>()
</script>
<template>
<main>
<slot name="main-content">
<ValaxyMd :frontmatter="frontmatter">
<slot name="main-content-md" />
<slot />
</ValaxyMd>
</slot>
</main>
</template>
样式
引入默认样式
Valaxy 提供了一些默认样式,你需要在主题中自行引入。
例如,新建 valaxy-theme-yun/setup/main.ts
:
import { defineAppSetup, scrollTo } from 'valaxy'
import { nextTick } from 'vue'
// 引入 valaxy 公共样式
import 'valaxy/client/styles/common/index.scss'
// 你也可以按需引入
// common
import 'valaxy/client/styles/common/code.scss'
import 'valaxy/client/styles/common/hamburger.scss'
import 'valaxy/client/styles/common/transition.scss'
// Markdown Style
import 'valaxy/client/styles/common/markdown.scss'
export default defineAppSetup((ctx) => {
const { router, isClient } = ctx
if (!isClient)
return
router.afterEach((to, from) => {
if (to.path !== from.path)
return
nextTick(() => {
scrollTo(document.body, to.hash, {
smooth: true,
})
})
})
})
Markdown 样式
Markdown 样式是主题呈现文章样式的部分,需要由主题自定义。
你可以参考 valaxy-theme-press 自定义 Markdown 主题的方式,见 styles/markdown.scss。
如果你想先使用常见的默认样式(后续再进行定制),你可以直接使用 star-markdown-css。 使用方式可参见 valaxy-theme-yun/styles
NProgress 进度条
内置了基础的 nprogress 样式,你可以通过覆盖 nprogress 的默认样式进行定制:
// your-theme/styles/index.scss
#nprogress {
pointer-events: none;
.bar {
background: var(--va-c-primary);
opacity: 0.75;
position: fixed;
z-index: 1024;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
}
功能
API
你还可以使用 Valaxy 内置的 API 以快速实现相关功能。
获取用户的 Valaxy Config
你可以通过内置的 useValaxyConfig
获取用户的 Valaxy 配置。
TIP提示
这部分配置与用户的 valaxy.config.ts
中的配置相对应,但它仅在客户端使用,因此并不包含 Node 端相关配置(如 vite
等)。
import { useSiteConfig, useValaxyConfig } from 'valaxy'
import { useThemeConfig } from 'valaxy-theme-custom'
const config = useValaxyConfig()
// site.config.ts or config.value.siteConfig
const siteConfig = useSiteConfig()
// theme.config.ts or config.value.themeConfig
const themeConfig = useThemeConfig()
提供 Typed useThemeConfig
你可以提供一个主题的 useThemeConfig
函数,以便自己/用户获得带有类型约束的配置。
// custom your theme type
import type { YunTheme } from '../types'
// composables/config.ts
import { useValaxyConfig } from 'valaxy'
/**
* getThemeConfig
*/
export function useThemeConfig<ThemeConfig = YunTheme.Config>() {
const config = useValaxyConfig<ThemeConfig>()
return computed(() => config!.value.themeConfig)
}
// use
import { useThemeConfig } from 'valaxy-theme-custom'
const themeConfig = useThemeConfig()
获取文章列表
获取文章列表有两种方式。
usePostList
: 获取文章列表(不推荐)
import { usePostList } from 'valaxy'
const postList = usePostList()
useSiteStore
: 获取全局站点信息(推荐)
const site = useSiteStore()
// site.postList
以上两者之间的区别是,usePostList
是一个基础函数,每次调用都会获取所有文章并重新过滤一次,而 useSiteStore
则会先调用 usePostList
并将获取的文章列表缓存在全局的状态中,以供你后续调用。
(此外,useSiteStore
还实现了保存文章时(如标题)热更新列表信息的功能。)
valaxy/packages/valaxy-theme-yun/components/YunPostList.vue 是一个使用
useSiteStore
展示文章列表的示例。 分页功能可参考 valaxy-theme-yun/pages/page/[page].vue 与 valaxy-theme-yun/components/YunPostList.vue。
获取文章分类与标签
在你获取文章列表后,site.postList
中的每篇文章都具有 categories
(分类) 与 tags
(标签) 属性。
你还可以通过 useCategories
与 useTags
获取所有分类、标签,其中便包含了与文章的对应关系。
import { useCategories, useTags } from 'valaxy'
const categories = useCategories()
const tags = useTags()
- valaxy/packages/valaxy-theme-yun/layouts/categories.vue 是一个使用
useCategories
展示文章分类的示例。 - valaxy/packages/valaxy-theme-yun/layouts/tags.vue 是一个使用
useTags
展示文章标签的示例。(useYunTags
是主题对useTags
的封裝。)
useTags
中的tags
为一个对象,其键为标签名,值为对应的文章列表。useCategories
可传入参数category
(useCategories('aaa')
) 以获取指定分类的文章列表。
获取 Front-matter
你可以通过 useFrontmatter
获取当前页面的 Front-matter。
譬如:
<script lang="ts" setup>
import { useFrontmatter } from 'valaxy'
const fm = useFrontmatter()
</script>
<template>
<h1>{{ fm.title }}</h1>
</template>
全局状态管理
你可以借助 Pinia (Valaxy 内置)建立自己的全局状态,并在随后使用它,
// stores/app.ts
import { acceptHMRUpdate, defineStore } from 'pinia'
// custom your theme name
export const useYunAppStore = defineStore('yun-app', () => {
// global cache for yun
return {}
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useYunAppStore, import.meta.hot))
// where you want to use
// components/YunExample.vue
import { useYunAppStore } from '../stores/app'
const yun = useYunAppStore()
上一篇/下一篇
文章底部通常存在切换上一篇/下一篇的导航。
你可以利用 siteStore.postList
自行实现,也可以使用 Valaxy 提供的 usePrevNext
。
import { usePrevNext } from 'valaxy'
const [prev, next] = usePrevNext()
// prev/next type is PostFrontMatter
// prev.title prev.path
目录
如果你想要快速实现一个目录,Valaxy 提供了一个内置钩子函数 useOutline
。
你可以用它快速获取文章页的目录信息 headers
与对应点击事件 handleClick
,如:
<script setup lang="ts">
import { useOutline } from 'valaxy'
const { headers, handleClick } = useOutline()
</script>
<template>
<nav aria-labelledby="doc-outline-aria-label">
<span id="doc-outline-aria-label" class="visually-hidden">
Table of Contents
</span>
<PressOutlineItem
class="va-toc relative z-1 css-i18n-toc"
:headers="headers"
:on-click="handleClick"
root
/>
</nav>
</template>
引用静态资源
当主题需要内置一些静态资源(如:图片等),你可以通过相对引用的方式实现。(这在 scss
样式文件中也适用)
譬如 assets
与 components
处于同一目录下时:
<!-- assets/images/valaxy-logo.png -->
<!-- components/ValaxyLogo.vue -->
<script lang="ts" setup>
import valaxyLogoPng from '../assets/images/valaxy-logo.png'
</script>
<template>
<img max-w="50" m="auto" :src="valaxyLogoPng" alt="Valaxy Logo" z="1">
</template>
<style scoped>
.test-image {
background-image: url('../assets/images/valaxy-logo.png');
}
</style>
Third Plugin
实现评论
作为博客,用户通常会有评论的需求。
而由于评论系统各不相同,如 Hexo 等主题开发者们通常需在主题侧重复实现多款评论系统。 这显然是繁琐的。
Valaxy 决定通过插件中心化地提供各类封装好的评论组件和辅助函数。
譬如主题开发者,可以借助 valaxy-addon-waline
来快速实现 Waline 评论系统的集成。 而用户则可以使用相同的配置穿梭漫游于不同的主题之间。
集成参见 valaxy-addon-waline。
性能优化
Performance Optimization
添加依赖预构建 optimizeDeps
Add Dep Pre-bundling optimizeDeps
为了提高后续页面的加载性能,Vite 将那些具有许多内部模块的 ESM 依赖项转换为单个模块。 如果你的主题依赖了一些大型的 ESM 包,你可以通过添加 optimizeDeps
选项来预构建这些依赖项。
dayjs
已被默认预构建,您无需再次添加。 为什么用 dayjs 而不是 date-fns?
To improve the loading performance of subsequent pages, Vite bundles ESM dependencies with many internal modules into a single module. If your theme depends on some large ESM packages, you can pre-build these dependencies by adding the optimizeDeps
option.
dayjs
has been pre-built by default, you don’t need to add it again.
// valaxy.config.ts
import { defineTheme } from 'valaxy'
export default defineTheme({
vite: {
optimizeDeps: {
include: ['lodash-es'],
},
}
})
提醒特殊需求的用户安装第三方插件
如果您的主题适配了多个 addon
,但用户并非都需要安装。 如评论插件:
valaxy-addon-waline
valaxy-addon-twikoo
当用户没有主动安装对应 addon
时(即 addon
不存在的情况),则会默认重定向至一个空函数。
因此,如果某个插件不是必须的,请在主题文档中提醒想要使用该功能的用户安装对应插件。
To Be Continued.