Vitepress 自定义主题开发教程

这篇文章是什么呢? 就是如题所述告诉你如何自定义极简的博客主题。

本文中使用到的代码示例可以在 stackblitz 上查看

vitepress 是什么

VitePress 是一个静态站点生成器 (SSG),专为构建快速、以内容为中心的站点而设计。简而言之,VitePress 获取用 Markdown 编写的内容,对其应用主题,并生成可以轻松部署到任何地方的静态 HTML 页面。

功能需求

首先确定我们开发一个主题需要哪些页面和功能,一个极简的博客可以只需要下面三个页面。加载文章数据生成首页,以及显示文章的详情页

  • 首页
  • 文章详情页
  • 404 页

实现

需要注意的是 vitepress 自定义主题需要区分哪些是运行在 node 环境的,哪些是运行在 浏览器 上的。

只运行在 node 中的

  • 配置文件 config.ts
  • 构建是加载数据 .data.js.data.ts 结尾文件

node 和 浏览器都会运行的

  • SFC 文件,因为使用的 SSR 渲染,所有需要注意使用范围

原则上只在 Vue 组件的 beforeMount 或 mounted 钩子中访问浏览器或 DOM API。

自定义主题

vitepress 想要自定义主题,只需要在主题启动入口文件里申明使用自定义主题即可 ( .vitepress/theme/index.js.vitepress/theme/index.ts 为主题入口文件)

bash 复制代码
.
│─ .vitepress
│  ├─ theme
│  │  └─ index.ts   # 主题入口
│  └─ config.ts     # 配置文件
│─ index.md
└─ package.json

在入口文件中导出根布局组件,Layout.vue 会替代 vitepress 默认主题进行页面的渲染,让我们从 Layout.vue 开始完善一个完整的主题吧。

ts 复制代码
// .vitepress/theme/index.ts

// 可以直接在主题入口导入 Vue 文件
import Layout from "./Layout.vue";
export default {
  Layout, // 每个页面的根布局组件
};

在开始之前,我们确定我们项目页面结构组成,暂定为如下目录结构,将文章集中放在 _posts 目录下

bash 复制代码
.
│─ .vitepress
│  ├─ theme
│  │  └─ index.ts   # 主题入口
│  └─ config.ts     # 配置文件
│─ _posts # 文章存放目录
│  ├─ xxxx.md
│  └─ xxxx.md
│─ index.md # 首页
└─ package.json

加载 _posts 文章

首页里的内容除了通用的布局样式之外 (导航、侧栏、横幅、....) 主要的就是文章列表了,我们先来实现对 _posts 目录下文件加载和分页,以及文章置顶排序等实现。

在 vitepress 提供了构建是加载数据的钩子,我们可以自定义加载数,并且在页面中使用它。使用方式也很简单只需要文件 .data.js.data.ts 结尾即可。

我们添加 posts.data.ts 文件,大致流程就是 读取文件 => 处理 frontmatter 参数、摘要、内容 => 对文章排序处理

需要注意的是: .data.ts 是运行在 node 环境下的,避免使用浏览器的 api

ts 复制代码
export default {
  watch: ["_posts/**/*.md"],
  // files 是一个所匹配文件的绝对路径的数组。
  load(files) {
    // 使用 vitepress 内置 渲染插件渲染 md 文件里提取摘要信息
    md = md || (await createMarkdownRenderer(config.srcDir, config.markdown, config.site.base, config.logger));
    const raw = [];
  },
};

通过 fs 读取文件内容和文件信息,以及文件信息(创建时间 和 修改时间),可以将生成的文章信息缓存起来,通过判断 mtimeMs 是否变更来判单是否需要重新生成。

ts 复制代码
const { mtimeMs: timestamp, birthtimeMs } = fs.statSync(file);
const cached = cache.get(file);

if (cached && timestamp === cached.timestamp) {
  raw.push(cached.data);
  continue;
}

使用 gray-matter 库来提取 md 文件里的 YAML frontmatter 信息,在 gray-matterexcerpt 添加摘要生成函数。

ts 复制代码
const fileContent = fs.readFileSync(file, "utf-8");
let excerpt = "";
const { data: meta } = matter<string, any>(fileContent, {
  excerpt: ({ content }: matter.GrayMatterFile<string>) => {
    const reg = /<!--\s*more\s*-->/gs; //  将 <!-- more --> 前面内容视为摘要格式
    const rpt = reg.exec(content);
    excerpt = rpt ? content.substring(0, rpt.index) : "";
  },
});

获取文章创建时间,取值规则如下 md => git => file,先去 frontmatter 里声明时间,然后获取 git 提交时间,如果前面两个都没获取到则获取文件的时间。

frontmatter 和 文件创建时间 上面都以及获取到了接下来主要是获取 git 提交时间 ,主要是通过 git log 命令获取文件信息。

node 实现如下

ts 复制代码
import { spawnSync } from "node:child_process";

