暂无内容
如何在 VitePress 博客中实现 Markdown 热更新
在开发博客过程中,我发现对 Markdown 内容的修改不会自动热更新,必须重启开发环境,这极大影响了开发效率。之前我使用过 vuepress-theme-hope 主题,它内置了完善的热更新功能。通过分析它的实现方式,我尝试在 VitePress 中复现类似功能。
背景与需求
VitePress 默认对 .md
文件的改动并不会触发页面热更新,开发时修改内容必须重启,体验不佳。参考 vuepress-theme-hope 的做法,发现它在 .temp
目录下存储了文章分类、路径和内容等信息,并利用 Vite 的 HMR 机制实现数据的热更新。
设计思路
- 启动开发环境时,扫描所有 Markdown 文件,解析提取文章信息、分类、标签等数据,缓存至内存。
- 使用自定义 Vite 插件,结合 chokidar 监听
posts
文件夹内 Markdown 文件的新增、修改和删除。 - 当检测到文件变化时,更新缓存数据,并通过 Vite 的 HMR 机制通知前端更新对应的数据。
- 前端将数据存入 Pinia 统一管理,组件从 Pinia 读取最新数据实现自动刷新。
初始化所有 Markdown 文件数据
启动时通过递归读取所有 .md
文件,解析后存入缓存。
js
import fs from "fs";
import path from "path";
export const initData = async () => {
await loadCache();
// 递归获取 posts 目录下所有 Markdown 文件
const allMdFiles = await getAllMdFiles(postDir);
// 现有缓存中所有文章的 href,用于后续清理失效数据
const existingCacheKeys = Object.keys(cache.md);
await Promise.all(
allMdFiles.map(async (file) => {
const post = await getMdData(file);
if (post) {
const href = post.href; // 文章访问路径
cache.md[href] = { timestamp: post.update, post };
updateCategoryAndTag(post); // 更新分类和标签缓存
updateSortedCache(cache.posts, post, "top"); // 更新排序缓存
updateSortedCache(cache.star, post, "star");
existingCacheKeys.splice(existingCacheKeys.indexOf(href), 1);
}
})
);
// 删除缓存中已不存在的文章数据
existingCacheKeys.forEach((key) => {
const cachedPost = cache.md[key].post;
removeCategoryAndTag(cachedPost);
delete cache.md[key];
});
await saveCache();
};
// 递归读取目录内所有 Markdown 文件
const getAllMdFiles = async (dir) => {
const files = await fs.promises.readdir(dir, { withFileTypes: true });
const mdFiles = await Promise.all(
files.map(async (file) => {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
return await getAllMdFiles(fullPath);
}
return fullPath.endsWith(".md") ? fullPath : null;
})
);
return mdFiles.flat().filter(Boolean);
};
自定义 Vite 插件监听 Markdown 文件变化
使用 chokidar 监听 posts
文件夹,分别处理新增、修改、删除事件。
js
import chokidar from "chokidar";
import path from "path";
export default {
name: "md-file-watcher",
configureServer(server) {
const watcher = chokidar.watch(path.join(process.cwd(), "posts"));
watcher
.on("add", (file) => {
addMd(file);
// 新增文件时全量刷新页面,确保路由等正确更新
server.ws.send({ type: "full-reload" });
})
.on("change", (file) => {
changeMd(file);
})
.on("unlink", (file) => {
delMd(file);
});
},
};
处理函数一调用 updateMdCache
更新缓存:
js
export const addMd = async (filePath) => updateMdCache(filePath, "add");
export const delMd = async (filePath) => updateMdCache(filePath, "del");
export const changeMd = async (filePath) => updateMdCache(filePath, "change");
const updateMdCache = async (filePath, operation) => {
const href = createHref(filePath); // 根据文件路径生成访问路径
const cachedPost = cache.md[href];
if (operation === "del" && cachedPost) {
// 删除操作,清理分类、标签及缓存
removeCategoryAndTag(cachedPost.post);
delete cache.md[href];
removeFromCache(cache.posts, href);
removeFromCache(cache.star, href);
} else {
// 修改或新增,先清理旧缓存分类数据(如果有)
if (operation === "change" && cachedPost) {
removeCategoryAndTag(cachedPost.post);
}
const post = await getMdData(filePath);
if (!post) return;
updateCategoryAndTag(post);
updateSortedCache(cache.posts, post, "top");
updateSortedCache(cache.star, post, "star");
cache.md[href] = { timestamp: post.update, post };
}
await saveCache();
};
在 Markdown 文件底部添加 HMR 代码
通过 Vite 的 HMR API,实现模块热替换时数据更新。
js
if (import.meta.hot) {
import.meta.hot.accept(({ mdData }) => {
if (__VUE_HMR_RUNTIME__.mdDataUpdate) {
__VUE_HMR_RUNTIME__.mdDataUpdate(mdData);
}
});
}
这里的 VUE_HMR_RUNTIME 是 VitePress 内置的全局对象,用于管理 HMR 更新逻辑。
前端:使用 Pinia 管理并响应热更新数据
统一状态管理,方便组件读取并响应数据变化。
js
import { defineStore } from "pinia";
import {
categoryData,
mdData,
postsData,
starData,
tagsData,
} from "@casual/index";
export const useDataStore = defineStore("data", {
state: () => ({
categoryData,
mdData,
postsData,
starData,
tagsData,
}),
});
在布局组件中接收并处理 HMR 事件
js
import { useDataStore } from "@/store/index";
import { storeToRefs } from "pinia";
const dataStore = useDataStore();
const { categoryData, mdData, postsData, starData, tagsData } =
storeToRefs(dataStore);
if (import.meta.env.DEV && import.meta.hot) {
__VUE_HMR_RUNTIME__.categoryDataUpdate = (data) => {
categoryData.value = {};
Object.assign(categoryData.value, data);
};
__VUE_HMR_RUNTIME__.mdDataUpdate = (data) => {
mdData.value = {};
Object.assign(mdData.value, data);
};
__VUE_HMR_RUNTIME__.postsDataUpdate = (data) => {
postsData.value = [];
Object.assign(postsData.value, data);
};
__VUE_HMR_RUNTIME__.starDataUpdate = (data) => {
starData.value = [];
Object.assign(starData.value, data);
};
__VUE_HMR_RUNTIME__.tagsDataUpdate = (data) => {
tagsData.value = {};
Object.assign(tagsData.value, data);
};
}
这样,当 Markdown 文件被修改,前端就会接收到对应的更新,自动刷新数据,无需手动刷新页面。
评论