-
Notifications
You must be signed in to change notification settings - Fork 2
开发日志
以下部分是我基于 VuePress 的默认主题进行二次开发的踩坑笔记。
-
插件或者主题的入口文件会在 Node App 中被加载,因此它们需要使用 CommonJS 格式。
-
客户端文件会在 Client App 中被加载,它们最好使用 ESM 格式。
-
VuePress 会在构建过程中生成一个 SSR 应用,用以对页面进行预渲染。
-
如果一个组件在
setup()
中直接使用浏览器 / DOM API ,它会导致构建过程报错,因为这些 API 在 Node.js 的环境中是无法使用的。在这种情况下,你可以选择一种方式:-
将需要访问浏览器 / DOM API 的代码部分写在
onBeforeMount()
或onMounted()
Hook 中。 -
使用
<ClientOnly>
包裹这个组件。 -
如果在组件中导入的模块会立即执行访问 DOM API 操作,使用
<ClientOnly>
包裹这个组件也可能会在构建过程报错,可以使用动态导入的方式引入模块。export default { setup(props) { onMounted(async () => { // dynamic import masonry const module = await import("masonry-layout"); const Masonry = module.default; // do something }) } }
-
-
布局组件
Layout
应该包含 Content 组件来展示 Markdown 内容
项目的打包工具使用 Vite
-
按照 Tailwind CSS 官方文档安装依赖并生成配置文件
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest npx tailwindcss init -p
💡 执行完以上命令后,会在项目的根文件夹下生成配置文件
tailwind.config.js
,可以配置其中的属性purge
,以在生产环境下优化 Tailwind CSS 文件大小。但是由于项目中存在通过拼接字符串构成的类名,如果执行优化会出现样式丢失的情况,因此没有配置purge
属性。 -
在文件
.vuepress/config.js
中为打包工具配置 postcss 参数// ... module.exports = { // ... bundlerConfig: { // vite 打包工具的选项 viteOptions: { css: { postcss: { plugins: [ require('tailwindcss'), require('autoprefixer') ] } }, } }, // ... }
-
在文件
.vuepress/styles/index.scss
的开头中引入 Tailwind@tailwind base; @tailwind components; @tailwind utilities;
⚠️ 由于引入了@tailwind base
可能会会造成默认主题样式的重置,可以手动添加相应的 CSS 样式修正。
VuePress 使用 markdown-it 来解析 Markdown 内容,支持通过安装插件来实现语法扩展,还可以对插件进行参数配置。
安装插件的方法参考:
添加 @neilsustc/markdown-it-katex
插件为 Markdown 添加数学公式的支持。
-
安装依赖
npm i @neilsustc/markdown-it-katex
💡
markdown-it-katex
这个包在 npm 里下载量最多,但是它依赖的 katex 版本很低,已经没有维护了,所以换成@neilsustc/markdown-it-katex
-
在文件
.vuepress/config.js
中配置 markdown-it 插件// ... module.exports = { // ... extendsMarkdown: (md) => { md.use(require('@neilsustc/markdown-it-katex'), {output: 'html'}) }, // ... }
⚠️ 根据 KaTex 官方文档可以通过属性output
来设置数学公式的渲染模式,应该设置为html
,因为默认值ouput: htmlAndMathml
会将一些 mathml 格式的非标准标签插入到 HTML,导致构建过程报错 -
除了安装插件,还需要引入 katex 的样式表,在文件
.vuepress/config.js
中配置 head 参数// ... module.exports = { // ... head: [ ['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/katex@0.13.5/dist/katex.min.css' }], ], // ... }
借助 VuePress 提供的插件 API 可以为网站新增页面(不依赖 Markdown 文件),还可以为指定的页面提供额外的数据。
💡 VuePress 插件是一个符合插件 API 的 JS 对象或返回值为 JS 对象的函数,具体参考官方文档开发插件一章。
VuePress 提供的插件 API 有多种 Hooks,它们的执行顺序和时间点都不同,可以参考官方文档的核心流程与 Hooks 一节,代码需要在合适 Hooks 下执行才不会报错。
参考官方插件 Git 的代码,通过插件 .vuepress/plugins/addTime.js
为 Markdown 文件添加时间修改的信息。
使用 extendsPageOptions Hook 将 Markdown 文件的创建时间 createdTime
和更新时间 updatedTime
作为 Frontmatter 字段添加到相应文件中。
/**
* refer to @vuepress/plugin-git: https://www.npmjs.com/package/@vuepress/plugin-git
*/
const execa = require('execa')
/**
* Check if the git repo is valid
*/
const checkGitRepo = (cwd) => {
try {
execa.commandSync('git log', { cwd })
return true
} catch {
return false
}
}
const getUpdatedTime = async (filePath, cwd) => {
const { stdout } = await execa(
'git',
['--no-pager', 'log', '-1', '--format=%at', filePath],
{
cwd,
}
)
return Number.parseInt(stdout, 10) * 1000
}
const getCreatedTime = async (filePath, cwd) => {
const { stdout } = await execa(
'git',
['--no-pager', 'log', '--diff-filter=A', '--format=%at', filePath],
{
cwd,
}
)
return Number.parseInt(stdout, 10) * 1000
}
const addTime = {
name: 'vuepress-plugin-addTime',
async extendsPageOptions(options, app) {
if (options.filePath) {
filePath = options.filePath;
const cwd = app.dir.source()
const isGitRepoValid = checkGitRepo(cwd)
let createdTime = null;
let updatedTime = null;
if (isGitRepoValid) {
createdTime = await getCreatedTime(filePath, cwd)
updatedTime = await getUpdatedTime(filePath, cwd)
}
return {
frontmatter: {
createdTime,
updatedTime
},
}
} else {
return {}
}
}
}
module.exports = addTime
参考官方文档添加额外页面一章,通过插件 .vuepress/plugins/createHomePage.js
为网站添加主页,通过插件 .vuepress/plugins/generateFolderPages.js
和 .vuepress/plugins/generateListPages.js
创建一些导航页。
主要使用 createPage
方法异步创建页面,由于导航页需要基于所有 Markdown 文件的数据,代码所以需要在 onInitialized Hook 下执行,此时页面已经加载完毕。
// 创建首页
const { createPage } = require('@vuepress/core')
const createHomePage = (options, app) => {
return {
name: 'vuepress-plugin-createHomePage',
async onInitialized(app) {
// if homepage doesn't exist
if (app.pages.every((page) => page.path !== '/')) {
// async create a homepage
const homepage = await createPage(app, {
path: '/',
// set frontmatter
frontmatter: {
layout: 'HomeLayout',
cards: options.cards || []
},
})
// push the homepage to app.pages
app.pages.push(homepage)
}
},
}
}
module.exports = createHomePage
参考官方文档继承一个主题一章。
由于我继承的主题并没有发布到 NPM 上而是作为本地主题,因此在文件 .vuepress/config.js
中配置 theme 参数时通过绝对路径来使用它
// ...
module.exports = {
// ...
theme: path.resolve(__dirname, './theme/index.js'),
// ...
}
有两种方式新增布局,然后就可以直接在 Markdown 文件的顶部 Frontmatter 字段 layout 中使用它们:
-
方法一:如果布局组件放置在主题的
.vuepress/theme/layouts/
目录下,需要在主题的入口文件.vuepress/theme/index.js
中通过配置属性layouts
来显式指定const { path } = require('@vuepress/utils') module.exports = { name: 'vuepress-theme-two-dish-cat-fish', extends: '@vuepress/theme-default', // registe 4 layouts layouts: { HomeLayout: path.resolve(__dirname, 'layouts/HomeLayout.vue'), ClassificationLayout: path.resolve(__dirname, 'layouts/ClassificationLayout.vue'), FolderLayout: path.resolve(__dirname, 'layouts/FolderLayout.vue'), Layout: path.resolve(__dirname, 'layouts/Layout.vue'), }, }
-
方法二:如果使用默认主题(不进行继承拓展),可以通过插件 API 的 clientAppEnhanceFiles Hook 来注册自定义的布局组件。
-
创建
.vuepress/clientAppEnhance.js
文件 -
在文件中使用
clientAppEnhanceFile
方法注册组件import { defineClientAppEnhance } from '@vuepress/client' import CustomLayout from './CustomLayout.vue' export default defineClientAppEnhance(({ app }) => { app.component('CustomLayout', CustomLayout) })
-
💡 如果布局是基于默认主题的布局组件 Layout
进行二次开发,可以使用该组件提供的的插槽
navbar-before
navbar-after
sidebar-top
sidebar-bottom
page-top
page-bottom
基于 Markdown 文件在存储系统的位置,使用 D3.js 构建树形图来可视化文件夹的嵌套层级关系。
-
安装 D3.js 依赖
npm install d3@6.5.0
-
在插件
.vuepress/plugins/generateFolderPages.js
中通过遍历所有 Markdown 文件(生成的页面),使用 extendsPageData Hook 为笔记导航页添加额外的数据。const { createPage } = require('@vuepress/core'); const generateFolderPages = (options, app) => { let postFolders = {} options.postFolders.forEach(folder => { postFolders[folder] = { posts: [], tags: [] } }) return { name: 'vuepress-plugin-generateFolderPages', async onInitialized(app) { // rearrange posts to different folder app.pages.forEach((page) => { let folder = ''; if (page.filePathRelative) { folder = page.filePathRelative.split("/")[0] if (!(folder in postFolders)) return } else { return } const post = { key: page.key, title: page.title, path: page.path, pathRelative: page.htmlFilePathRelative, filePathRelative: page.filePathRelative, tags: page.frontmatter.tags || [], createdTime: page.frontmatter.createdTime || null, updatedTime: page.frontmatter.updatedTime || null, date: page.frontmatter.date || null, collection: page.frontmatter.collection || '', collectionOrder: page.frontmatter.collectionOrder || 0, } postFolders[folder].posts.push(post); postFolders[folder].tags = [...new Set([...postFolders[folder].tags, ...post.tags])] }) //... }, extendsPageData: (page, app) => { // add data to each folder navigation pages if (page.frontmatter.folder) { return { postsData: postFolders[page.frontmatter.folder] } } else { return {} } }, } } module.exports = generateFolderPages
-
为笔记导航页添加的额外数据是一个数组,在布局组件
.vuepress/theme/layouts/FolderLayout.vue
中将这个扁平的数据结构转换为一个 JS 对象,使它符合 D3.js 用于计算层次布局的数据结构要求。<script> //... function buildPostsTreeData(rootName, postsList) { let tree = { name: rootName, type: "root", parent: null, children: [], }; const mdReg = /\.md$/; postsList.forEach((post) => { const paths = post.filePathRelative.split("/").slice(1); let folder = tree; let currentContent = tree.children; for (let index = 0; index < paths.length; index++) { const path = paths[index]; // let existingPath = getLocation(currentLevel, "name", path); let existingPath = currentContent.find((item) => { return item.name === path; }); if (existingPath) { folder = existingPath; currentContent = existingPath.children; } else if (mdReg.test(path)) { const newPath = { name: path, type: "post", parent: folder, data: post, }; currentContent.push(newPath); } else { const newPath = { name: path, type: "folder", parent: folder, children: [], }; currentContent.push(newPath); folder = newPath; currentContent = newPath.children; } } }); return tree; } export default { setup(props) { // data const data = reactive({ //... folder: "", posts: [], postsTreeData: null, //... }); //... data.folder = page.value.frontmatter.folder; data.posts = page.value.postsData.posts; data.postsTreeData = buildPostsTreeData(data.folder, data.posts); //... } } </script>
-
在组件
.vuepress/components/PostsTree.vue
中构建树形图,使用 D3.js 计算树图节点的定位等数据,再使用 Vue3 将数据绑定到 DOM 上控制 svg 的生成。