Oblivion - A Vitepress Theme



上一个主题 reco 已经用了不短的时间了,最近关注 Vitepress,感觉很不错,于是萌生了自己再做一个主题的想法。

目前的功能

  • toc
  • Katex
  • Tags 和 Collections
  • Vitepress 默认主题中支持的几乎全部 Markdown 功能

加入各种功能时遇到的问题

在开发的过程中参考了很多现成主题的写法,包括 Vitepress 的 default-themedefault-themerecovitepress-blog-zaun等等。

中间也遇到了好多问题,比如 Vue 动画效果在首次载入时的表现问题、SSR与客户端代码的区分问题、Katex 构建问题等等,到目前为止这几个问题都得到了解决。

Katex

不得不说,Katex的加入给我带来了不小的麻烦,自定义标签会在 build 的时候报错:

Figure 1: Katex构建错误

找了好多的资料都没找到点上,最后还是在 Vitepress 自己的配置文件中增加了一项才解决问题。在 .vitepress/config.ts.vitepress/config.ts 中加入以下内容:

typescript
1
// Katex用到的自定义 Tag 列表。参考:[](https://github.com/KaTeX/KaTeX/blob/main/src/mathMLTree.js#L23)
2
const mathTags = [
3
  "math", "annotation", "semantics",
4
  "mtext", "mn", "mo", "mi", "mspace",
5
  "mover", "munder", "munderover", "msup", "msub", "msubsup",
6
  "mfrac", "mroot", "msqrt",
7
  "mtable", "mtr", "mtd", "mlabeledtr",
8
  "mrow", "menclose",
9
  "mstyle", "mpadded", "mphantom", "mglyph"
10
]
11

12
// config 中加入 vue 字段
13
export default {
14
  vue: {
15
    template: {
16
      compilerOptions: {
17
          isCustomElement: tag => mathTags.includes(tag)
18
      }
19
    }
20
  },
21
}
1
// Katex用到的自定义 Tag 列表。参考:[](https://github.com/KaTeX/KaTeX/blob/main/src/mathMLTree.js#L23)
2
const mathTags = [
3
  "math", "annotation", "semantics",
4
  "mtext", "mn", "mo", "mi", "mspace",
5
  "mover", "munder", "munderover", "msup", "msub", "msubsup",
6
  "mfrac", "mroot", "msqrt",
7
  "mtable", "mtr", "mtd", "mlabeledtr",
8
  "mrow", "menclose",
9
  "mstyle", "mpadded", "mphantom", "mglyph"
10
]
11

12
// config 中加入 vue 字段
13
export default {
14
  vue: {
15
    template: {
16
      compilerOptions: {
17
          isCustomElement: tag => mathTags.includes(tag)
18
      }
19
    }
20
  },
21
}
typescript
1
// Katex用到的自定义 Tag 列表。参考:[](https://github.com/KaTeX/KaTeX/blob/main/src/mathMLTree.js#L23)
2
const mathTags = [
3
  "math", "annotation", "semantics",
4
  "mtext", "mn", "mo", "mi", "mspace",
5
  "mover", "munder", "munderover", "msup", "msub", "msubsup",
6
  "mfrac", "mroot", "msqrt",
7
  "mtable", "mtr", "mtd", "mlabeledtr",
8
  "mrow", "menclose",
9
  "mstyle", "mpadded", "mphantom", "mglyph"
10
]
11

12
// config 中加入 vue 字段
13
export default {
14
  vue: {
15
    template: {
16
      compilerOptions: {
17
          isCustomElement: tag => mathTags.includes(tag)
18
      }
19
    }
20
  },
21
}
1
// Katex用到的自定义 Tag 列表。参考:[](https://github.com/KaTeX/KaTeX/blob/main/src/mathMLTree.js#L23)
2
const mathTags = [
3
  "math", "annotation", "semantics",
4
  "mtext", "mn", "mo", "mi", "mspace",
5
  "mover", "munder", "munderover", "msup", "msub", "msubsup",
6
  "mfrac", "mroot", "msqrt",
7
  "mtable", "mtr", "mtd", "mlabeledtr",
8
  "mrow", "menclose",
9
  "mstyle", "mpadded", "mphantom", "mglyph"
10
]
11

