vitepress静态站点生成
介绍
vitepress 是一个静态站点生成器,专为构建快速、以内容为中心的站点而设计。
它能基于markdown文件生成静态网页!
现在,它已经有完善的中文文档了,可以快速上手构建,以编写markdown文档的形式,构建以内容为中心的网站。例如 vite, vue 等官方文档。
TIP
vitepress 使用 Markdown-it 作为解析器,使用 Shiki 来高亮不同语言语法。让文章内容更加“随心所欲”。
还可以在 Markdown 使用 Vue,除了html,vue插值语法及指令,还可以在 markdown 中嵌入组件、样式、脚本,使用css预处理器!把静态的markdown变成了vue sfc!
比方说,在文章中,你可以写着写着,突然插入一个demo:
学习路线
快速上手
浏览官网的快速上手部分,先创建一个demo,本地运行查看效果。
基本概念
了解以下基本概念
- 路由
VitePress 使用基于文件的路由:每个 .md 文件将在相同的路径被编译成为 .html 文件
了解路由
- 主题
可以简单理解为网站布局,不改布局的话先直接跳过吧,有了全面的了解后再来研究如何自定义主题
配置
1. 站点配置
站点配置可以定义站点的全局设置,如根目录、lang、title、head等
其它的配置优先级往后放,后面再浏览选用,如 cleanUrls: true 可以删除url中的.html后缀
2. frontmatter配置
frontmatter配置是页面配置,可以用在页面中覆盖站点配置。除此之外,还有很多页面配置,例如,它可以选择页面的布局,默认生成的 index.md 中,使用了 hero 布局,其它页面默认使用 doc 布局。其它的辅助功能都可以快速浏览下。
3. 主题配置
关于如何组织分类内容,主题配置是首要学习的:
- 导航栏
导航栏:需要注意导航栏的嵌套语法
- 侧边栏
侧边栏:侧边栏更复杂一点。它可以是页面链接组成的一维数组,体现在页面上就是所有链接顺序展示在侧边栏上。可以将链接嵌套(最多6层)实现多级分组,还可以设置多侧边栏,搭配菜单使用。
export default {
cleanUrls: true,
themeConfig: {
sidebar: [
{ text: 'Page 1', link: '/route/path/page1' },
{ text: 'Page 2', link: '/route/path/page2' },
// ...
]
}
}export default {
cleanUrls: true,
themeConfig: {
sidebar: [
{
text: 'Page 1',
items: [
{ text: 'Page 1-1', link: '/route/path/page1-1' },
{ text: 'Page 1-2', link: '/route/path/page1-2' },
// ...
]
},
{ text: 'Page 2', link: '/route/path/page2' },
// ...
]
}
}export default {
cleanUrls: true,
themeConfig: {
sidebar: {
'/front-end/js/': [
{
text: 'Page 1',
items: [
{ text: 'Page 1-1', link: '/front-end/js/page1-1' },
{ text: 'Page 1-2', link: '/front-end/js/page1-2' },
// ...
]
},
{ text: 'Page 2', link: '/front-end/js/page2' },
// ...
],
'/front-end/css/': [
{ text: 'Page 3', link: '/front-end/css/page3' },
// ...
],
'/back-end/': [
{ text: 'Page 4', link: '/back-end/page4' },
// ...
],
// ...
}
}
}4. 写作
vitepress 内置了丰富的 Markdown 扩展,例如代码块、代码组、容器、代码高亮等等。你甚至可以在 markdown 文件中导入其它文件内容、包含其它 markdown 文件。
除了上面的 markdown 扩展,vitepress 允许你在 markdown 中直接使用任何 Vue 功能,包括动态模板、使用 Vue 组件或通过添加 <script> 标签为页面的 Vue 组件添加逻辑。
5. 其它配置
其它的还有社交链接、深色模式等等,选配方便
案例分析
记录一些实践中遇到的功能场景
1. Markdown 中使用 Vue 组件
.vitepress
├─ theme
│ ├─ components
│ │ └─ DemoIframe.vue # Demo 组件
│ └─ index.js
└─ config.js创建 DemoIframe 组件并全局声明
import DefaultTheme from 'vitepress/theme'
import DemoIframe from './components/DemoIframe.vue'
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('DemoIframe', DemoIframe)
},
}DemoIframe.vue
这里使用 vitepress 提供的引入语法引入代码
<<< @/.vitepress/theme/components/DemoIframe.vue
<script setup>
import { useData } from 'vitepress'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import FullScreen from './FullScreen.vue'
/**
* Demo via iframe
*
* @example
* <DemoIframe title="Demo Title" src="https://play.vuejs.org/" />
*/
const props = defineProps({
height: { type: [String, Number], default: 500 },
src: { type: String, required: true },
title: { type: String, default: 'Demo via ifame' },
// 仅在第三方网站模式依赖于class的时候使用
watchTheme: { type: Boolean, default: false },
})
const { isDark } = useData()
const targetRef = ref()
/** @type {IntersectionObserver | null} */
let observer = null
const inView = ref(false) // 状态:是否已进入视口
onMounted(() => {
if (props.watchTheme) watch(isDark, reloadIframe)
observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
inView.value = true
observer?.unobserve(targetRef.value)
observer?.disconnect()
}
},
{ threshold: 0.1 }
)
if (targetRef.value) observer.observe(targetRef.value)
})
onUnmounted(() => {
if (observer) observer.disconnect()
})
const iframeRef = ref()
const loaded = ref(false)
function reloadIframe() {
if (!loaded.value || !iframeRef.value?.src) return
const url = new URL(iframeRef.value.src)
url.searchParams.set('_theme_refresh', String(Date.now()))
iframeRef.value.src = url.toString()
}
</script>
<template>
<FullScreen>
<div ref="targetRef" class="demo-wrap">
<iframe
v-if="inView"
ref="iframeRef"
:height
style="width:100%"
scrolling="no"
:title
:src
frameborder="no"
loading="lazy"
allowtransparency="true"
allowfullscreen="true"
@load="loaded = true"
>
Failed to load this demo({{ src }}).
</iframe>
<div v-show="!loaded" class="placeholder" />
</div>
</FullScreen>
</template>
<style lang="scss" scoped>
.demo-wrap {
position: relative;
}
.placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.04) 33%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.04) 66%);
background-color: var(--tg-bg-color);
background-size: 300% 100%;
animation: loading 1s infinite linear;
}
.dark .placeholder {
background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.04) 33%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.04) 66%);
}
.fullscreened .demo-wrap, .fullscreened .demo-wrap iframe {
height: 100%;
}
@keyframes loading {
0% {
background-position: right;
}
}
</style>正文在此处下插入了组件DemoIframe:
<DemoIframe title="Demo Title" src="https://play.vuejs.org/" />
效果如下:
2. Markdown 内容替换
markdown扩展 VS markdown内组件 VS markdown内容替换
- Vitepress 提供的 Markdown 扩展会解析指定格式的 md 内容
- Vitepress 在解析 markdown 文件时,文件内使用的 Vue 组件也会被它编译
- 在 markdown 渲染器中更改 markdown 内容
有时,我们希望批量得对 Markdown (渲染后的)内容做些自定义更改或替换,例如:
- 文章页标题默认为 markdown 的一级标题,如何在标题后添加自定义的标签呢?
- 如何在标题下批量插入一行文章元数据信息呢?(发布时间、字数、点击数等等)
- 如何统一为文章内的图片添加图片查看器?
这类自定义需求下,如果还通过前两种方式:
一方面书写繁琐,只能是通过手动添加 Vue 组件,添加在每个需要的 markdown 文件中。
另一方面,只能是“插入”,无法“更改”、“替换”,且无法插入到 markdown 渲染单元内部。
export default {
// ...
markdown: {
// 对markdown中的内容进行替换或者批量处理
config: (md) => {
// 创建 markdown-it 插件
md.use((md) => {
// 重写标题关闭标签的渲染规则
md.renderer.rules.heading_close = (tokens, idx, options, env, slf) => {
if (tokens[idx].tag !== 'h1') return slf.renderToken(tokens, idx, options)
// REQ1:标题内追加标签组件(TitleBadge)
let htmlResult = '<TitleBadge />' + slf.renderToken(tokens, idx, options)
// REQ2:标题下添加文章元数据组件(DocTitleMeta)
htmlResult += `<doc-title-meta />`
return htmlResult
}
// REQ3:自动为所有图片添加 v-viewer 查看器支持
const defaultImageRender = md.renderer.rules.image
md.renderer.rules.image = (tokens, idx, options, env, slf) => {
const imgHtml = defaultImageRender
? defaultImageRender(tokens, idx, options, env, slf)
: slf.renderToken(tokens, idx, options)
// 用 ClientOnly 和 v-viewer 容器包装图片
return `<ClientOnly><div v-viewer>${imgHtml}</div></ClientOnly>`
}
})
}
}
}import DefaultTheme from 'vitepress/theme'
import DocTitleMeta from './components/DocTitleMeta.vue'
import TitleBadge from './components/TitleBadge.vue'
import useViewer from './useViewer'
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('DocTitleMeta', DocTitleMeta)
app.component('TitleBadge', TitleBadge)
useViewer(app)
},
}3. 字体更换
import DefaultTheme from 'vitepress/theme'
import DefaultTheme from 'vitepress/theme-without-fonts'
import './styles/fonts.css'
import './styles/custom.css'
export default DefaultTheme@font-face {
font-family: 'ComicShannsMono Regular';
src: url(./ComicShannsMono-Regular.ttf);
}:root {
--vp-font-family-base: 'ComicShannsMono Regular', '思源黑体', '微软雅黑'; /* normal text font */
--vp-font-family-mono: 'ComicShannsMono Regular'; /* code font */
}其实也就是简单的样式更改,关键在于规范引入样式
4. 深色主题样式补充
基于上一小节,添加样式及深色样式。以滚动条样式为例:
import DefaultTheme from 'vitepress/theme-without-fonts'
import Layout from './Layout.vue'
import './font/font.css'
import './style/custom.css'
import './style/scrollbar.css'
export default {
...DefaultTheme,
Layout,
}:root {
/* 代码高亮bg */
--vp-code-line-highlight-color: rgb(96, 215, 255, 0.2);
/* 滚动条 */
--scrollbar-bg: rgb(239, 239, 239);
--scrollbar-thumb-bg: rgba(144, 147, 153, 0.3);
--scrollbar-thumb-hover-bg: rgba(144, 147, 153, 0.5);
--scrollbar-thumb-active-bg: rgba(144, 147, 153, 0.4);
--scrollbar-thumb-border-color: rgba(255, 255, 255, 0.4);
--scrollbar-corner-bg: rgba(255, 255, 255, 0.3);
--scrollbar-corner-hover-bg: rgba(144, 147, 153, 0.15);
}
.dark {
--vp-code-line-highlight-color: rgb(0, 110, 146, 0.2);
--scrollbar-bg: rgb(66, 66, 66);
--scrollbar-thumb-bg: rgba(192, 192, 192, 0.3);
--scrollbar-thumb-hover-bg: rgba(192, 192, 192, 0.5);
--scrollbar-thumb-active-bg: rgba(192, 192, 192, 0.4);
--scrollbar-thumb-border-color: rgba(0, 0, 0, 0.4);
--scrollbar-corner-bg: rgba(192, 192, 192, 0.3);
--scrollbar-corner-hover-bg: rgba(192, 192, 192, 0.15);
}/* scrollbar */
::-webkit-scrollbar-track-piece {
background: var(--scrollbar-bg);
}
::-webkit-scrollbar {
width: 12px !important;
height: 12px !important;
background: transparent;
}
::-webkit-scrollbar:hover {
background: rgba(128, 128, 128, 0.2);
}
/* thumb */
::-webkit-scrollbar-thumb {
border: 1px solid var(--scrollbar-thumb-border-color) !important;
background-color: var(--scrollbar-thumb-bg) !important;
z-index: 2147483647;
border-radius: 12px;
-webkit-border-radius: 12px;
background-clip: content-box;
transition: 0.3s background-color;
cursor: pointer;
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover-bg) !important;
}
::-webkit-scrollbar-thumb:active {
background-color: var(--scrollbar-thumb-active-bg) !important;
}
/* corner */
::-webkit-scrollbar-corner {
background-color: var(--scrollbar-corner-bg);
border: 1px solid transparent;
}
::-webkit-scrollbar-corner:hover {
background-color: var(--scrollbar-corner-hover-bg) !important;
}常见适配方案均可
5. 接入评论
utterances:A lightweight comments widget built on GitHub issues. Use GitHub issues for blog comments, wiki pages and more!
接入很方便,选择好选项后,将 script 拷贝至页面中即可。vitepress需要多做一点,因为页面是对应markdown文件的,不会有人愿意在每个markdown中添加上面的 script 吧?
这里就需要稍微更改下vitepress的默认主题了(扩展默认主题)
初始目录结构:
.vitepress
├─ theme
│ └─ index.js # 主题入口
└─ config.js # 配置文件下面从上至下展示如何更改
index.js中如下覆盖默认主题的布局:
import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'
export default {
...DefaultTheme,
Layout,
}<script setup>
import { useData } from 'vitepress'
import Theme from 'vitepress/theme'
import Comment from './Comment.vue'
const { Layout } = Theme
const { page } = useData()
</script>
<template>
<Layout>
<template #doc-after>
<Comment :key="page.relativePath" />
</template>
</Layout>
</template><script setup>
import { onMounted, watch } from 'vue'
import { useData } from 'vitepress'
const { isDark } = useData()
onMounted(() => {
const script = document.createElement('script')
script.src = 'https://utteranc.es/client.js'
script.setAttribute('repo', 'owner/repo')
script.setAttribute('issue-term', 'pathname')
script.setAttribute('theme', isDark.value ? 'github-dark' : 'github-light')
script.setAttribute('label', 'comment')
script.async = true
script.crossOrigin = 'anonymous'
document.querySelector('#comment').appendChild(script)
watch(isDark, updUtteranceTheme)
})
function updUtteranceTheme(isDark) {
const utterances = document.querySelector('#comment iframe')
if (!utterances) return
utterances.contentWindow.postMessage(
{ type: 'set-theme', theme: isDark ? 'github-dark' : 'github-light' },
'https://utteranc.es'
)
}
</script>
<template>
<div id="comment"></div>
</template>实际上,Layout.vue 中并没有重写 Layout,仅仅只是通过其插槽渲染评论组件而已。当然,你完全可以重写以完全自定义。更多插槽参见官网扩展默认主题-布局插槽
最终的目录结构:
.vitepress
├─ theme
│ ├─ Comment.vue # 评论组件
│ ├─ Layout.vue # 扩展的默认主题布局
│ └─ index.js # 主题入口
└─ config.js # 配置文件6. PWA
相关链接:
install
npm i @vite-pwa/vitepress -D
# yarn
yarn add @vite-pwa/vitepress -D
# pnpm
pnpm add @vite-pwa/vitepress -Dusage
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
import { withPwa } from '@vite-pwa/vitepress'
export default withPwa(defineConfig({
/* your VitePress options */
/* Vite PWA Options */
pwa: {}
}))export default defineConfig({
sitemap: {
hostname: 'https://example.com'
}
})