function getFileBirthTime(url: string) {
  try {
    // ct
    const infoStr = spawnSync("git", ["log", '--pretty="%ci"', url]).stdout?.toString().replace(/["']/g, "").trim();
    const timeList = infoStr.split("\n").filter((item) => Boolean(item.trim()));
    if (timeList.length > 0) {
      return new Date(timeList.pop()!).getTime();
    }
  } catch (error) {
    return undefined;
  }
}

渲染摘要信息 和 处理文件路径

ts 复制代码
let url = normalizePath(path.relative(config.srcDir, file));
url = config.rewrites.map[url] ?? url;
url = "/" + url.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, config.cleanUrls ? "" : ".html");

const renderedExcerpt = excerpt ? md.render(excerpt) : void 0;
const data = {
  excerpt: renderedExcerpt,
  ...meta,
  url: withBase(config.site.base, url),
  filePath: slash(path.relative(config.srcDir, file)),
};
cache.set(file, { data, timestamp });
raw.push(data);

对生成数据进行排序并返回

ts 复制代码
return sortBy(raw, "-date");

页面布局

对文章的数据加载已经实现 ,在布局组件里直接导入 post.data 文件, layout 为 home 视为首页, 其他则为文章详情页面 。

vue 复制代码
<script setup lang="ts">
import { useData } from "vitepress";
import { data as allPosts } from "./post.data";
import { formatDate } from "./util/client";
const { page } = useData();
</script>

<template>
  <div v-if="page.isNotFound">404</div>
  <template v-else>
    <div v-if="page.frontmatter.layout === 'home'" class="post-list">
      <div v-for="item in allPosts" class="post-list-item">
        <a :href="item.url">
          <span>{{ item.title }}</span>
          <span>{{ formatDate(item.date, "YYYY-MM-dd") }}</span>
        </a>
      </div>
    </div>
    <template v-else>
      <div class="post-title">
        <div>{{ page.title }}</div>
        <a href="/">返回</a>
      </div>
      <div class="post-info">
        <Content />
      </div>
    </template>
  </template>
</template>

把所有页面逻辑都写在一个页面会难以维护 ,我们拆分一下

vue 复制代码
<template>
  <NotFoundPage v-if="page.isNotFound" />
  <template v-else>
    <HomePage v-if="page.frontmatter.layout === 'home'" />
    <PostPage v-else />
  </template>
</template>

实现分页

在 HomePage 对 allPost 实现前端分页,根据当前 currentPage 和 pageSize 将文章数组进行拆分

ts 复制代码
const currentPage = ref(1);
const pageSize = 3;

// 当前页显示 文章
const curPageList = computed(() => {
  const startIdx = (currentPage.value - 1) * pageSize;
  const endIdx = startIdx + pageSize;
  return allPosts.slice(startIdx, endIdx);
});

根据当前页码信息,生成一个简易分页组件数据

ts 复制代码
// 页码生成
const pageList = computed(() => {
  const count = Math.ceil(allPosts.length / pageSize);
  if (count > 5) {
    const list = [1, currentPage.value - 1, currentPage.value, currentPage.value + 1, count].filter((i) => i > 0 && i <= count);
    return [...new Set(list)];
  } else {
    return new Array(count).fill(null).map((_v, i) => i + 1);
  }
});

将模板中遍历 allPosts 替换成 curPageList ,并渲染出页码信息,切换页码时候设置 currentPage 就可以了

自定义配置

在上面都是将一些信息写死在代码中的,vitepress 提供了注册配置方式的,我们可以将主题中的一些参数写入到配置中。

.vitepress/config.ts 是 vitepress 加载配置的文件, 我们要自定义主题所以不能在使用 vitepress 提供的 defineConfig 方法。

如果时仅仅自己使用,可以直接导出我们配置就可以了

ts 复制代码
import { UserConfig } from "vitepress";
const themeConfig = {};
export type ThemeConfig = typeof themeConfig;
export default {
  title: "vitepress-demo",
  description: "viteoress 自定义主题示例",
  themeConfig, // 这里将配置我们主题的配置
} as UserConfig<ThemeConfig>;

比如我们上面存在文章的目录、页码 等等都可以写在配置里, 以方便细粒度配置。

ts 复制代码
const themeConfig = {
  pageSize: 3,
  sort: "-date",
  postDir: "_posts",
};

在主题中可以通过 vitepress 提供 useData 使用

ts 复制代码
const { theme } = useData<ThemeConfig>();

如果我们需要将主题发布到 npm 供其他人使用,当主题更为复杂的时候,会有更多的配置,所有我们需要处理主题默认的配置 和 用户配置合并,提供的 defineConfig 方便用户的使用

ts 复制代码
import { mergeConfig as mergeViteConfig } from "vite";
import type { UserConfig } from "vitepress";

export const defineConfig = (config: UserConfig<ThemeConfig>) => {
  config = mergeConfig(defaultConfig, config);

  // 可以在这里处理一些 配置合并问题

  return config;
};

总结

本文中使用到的代码示例可以在 stackblitz 上查看

一个复杂的主题当然还会有更多的共功能,页面中更良好的布局,那些都是在此基础上进行的扩展。


更多功能实现可以参考我写的 vitepress 主题

相关推荐
月下点灯16 分钟前
使用Set集合新特性,快速实现一个商品SKU(单品)规格选择器
前端·javascript·vue.js
大侠Luffy17 分钟前
做了这些SEO动作,独立开发的网站开始被搜索引擎逐量收录
前端·seo
四棱子24 分钟前
炫酷!18.5kb实现流体动画,这个开源项目让个人主页瞬间高大上!
前端·开源
Sparkxuan24 分钟前
封装WebSocket
前端·websocket
工呈士24 分钟前
Redux 实践与中间件应用
前端·react.js·面试
Nano25 分钟前
深入解析 JavaScript 数据类型:从基础到高级应用
前端
无羡仙25 分钟前
浮动与BFC容器
前端
xphjj25 分钟前
树形数据模糊搜索
前端·javascript·算法
刺客_Andy26 分钟前
React 第三十四节 Router 开发中 useLocation Hook 的用法以及案例详解
前端·react.js
我的div丢了肿么办26 分钟前
HarmonyOS鸿蒙tabBar的详细讲解
前端·javascript·harmonyos