12
// config 中加入 vue 字段
13
export default {
14
  vue: {
15
    template: {
16
      compilerOptions: {
17
          isCustomElement: tag => mathTags.includes(tag)
18
      }
19
    }
20
  },
21
}

参考链接:

  1. KaTeX
  2. Vitepress

Server-side Rendering(SSR)

Vitepress 使用的是服务端构建,虽然官方文档中说要尽量区分客户端 js 代码和服务端 js 代码,但在实践中因为我之前压根没怎么接触过 Vue,所以对具体的实现方式一头雾水。在查阅大量资料之后,我的解决方案是,如果有只能在客户端执行的 js 代码(比如只有在浏览器中才存在的 windowwindow 对象),那我就把他放到 onMountedonMounted 方法中去。

例如在 Tags 组件中:

vue
1
<script setup lang="ts">
2
import { useData } from "vitepress"
3
import { ref, computed, watchEffect, onMounted } from "vue"
4
import { getStorageTag, getPostsByTag } from "../helpers/tags.ts"
5
import { getStoragePage } from "../helpers/pagination.ts"
6

7
// 先设定默认值,可在服务端执行
8
const currentPage = ref(1)
9
const allPosts = useData().theme.value.posts
10
const currentTag = ref('')
11
const postsByTag = computed(() => getPostsByTag(currentTag.value))
12
const watchFun = ref(() => { })
13
const show = ref(false)
14

15
// 调用浏览器 API,只在客户端执行
16
onMounted(() => {
17
    currentPage.value = getStoragePage()
18
    currentTag.value = getStorageTag()
19
    
20
    watchEffect(() => {
21
        if (window.location.hash) {
22
            currentTag.value = window.location.hash.replace('#', '');
23
        }
24
    })
25
    
26
    show.value = true
27
})
28
</script>
1
<script setup lang="ts">
2
import { useData } from "vitepress"
3
import { ref, computed, watchEffect, onMounted } from "vue"
4
import { getStorageTag, getPostsByTag } from "../helpers/tags.ts"
5
import { getStoragePage } from "../helpers/pagination.ts"
6

7
// 先设定默认值,可在服务端执行
8
const currentPage = ref(1)
9
const allPosts = useData().theme.value.posts
10
const currentTag = ref('')
11
const postsByTag = computed(() => getPostsByTag(currentTag.value))
12
const watchFun = ref(() => { })
13
const show = ref(false)
14

15
// 调用浏览器 API,只在客户端执行
16
onMounted(() => {
17
    currentPage.value = getStoragePage()
18
    currentTag.value = getStorageTag()
19
    
20
    watchEffect(() => {
21
        if (window.location.hash) {
22
            currentTag.value = window.location.hash.replace('#', '');
23
        }
24
    })
25
    
26
    show.value = true
27
})
28
</script>
vue
1
<script setup lang="ts">
2
import { useData } from "vitepress"
3
import { ref, computed, watchEffect, onMounted } from "vue"
4
import { getStorageTag, getPostsByTag } from "../helpers/tags.ts"
5
import { getStoragePage } from "../helpers/pagination.ts"
6

7
// 先设定默认值,可在服务端执行
8
const currentPage = ref(1)
9
const allPosts = useData().theme.value.posts
10
const currentTag = ref('')
11
const postsByTag = computed(() => getPostsByTag(currentTag.value))
12
const watchFun = ref(() => { })
13
const show = ref(false)
14

15
// 调用浏览器 API,只在客户端执行
16
onMounted(() => {
17
    currentPage.value = getStoragePage()
18
    currentTag.value = getStorageTag()
19
    
20
    watchEffect(() => {
21
        if (window.location.hash) {
22
            currentTag.value = window.location.hash.replace('#', '');
23
        }
24
    })
25
    
26
    show.value = true
27
})
28
</script>
1
<script setup lang="ts">
2
import { useData } from "vitepress"
3
import { ref, computed, watchEffect, onMounted } from "vue"
4
import { getStorageTag, getPostsByTag } from "../helpers/tags.ts"
5
import { getStoragePage } from "../helpers/pagination.ts"
6

