Restent's Notebook

  • · 技术

    使用 Valaxy 重构我的博客

    原先的 VitePress 博客在不断推进度后,凭我的智商难以再继续向前推进度。正好在 [Big_Cake](https://lihaoyu.cn) 的推荐下,我计划将博客迁移到 Valaxy。有一说一,Valaxy 是一个很好用的博客框架,虽然自己也在这其中踩过不少的坑。也是趁着博客的 UI / UX 基本稳定下来后,我想着写(shui)一篇文章,分享一下自己写主题的过程好了。


    ::: tip 本文提到的功能可能不完整

    在这篇文章发表时,我并没有给博客添加完所有我需要的功能。

    下文提到的功能是截至本文章发表时,我已经给博客添加的功能。

    :::

    首先,如何开始?

    在此之前,Valaxy 只提供了以下选择给用户:

    • 开箱即用的 Valaxy 博客模板,配备 valaxy-theme-yun 主题;
    • 面向主题开发者的 Valaxy 模板,默认配备 valaxy-theme-starter 主题;
    • 面向插件开发者的 Valaxy 模板,主题是什么我也不知道(戳手)。

    但是,在一两个月之前,我的博客已经改为闭源,目前只有部分人可以查看到源码,所以如果我要写主题那肯定也不会开源;而 Valaxy 给主题开发者的模板是带有 NPM 上传的选项的,也就是说如果我要写主题那我必须得开源,甚至是面向公众、之后还得维护,作为一个懒虫我肯定不能接受。

    但现在,这不是问题。云游君提供了一个本地写主题的 模板,上述的问题完美解决,我也开始筹备迁移工作。

    过年还真光写代码了

    新手入门

    Valaxy 的 Layout

    接下来要假设你的位置在根目录,请多加注意(

    在普遍的 Vue 项目中,我们都是在 App.vue 里编写 Layout;而在 Valaxy,我们需要在 valaxy-theme-custom/components/layout.vue 里编写,而编写的内容则与前者无异。

    再到内容渲染,Valaxy 使用 Vue Router 作为路由系统,那么用过 Vue Router 的访客们都熟悉了,需要用 <RouterView /><router-view /> 来渲染当前页面内容。

    <slot>
      <RouterView />
    </slot>
    

    同时,Valaxy 允许你创建多个 Layout 模板,并且它们存放在 valaxy-theme-custom/layouts 目录下。至于这有什么用处,下面就会提到了。

    Valaxy 的 Pages

    上面提到,Valaxy 使用 Vue Router 作为路由系统,所以不出意外的,你会在主题目录(代指 valaxy-theme-custom 目录)下看到一个 pages 文件夹。你可以在这里创建所需要的页面组件,比如 index.vue, tags.vue, categories.vue 等。

    如果你浏览 valaxy-theme-starter 主题的 pages 目录的 index.vue 页面组件的话,你会发现有这么几行内容:

    <route lang="yaml">
    meta:
      layout: home
    </route>
    

    它的用途便是结合你创建的 Layout 模板,上述代码便是指定 index.vue 使用名为 home 的 layout 组件。

    到这里,你应该能够理解 Valaxy 的 Layout 和 Pages 了,这对我们接下来的主题编写很有帮助。

    开始写主题

    获取站点信息和主题信息

    在根目录下,你会发现有 site.config.tsvalaxy.config.ts 两个配置文件。其中,前者是站点信息的配置文件,后者是站点主题的配置文件。而在编写主题时,譬如站点标题、作者信息之类的内容肯定要从这其中提取。

    获取站点信息

    Valaxy 给予了一个 useSiteStore 的函数,并允许你通过这个函数获取站点信息。那么,先来引用这一个函数:

    import { useSiteStore } from "valaxy";
    
    const site = useSiteStore();
    

    这样,你就可以通过这个函数来引用并输出站点信息了。比如,你可以这样输出站点信息:

    <template>
      <p>{{ site.title }}</p>
    </template>
    

    如果你的组件是类似 <component title="Site Title" /> 这样输出文本的话,那你可以这么做:

    <template>
      <component :title="site.title" />
    </template>
    

    获取主题信息

    valaxy-theme-custom/types 下有一个 TypeScript 声明文件,指定了你的主题配置项;而 valaxy-theme-custom/composables 下有两个相关的配置文件,让你能够通过它们用一个名为 useThemeConfig 的函数引用主题信息。

    还是老样子,先来引用这一个函数

    import { useThemeConfig } from "../composables";
    
    const themeConfig = useThemeConfig();
    

    ::: tip 细节

    • 这里假设你的 Vue 组件放置在 valaxy-theme-custom/components 路径下;
    • useThemeConfig 函数是由 valaxy-theme-custom/composables 下的配置文件给予的。但因为另外一个配置文件 config.ts 已经在 index.ts 中 export,所以你可以直接从 ../composables 引用;
    • 具体的主题配置需要在 valaxy-theme-custom/node 下的 TypeScript 声明文件指定。

    :::

    这样,你就可以通过这个函数来引用并输出主题信息了。比如,valaxy.config.ts 下有一个页脚的站点创建年份:

    export default defineValaxyConfig<ThemeConfig>({
      /* ... */
      themeConfig: {
        footer: {
          since: 2019,
        },
      },
    
      /* ... */
    });
    

    你可以通过这样输出:

    <template>
      <p>{{ themeConfig.footer.since }}</p>
    </template>
    

    原理上是和上面的站点信息差不多的。

    获取文章相关信息

    生成文章列表

    在 Vue 中,文章列表这种东西肯定是要靠遍历生成的啦。这里我们要借助 Valaxy 的 useSiteStore 函数来生成文章列表。

    如果你想分页也一起兼顾的话,那就请看下一部分。

    在我编写主题过程中,我参考了 valaxy-theme-starter 的相关源码,最后得出如下代码:

    /*- PostList.vue -*/
    <script setup lang="ts">
    import { computed } from "vue";
    import { useSiteConfig, useSiteStore } from "valaxy";
    import type { Post } from "valaxy/types";
    
    // 定义 props
    const props = withDefaults(
      defineProps<{
        type?: string;
        posts?: Post[];
      }>
    );
    
    // 数据源
    const site = useSiteStore();
    const siteConfig = useSiteConfig();
    const posts = computed(() =>
      (props.posts || site.postList).filter((post) =>
        import.meta.env.DEV ? true : !post.hide
      )
    );
    </script>
    
    // 通过遍历生成文章列表
    <template>
      <ul class="space-y-4">
        <li v-for="(post, index) in posts" :key="post.path">
          <PostCard :post="post" />
        </li>
      </ul>
    </template>
    
    /*- PostCard.vue -*/
    <script setup lang="ts">
    // 导入 Post 的 TypeScript 声明
    import type { Post } from "valaxy";
    
    // 定义 props
    defineProps<{
      post: Post;
    }>();
    
    // 格式化日期的 script,返回中国时间格式
    const formatDate = (date: string | number | Date) => {
      const options: Intl.DateTimeFormatOptions = {
        year: "numeric",
        month: "long",
        day: "numeric",
      };
      return new Date(date).toLocaleDateString("zh-CN", options);
    };
    </script>
    
    <template>
      <RouterLink v-if="post.path" :to="post.path">
        <div class="card">
          <div class="card-image">
            <figure class="image h-80">
              <img
                style="width:100%;height:100%;object-fit:cover;"
                :src="post.banner ?? '默认封面链接'"
                alt="Post banner image"
              />
            </figure>
          </div>
          <div class="card-content">
            <div class="media">
              <div class="media-content">
                <p class="title is-4">{{ post.title }}</p>
                <p class="subtitle is-6">
                  {{ formatDate(post.date as Date) }} ·
                  {{ post.categories ?? "未分类" }}
                </p>
              </div>
            </div>
            <div class="content">
              {{ post.description }}
            </div>
          </div>
        </div>
      </RouterLink>
    </template>
    

    与之前写 VitePress 博客主题相比,我将文章卡片从文章列表中拆离、单独成为一个组件,并在文章列表的组件中引用卡片组件。这样,在后续修改中,我就不用翻代码翻到两眼一黑。

    为文章列表添加分页

    一般的博客都是固定一页显示多少篇文章,而不是全部文章长长一条堆积在首页,所以分页是十分重要的。

    Valaxy 提供有一个 <ValaxyPagination /> 组件供你使用,但一些诸如页面数量计算的 script 还是需要自己补充的。这里参考了 valaxy-theme-yun 主题的相关代码,并融合上面文章列表的代码,最后如下:

    /*- PostList.vue -*/
    <script setup lang="ts">
    import { computed } from "vue";
    import { useSiteConfig, useSiteStore } from "valaxy";
    import type { Post } from "valaxy/types";
    
    const props = withDefaults(
      defineProps<{
        type?: string;
        posts?: Post[];
        curPage?: number;
      }>(),
      {
        curPage: 1,
      }
    );
    
    const site = useSiteStore();
    const siteConfig = useSiteConfig();
    const pageSize = computed(() => siteConfig.value.pageSize);
    
    const posts = computed(() =>
      (props.posts || site.postList).filter((post) =>
        import.meta.env.DEV ? true : !post.hide
      )
    );
    
    const displayedPosts = computed(() =>
      posts.value.slice(
        (props.curPage - 1) * pageSize.value,
        props.curPage * pageSize.value
      )
    );
    </script>
    
    <template>
      <ul class="space-y-4">
        <li
          v-for="(post, index) in displayedPosts"
          :key="post.path"
          :class="{ 'mb-0': index === posts.length - 1 }"
        >
          <PostCard :post="post" />
        </li>
      </ul>
      <ValaxyPagination
        class="mt-4"
        :cur-page="curPage"
        :page-size="pageSize"
        :total="posts.length"
      />
    </template>
    

    ::: tip

    因为主题需要,所以我在 <li /> 遍历时添加了这一行::class="{ 'mb-0': index === posts.length - 1}"。 你可以按自己的需求参考或保留。

    :::

    ::: warning 提醒

    <ValaxyPagination /> 如果在你的主题上调整不当,构建后其样式可能会变得异常奇怪。我建议查找它的源码,并自己重新写一个,至少我是这么做的。

    :::

    获取文章和分类的数量

    我的侧边栏的站长卡片会展示我的文章数量和分类数量。而要做到这种效果,只需要计算它们的 length 就可以了。

    这里我们会用到 useSiteStoreuseCategories 两个函数。前者老样子用于文章,后者则用于分类。

    import { useSiteStore, useCategories } from "valaxy";
    
    // 获取数据
    const site = useSiteStore();
    const categories = useCategories();
    
    // 处理文章数据
    const posts = computed(() =>
      (props.posts || site.postList).filter((post) =>
        import.meta.env.DEV ? true : !post.hide
      )
    );
    

    这样,你就可以通过这两个函数来引用并输出主题信息了。以下是输出方法:

    <template>
      <p>{{ posts.length }}</p> // 文章数量
      <p>{{ (Array.from(categories.children).length) }}</p>
    </template>
    

    理论上 tags 可以用 {{ (Array.from(tags).length) }} 获取,但是我这边因为一些奇怪的问题报错了。加上我不是很喜欢用 Tag 分类,如果你有这类需求就麻烦你自行研究了(((

    获取近期四篇文章

    还是我的侧边栏,有一个「近期文章」的卡片,会列出从新到旧的四篇文章。

    获取文章就很简单了,和上面生成文章列表一样,用 useSiteStore 函数获取,最后再 slice 出四篇文章。

    <script setup lang="ts">
    import { useSiteStore } from "valaxy";
    
    const site = useSiteStore();
    const recentPosts = computed(() => (props.posts || site.postList).slice(0, 4));
    </script>
    
    <template>
      <div class="p-4">
        <div class="mb-3 text-sm">近期文章</div>
        <ul>
          <template v-for="post in recentPosts" :key="post.path">
            <li>
              <ul>
                <li class="text-sm">{{ formatDate(post.date as Date) }}</li>
                <RouterLink v-if="post.path" :to="post.path"
                  ><span class="text-sm">{{ post.title }}</span>
                </RouterLink>
              </ul>
            </li>
          </template>
        </ul>
      </div>
    </template>
    

    生成文章目录

    浏览文章的时候,目录肯定是必不可少的啦。这里需要用到 useOutline 函数,同时还有一个 MenuItem 的 TypeScript 声明。

    <script setup lang="ts">
    import { useOutline } from "valaxy";
    
    const { headers, handleClick } = useOutline();
    </script>
    
    <template>
      <div class="p-4">
        <div class="mb-3 text-sm">目录</div>
        <div style="max-height:200px;overflow:scroll;">
          <OutlineItem :headers="headers" :on-click="handleClick" root />
        </div>
      </div>
    </template>
    
    /*- OutlineItem.vue -*/
    <script setup lang="ts">
    import type { MenuItem } from "valaxy";
    
    defineProps<{
      headers: MenuItem[];
      onClick: (e: MouseEvent) => void;
      root?: boolean;
    }>();
    </script>
    
    <template>
      <ul :class="root ? 'root' : 'nested'" class="toc">
        <li
          v-for="{ children, link, title, lang } in headers"
          :key="link"
          class="va-toc-item"
          :lang="lang || 'zh-CN'"
        >
          <a class="outline-link" :href="link" @click="onClick">
            {{ title }}
          </a>
          <template v-if="children?.length">
            <OutlineItem :headers="children" :on-click="onClick" />
          </template>
        </li>
      </ul>
    </template>
    

    这里不用 <RouterLink /> 是因为点击之后并不会触发。

    文章导航

    当你阅览完文章之后,如果尚有兴趣,肯定会向上或向下一篇文章继续阅读。Valaxy 专门提供了一个 usePrevNext 函数,供我们制作这个文章导航。

    <script setup lang="ts">
    import { usePrevNext } from "valaxy";
    
    const [prev, next] = usePrevNext();
    </script>
    
    <template>
      <div class="my-4">
        <div class="grid grid-cols-2">
          <div class="text-left">
            <div class="font-bold">上一篇文章</div>
            <RouterLink v-if="prev" :to="prev.path || ''" :title="prev.title">
              {{ prev.title }}
            </RouterLink>
            <div v-else>别看了,没有了</div>
          </div>
          <div class="text-right">
            <div class="font-bold">下一篇文章</div>
            <RouterLink v-if="next" :to="next.path || ''">
              <span class="i-ic-baseline-home"></span>
              {{ next.title }}
            </RouterLink>
            <div v-else>别看了,没有了</div>
          </div>
        </div>
      </div>
    </template>
    

    制作返回顶部的按钮

    经过对大多数博客的观察后,我认为我对返回顶部按钮的需求是:长期至于页面右下角,并且优先级高于所有元素,不论遮挡。而这个按钮的实现方式并不复杂,因为 <a href="#" /> 会强制让你返回到顶部。

    <template>
        <div class="fixed bottom-0 right-0 m-8 z-50">
            <a href="#" class="button is-large is-info">
                <span class="icon i-ic-baseline-arrow-upward"></span>
            </a>
      </div>
    </template>
    

    End

    在前天刚发布这一篇文章的时候,写的水分实在太高了。虽然我发这篇文章的目的就是要水文,但就按那样子写可能真的不会给人多大帮助,虽然这一篇修改过后的文章也不会有多大帮助就是了。

    虽然手头上还有很多特性和 Bug 等待处理,但不得不说,Valaxy 真的是一个非常好用的博客框架,如果你已经跃跃欲试的话,我建议你立即行动起来。

    以及,我的博客今后不会再开源,只有对应仓库访问权限的人才能够看到源码。但上述部分我放出了很多我的博客所使用的原始代码,虽然在不断的 Bug 修复后已经逐渐改变。还是那句话:希望对你有所帮助。


© 2019 - 2024 Restent Ou

Powered by Nuxt