百家饭团队开发的百家饭OpenAPI平台是用vuepress2.0搭建的,搭建的时候不知道2.0还处在beta状态,所以导致后来踩了一些坑,使用过程中vuepress2.0也从2.0.0-beta.18升到了2.0.0-beta.48,有很多的变化,所以想写一个教程介绍一下vuepress2.0的情况以及使用经验。
大致计划写这些内容吧:
这里先给搜索进来的同学提个醒:凡是网上搜索到需要修改clientAppEnhance.js 文件的教程都已经过时,最新版本已不再使用。另外说个题外话,我查了下,我为数不多的搜索关键词里面,vuepress的占了大部分,这里感谢大家的关注,这个教程肯定有不合理的地方,如果有任何需要帮助的,大家都可以留言,我尽力解答。
今天收到一个催更,感谢大家对于这个系列的热爱,最近没更新,一是进入第10章,其实进入了一个比较深的程度,实用意义没有前几章大;二是忙于百家饭OpenAPI平台v0.6.x版本的更新,说起来我们的API调用代码自动生成功能也很好用的,我们自己都用,如果各位前端小伙伴能帮我们试试,感激不尽。
今天主要讲从零编写Vuepress2主题,首先要说的是,官方推荐大家从官方主题扩展生成自己的主题,这样,有些内置功能可以开箱即用,参考6-9章节的介绍。这也是我的推荐。
我们自己是从零写的自己的主题,最主要的原因是上面这个推荐不在官方文档的入手文档里……我们当时并不知道有个官方主题可以使用,所以……
所以现在写这章,我们要介绍一下的几个内容:
什么时候推荐自己从零写主题
从零写主题的基础架构是什么样的
官方主题有哪些可取的地方
更多自定义扩展
1)如果你试用默认主题,认为整体结构和你预想的差很多的时候,可以考虑自己写主题,例如我们的结构和框架就和默认主题差很多。
2) 使用默认主题,他允许你在主题内使用插槽和参数对页面进行有限的自定义,但是了解和熟练使用这些插槽,需要熟悉官方文档和教程,如果不是很想使用这些功能,有能力自行定义的时候,可以考虑自行编写主题。
3)希望深入了解vuepress主题。
现在我们开始为前几章使用的demo程序增加一个完全自行编写的主题。
在docs/.vuepress里面新加一个空白文件夹,叫theme-baijiafan(名称随意)
而这个自定义主题最基础的结构如下:
入口文件,基础定义以下内容:
module.exports = (options) => {
return {
name: 'vuepress-theme-baijiafan',
layouts: {
},
}
}
一个属性是主题的名称,另外一个是layouts对象,表明该主题提供几种模板。 官方说法是”一个主题必须至少有两个布局: `Layout` 和 `404` “。形如:
const { path} = require('@vuepress/utils')
const fooTheme = (options) => {
return {
name: 'vuepress-theme-foo',
layouts: {
Layout: path.resolve(__dirname, 'layouts/layout.vue'),
404: path.resolve(__dirname, 'layouts/404.vue'),
},
// ...
}
}
其中Layout是默认页面内容定义,如果要使用别的名称,必须在每个md文件的头部属性中定义layout。
---
layout: Main
---
经查询源代码,确认并无配置项可以更改这一默认名称,因此,我们必须默认定义Layout模板文件。而404模板则推荐添加,否则出现404情况时,展示空白内容。
if (isString(frontmatterLayout)) {
layoutName = frontmatterLayout
} else {
// fallback to default layout
layoutName = 'Layout'
}
主题中基础内容还包括在上述index.js里定义的layout的定义文件。比如我们继续创建layouts文件夹,并创建layout.vue和404.vue两个文件。
其中layout.vue必须包含的内容是,只有包含了这个,才会把md文件内容渲染出来。
比如我们定义一个最简单的:
这样内容就出来了,就是进一步的内容和样式要一点点往里加。
除此之外,虽然不是必要的步骤,但是我们通常还需要定义client.js作为客户端初始化的文件,原来叫clientAppEnhance.js,现在是一个在index.js中的属性:
module.exports = ({ themePlugins = {}, ...localeOptions }) => {
return {
...
clientConfigFile: path.resolve(__dirname, './client.js'),
只要是通过 clientConfigFile定义的文件都可以。
他主要的功能是增强生成的静态文件中js部分的功能(官方说是”客户端部分“,但是因为在里面仍然需要使用SSR判断是否是服务器环境,所以我个人觉得这个”客户端部分“的说法有歧义)。例子如下:
import ElementPlus from 'element-plus'
import { defineClientConfig } from '@vuepress/client'
import { ElCard } from "element-plus"
export default defineClientConfig({
enhance({ app }) {
if (!__VUEPRESS_SSR__) {
import('element-plus/es/locale/lang/zh-cn').then(module => {
app.use(ElementPlus, {
locale: module.default,
})
})
import("element-plus/es/components/card/style/css");
}
app.component('el-card', ElCard)
}
});
主要要定义的内容包括:
enhance({ app, router, siteData }){},
setup(){},
rootComponents: [],
enhance用于增强app,router,siteData的内容,我们的例子中通过app.component方法来注册 Vue 全局组件,并在非SSR时引用了部分样式文件。
另外还可以使用 vue-router 提供的Router方法 ,例如,添加导航钩子(通过router参数)
除了enhance,还有setup和rootComponents两个自定义函数。
1)setup
函数会在客户端 Vue 应用的 setup Hook 中被调用。可以定义一些全局的setup操作。
2)rootComponents
是一个组件数组,它们将会直接被放置在客户端 Vue 应用的根节点下。
这两个我们用的比较少,大家如果用到可以参考官方文档。
我们说上面这个图可以往里慢慢用vue加内容了,调好看了就行。那我们至少可以从官方主题中学到什么呢或者说官方主题最重要提供了什么呢?
有这么几点:
1)vuepress插件的引用
官方主题引用了如下的插件,我们开发主题时可以根据需要使用
//该插件会监听页面滚动事件。当页面滚动至某个 _标题锚点_ 后,如果存在对应的 _标题链接_ ,那么该插件会将路由 Hash 更改为该 _标题锚点_ 。
import { activeHeaderLinksPlugin } from '@vuepress/plugin-active-header-links'
//返回页面顶部插件
import { backToTopPlugin } from '@vuepress/plugin-back-to-top'
//该插件简化了 [markdown-it-container](https://github.com/markdown-it/markdown-it-container) 的使用方法
import { containerPlugin } from '@vuepress/plugin-container'
//该插件会为你 Markdown 内容中的外部链接添加一个图标
import { externalLinkIconPlugin } from '@vuepress/plugin-external-link-icon'
//该插件会收集你的页面的 Git 信息,包括创建和更新时间、贡献者等。
import { gitPlugin } from '@vuepress/plugin-git'
//为图片提供可缩放的功能。
import { mediumZoomPlugin } from '@vuepress/plugin-medium-zoom'
//在切换到另一个页面时会展示进度条。
import { nprogressPlugin } from '@vuepress/plugin-nprogress'
//为你的主题提供调色板功能。
import { palettePlugin } from '@vuepress/plugin-palette'
//语法高亮
import { prismjsPlugin } from '@vuepress/plugin-prismjs'
//支持通过useThemeData获得主题信息
import { themeDataPlugin } from '@vuepress/plugin-theme-data'
我们用到的有themeDataPlugin ,prismjsPlugin 和containerPlugin,其他的有些我们用的自己更熟悉的控件。
插件的使用方法是在主题的index.js中增加plugins字段,例如
const { path } = require('@vuepress/utils')
const { containerPlugin } = require('@vuepress/plugin-container')
module.exports = (options) => {
return {
name: 'vuepress-theme-baijiafan',
layouts: {
Layout: path.resolve(__dirname, 'layouts/layout.vue'),
},
plugins: [
containerPlugin({
type: 'carousel',
before: (info) =>
`\n`,
after: () => ' \n'
})
]
}
}
其中,我们引用了containerPlugin,并利用他的功能,定义了一个新的container。
2)对md的扩展。
对md的扩展在我们日常编写页面内容的时候,是非常重要的,如果对比前几章的效果图和上面的效果图,我们可以发现其中如下的内容
如果用官方主题,就会显示为
这些扩展内容就是官方主题使用上述 containerPlugin默认添加的,当我们自主编写主题时,也需要按需求添加多个功能,当然,第八章描述的扩展能力在自定义主题中仍然有效,我们仍然可以利用那些方法定义更多的内容。
3)@theme文件引用
默认主题可以使用@theme/xxx来引用theme文件夹下的部分vue模块,起初我们认为这个是自动的,后来才发现是主题初始化的时候自行添加的,添加方法如下:
//首先对需要引用的文件夹进行扫描,过滤获得需要的文件,然后手工把他map成一个数组
//数组的第一个字段是map之后的名称,比如下面我们要map成@theme/components/xxx
//那第一个字段就是`@theme/components/${file}`,第二个字段是真实的文件路径,注意
//文件和扫描时的前缀的对应关系,最后把数组用Object.fromEntries还原成对象即可。
const fileInThemeRoot = Object.fromEntries(fs
.readdirSync(path.join(__dirname, "components"))
.filter((file) => file.endsWith('.vue'))
.map((file) => [
`@theme/components/${file}`,
path.resolve(path.join(__dirname, "components"), file),
]))
//然后在exports中增加一个alias属性,按下面的写法把fileInThemeRoot添加进去
module.exports = (options) => {
return {
...
alias: { ...fileInThemeRoot},
通过上述alias的定义,我们就可以达到在md文件中import自主控件的目的,例如我们的用法:
---
title: 首页
---
:::: el-card header="用户信息配置"
::::
只需要在md文件中附加script setup,然后import即可,上述例子引用了一个UserConfig,然后在md文件中用html语法进行了引用。
4)目录等其他功能项
官方默认主题支持了文件内容的边栏菜单,我们也可以根据需要生成,例如百家饭的帮助页面左侧有所有文章的链接,就是通过扫描文件自动生成的
如果你有需要扫描所有md文件,并按目录生成菜单的需求,做法如下:
第一步:编写一个扫描所有md文件,并生成菜单的程序
const lodash = require('lodash');
const fs = require('fs');
const glob = require('glob');
const markdownIt = require('markdown-it');
const meta = require('markdown-it-meta');
const { dirname } = require('path');
// 在parent_path和dir(可选)构成的子目录中遍历所有md文件
const getChildren = function (parent_path, dir) {
files = glob
.sync(parent_path + (dir ? `/${dir}` : '') + '/**/*.md')
.reduce((existed, path) => {
// Get the order value
file = fs.readFileSync(path, 'utf8');
child_dir = dirname(path);
dirs = child_dir.split("/");
no = 0;
//按自身需求截取路径,形成目录和文件的多层关系,这个部分应根据自身需求修改
if (dirs.length>2) {
parts = dirs[2].split('_')
if (parts.length > 1) {
no = parseInt(parts[0])
child_dir = parts[1]
}
if(dirs.length>3){//跳过过深级别
return existed
}
}
if (existed[no] === undefined) {
existed[no] = {
title: child_dir,
docs: []
}
}
//读取md文件,达到获取其中meta字段中自定义文件排序、标题、是否置顶等信息的目的
md = new markdownIt();
md.use(meta);
md.render(file);
order = md.meta.order !== undefined ? md.meta.order : 9999;
top = md.meta.top !== undefined ? md.meta.top : false;
tags = md.meta.tags !== undefined ? md.meta.tags : [];
// 跳过指向上层文件夹的条目
path = path.slice(parent_path.length + 1, -3);
// 如果是README,README在vuepress中指向的路径是dir本身,因此这个地方进行了相关处理
if (path.endsWith('README')) {
path = path.slice(0, -6);
}
title = md.meta.title ? md.meta.title : path;
existed[no].docs.push({
path,
title,
order,
top,
tags
});
return existed
}, []);
//对输出进行排序,可以根据需要修改,这里是根据order字段排序,相同时再根据title排序。
files.forEach(dir => {
dir.docs = lodash.sortBy(dir.docs, ['order', 'title']);
})
return files
};
module.exports = {
getChildren,
};
上面的程序会扫描指定的目录中的所有md文件,并根据文件名称和文件中指定的附加信息进行排序。
第二步:在主题的index.js中引用上述文件,并指定文件夹获取目录信息,获取到的信息通过themeDataPlugin放到themeData中。
const { getChildren } = require("./getchild")
const { themeDataPlugin } = require('@vuepress/plugin-theme-data');
module.exports = (options) => {
return {
...
plugins: [
themeDataPlugin({
themeData: {
docs: getChildren("docs")
}
}),
]
}
}
第三步:在md文件中通过$theme引用该docs属性。
{{$theme.docs}}
然后就可以基于这个数据做样式了。
上面就是官方主题中我们认为比较有特别,应该在自定义主题中实现的功能,当然这个见仁见智,看各自需求,另外我们自己还定义了以下的一些功能:
1) 通过代码添加页面
有些页面我们不想通过md文件添加,可以采用下面的办法:
在主题index.js中,通过以下字段添加页面
const { createPage } = require('@vuepress/core');
module.exports = ({ themePlugins = {}, ...localeOptions }) => {
return {
...
async onInitialized(app) {
const login = await createPage(app, {
path: '/login',
// set frontmatter
frontmatter: {
layout: 'Login'
},
})
// push the register to app.pages
app.pages.push(login)
},
在onInitialized中,首先通过createPage创建了一个页面对象,然后把他push到了app的pages属性中。
上面确实会达到生成一个login页面的效果,编译的结果是,他会生成一个login目录,然后生成一个index.html的静态页面。如果你对这个目录结构有看法,就只有通过创建md文件的方式来创建页面了。
2)定义附加路径
百家饭在开发中还为同一个页面定义了动态路由,把所有的/api/detail/:id都转到了/api/detail页面,在页面中再通过query获得id,达到动态渲染一组相同呈现,不同数据的页面的目的。(该功能需要后台支持,例如配置nginx路由reweite或者通过后台服务器配置路由)。
这个功能按道理后台配好转发就好了,但是因为vuepress2会进行一些判断,所以需要增加以下配置,同样在主题的index.js中增加:
module.exports = ({ themePlugins = {}, ...localeOptions }) => {
return {
...
extendsPage: (page, app) => {
if (!app.env.isDev && page.filePathRelative == "api/detail.md") {
page.path = "/api/detail/:id"
}
},
如果发现是detail.md文件,将他的路径设置为/api/detail/:id,再配合vue的动态路由功能,达到获取id的目的:
好了,自定义主题的内容就差不多到这了,差不多vuepress2的内容也差不多结束了,下一篇介绍以下vuepress2升级过程中发生的一些变动,方便大家调试升级问题。