7
// 先设定默认值,可在服务端执行
8
const currentPage = ref(1)
9
const allPosts = useData().theme.value.posts
10
const currentTag = ref('')
11
const postsByTag = computed(() => getPostsByTag(currentTag.value))
12
const watchFun = ref(() => { })
13
const show = ref(false)
14

15
// 调用浏览器 API,只在客户端执行
16
onMounted(() => {
17
    currentPage.value = getStoragePage()
18
    currentTag.value = getStorageTag()
19
    
20
    watchEffect(() => {
21
        if (window.location.hash) {
22
            currentTag.value = window.location.hash.replace('#', '');
23
        }
24
    })
25
    
26
    show.value = true
27
})
28
</script>

这样就避免了构建过程中提示找不到对象的问题。

Transition 动画在第一次载入页面时不生效

在构建成功后运行查看效果时,发现页面中加入的 transitiontransition 效果在清除缓存刷新页面时不起作用。后来经过查找资料,我的解决方案是,在每个 transitiontransition 元素的子元素上增加 v-ifv-if,使其默认为 falsefalse 不显示,并在 onMountedonMounted 中将其设置为 truetrue 显示:

vue
1
<template>
2
  <transition appear enter-active-class="transition ease-out duration-300"
3
      enter-from-class="transform opacity-0 scale-95" enter-to-class="opacity-100 scale-100">
4
      <h1 v-if="show">Tags</h1>
5
  </transition>
6
</template>
7

8
<script setup lang="ts">
9
  const show = ref(false)
10
  onMounted(() => {
11
    show.value = true
12
  })
13
</script>
1
<template>
2
  <transition appear enter-active-class="transition ease-out duration-300"
3
      enter-from-class="transform opacity-0 scale-95" enter-to-class="opacity-100 scale-100">
4
      <h1 v-if="show">Tags</h1>
5
  </transition>
6
</template>
7

8
<script setup lang="ts">
9
  const show = ref(false)
10
  onMounted(() => {
11
    show.value = true
12
  })
13
</script>
vue
1
<template>
2
  <transition appear enter-active-class="transition ease-out duration-300"
3
      enter-from-class="transform opacity-0 scale-95" enter-to-class="opacity-100 scale-100">
4
      <h1 v-if="show">Tags</h1>
5
  </transition>
6
</template>
7

8
<script setup lang="ts">
9
  const show = ref(false)
10
  onMounted(() => {
11
    show.value = true
12
  })
13
</script>
1
<template>
2
  <transition appear enter-active-class="transition ease-out duration-300"
3
      enter-from-class="transform opacity-0 scale-95" enter-to-class="opacity-100 scale-100">
4
      <h1 v-if="show">Tags</h1>
5
  </transition>
6
</template>
7

8
<script setup lang="ts">
9
  const show = ref(false)
10
  onMounted(() => {
11
    show.value = true
12
  })
13
</script>

这样即使是第一次访问页面,动画效果也会存在。

下一步准备增加的功能

目前的功能只能说满足了基本的写作需求,但是类型还不够丰富,还是比较笼统。下一步准备实现更多功能:

  1. 时间线 - 这个不用多说,就是仿 Facebook 时间线,给自己回顾的时候用。
  2. 贡献热度图 - 就是 Github 贡献图的那个样子,打算自己用 svg 实现一个。
  3. 微博 - 方便自己随便记录一些内容用,目前想法是给现有的文章增加一个新类别,结构和正常文章完全一致,后续有了新的想法再说。

给想拿来用的人

如果有人想尝试使用本主题,直接将 vitepress-theme-oblivion 的内容复制到自己的仓库里构建发布就可以。也可以参考本人的 Pages 仓库 dyzdyz010.github.io。 有什么问题可以直接提 Issue。