主题
Vitepress 相关问题优化
非根目录部署
下列配置在./vitepress.config.js中
配置 base
默认情况下,我们假设站点将部署在域名 (/) 的根路径上。如果站点在子路径中提供服务,例如 https://mywebsite.com/blog/,则需要在 VitePress 配置中将 base 选项设置为 '/blog/'。
例:如果你使用的是 Github(或 GitLab)页面并部署到 user.github.io/repo/,请将 base 设置为 /repo/。
取消生成简洁的 URL
注意:
非根目录时, 需要将cleanUrls 设置成 false, 因为默认情况下,VitePress 会生成简洁的 URL,例如 /vite/bar.html 会被重定向到 /vite/bar。但是,当您在该页面上执行原地刷新时,浏览器会将相对路径与当前 URL 组合,导致它尝试加载 /vite/index.md 这样的路径。
根目录部署时,cleanUrls 推荐设置成 true, 这样网络路径可以省略.html, 显得更专业。
nginx配置如下
conf
location /vite {
#开启gzip
gzip on;
#低于1kb的资源不压缩
gzip_min_length 1k;
#压缩级别1-9,越大压缩率越高,同时消耗cpu资源也越多,建议设置在5左右。
gzip_comp_level 5;
#需要压缩哪些响应类型的资源,多个空格隔开。不建议压缩图片.
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
#配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
gzip_disable "MSIE [1-6]\.";
#是否添加“Vary: Accept-Encoding”响应头
gzip_vary on;
alias /data/web/vite/;
try_files $uri $uri/ /vite/index.html;
}
路径上带有驼峰法命名?大小写问题导致路由不能识别?
stepFun.html
改为stepfun.html
就可以正常访问了, 原因不明
开启GZIP压缩
修改package.json
json
"scripts": {
"dev": "cross-env NODE_ENV=development vitepress dev docs --port=8732",
"build": "npm run daily-notes && vitepress build docs",
"compress": "node ./docs/.vitepress/compress.js", 添加这行
"postbuild": "npm run compress", 添加这行
"build:docs": "vitepress build docs",
"daily-notes": "node ./scripts/daily-notes.js",
"update:friend": "node ./scripts/update-friend.js",
"preview": "vitepress preview docs --port 8730",
"lint": "prettier --write .",
"prepare": "husky install"
},
对应位置创建compress.js
js
import { promises as fs } from "fs";
import { gzip } from "zlib";
import { promisify } from "util";
import fg from "fast-glob";
const gzipAsync = promisify(gzip);
async function compressFiles() {
try {
const files = await fg(["./dist/**/*.{html,jpg,jpeg,png}"], { onlyFiles: true });
for (const file of files) {
const content = await fs.readFile(file);
const originalSize = content.length;
// Gzip 压缩
const gzipped = await gzipAsync(content);
const gzippedSize = gzipped.length;
if (gzippedSize < originalSize * 0.95) { // 如果压缩效果超过 5%
await fs.writeFile(`${file}.gz`, gzipped);
// console.log(`${file} 已保存为 .gz`);
} else {
// console.log(`${file} 的 Gzip 压缩效果不足 5%,跳过。`);
}
}
console.log("Compression complete.");
} catch (error) {
console.error("Error during compression:", error);
}
}
compressFiles();
即在构建完成后执行压缩操作。
nginx配置同上。
添加字数及阅读时长
自定义布局容器
修改主题布局文件, 增加插槽#doc-before
ts
<script setup>
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
....
</script>
<template>
<Layout v-bind="$attrs">
<!-- 这里指的是文章前, 同时可以在文章内设置readingTime=false来关闭 -->
<template #doc-before v-if="frontmatter.readingTime !== false">
<!-- 计算字数和阅读时间 -->
<ReadingTime />
</template>
<!-- 其他可扩展的插槽 -->
...
</Layout>
</template>
计算字数和阅读时间
ts
<script setup>
import { ref, watch, onMounted, nextTick } from 'vue'
import { useData } from 'vitepress'
const { page } = useData()
// 统计逻辑
const stats = ref({ words: 0, minutes: 0 })
const calculateStats = () => {
// 优先尝试从页面数据中获取内容(如果 theme 或 page 提供了内容字段)
let content = page.value.content
if (!content) {
// 如果没有则通过 DOM 查询获取
content = document.querySelector('.vp-doc')?.textContent || ''
}
// 去除所有空白字符后统计字符数(中文场景下更适合统计“字”)
const wordCount = content.replace(/\s+/g, '').length
const readingTime = Math.ceil(wordCount / 350)
stats.value = {
words: wordCount,
minutes: readingTime
}
}
// 当路由变化时重新计算阅读统计
watch(() => page.value.relativePath, async () => {
await nextTick() // 等待 DOM 更新完成
calculateStats()
})
// 初次挂载后计算一次
onMounted(async () => {
await nextTick()
calculateStats()
})
</script>
<template>
<div class="reading-stats" v-if="stats.words > 10">
<span class="reading-stats-text">
全文共 {{ stats.words }} 字, 预计阅读: {{ stats.minutes }} 分钟
</span>
</div>
</template>
<style scoped>
.reading-stats {
font-size: 0.8em;
text-align: center;
margin: 0px 0 25px;
}
.reading-stats-text {
color: rgb(145, 89, 48);
background-color: rgba(234, 179, 8, 0.14);
padding: 5px 16px;
border-radius: 5px;
}
</style>
不支持数学公式
VitePress 默认不支持 Math 公式,需要手动启用。你需要安装 markdown-it-mathjax3 并在配置文件中启用 Math 支持。
安装依赖:
bash
npm install -D markdown-it-mathjax3
在 .vitepress/config.js 或 .vitepress/config.ts 中启用 Math 支持:
javaScript
import { defineConfig } from 'vitepress'
import mathjax3 from 'markdown-it-mathjax3'
export default defineConfig({
markdown: {
config: (md) => {
md.use(mathjax3, {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']], // 行内公式
displayMath: [['$$', '$$'], ['\\[', '\\]']] // 块级公式
}
})
}
}
})
使用 $$ 包裹代码块
添加听全文功能
原理参考: Web Speech API 实现语音文字互转
我实现了一个自定义组件TextToSpeech.vue
vue
<template>
<div class="tts-container" v-if="showFlag.length > 10">
<button @click="toggleSpeech" class="tts-button">
<span class="music-icon">♪</span>
{{ isSpeaking ? '停止朗读' : '点击朗读' }}
</button>
</div>
</template>
<script setup>
import { useData } from 'vitepress'
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { eventBus } from './event-bus'
const { page } = useData();
const isSpeaking = ref(false);
const synth = window.speechSynthesis;
const utterance = new SpeechSynthesisUtterance();
// 设置语音属性
// utterance.lang = 'zh-CN'; // 不设置在中英文混合时更流畅
utterance.volume = 1;
utterance.rate = 1; // 语速
utterance.pitch = 1; // 音高
// 播放语音
const speak = (text) => {
utterance.text = text;
synth.speak(utterance);
isSpeaking.value = true;
eventBus.isSpeaking = true // 更新事件总线状态
};
// 停止语音
const stopSpeaking = () => {
synth.cancel();
isSpeaking.value = false;
eventBus.isSpeaking = false // 更新事件总线状态
};
// 切换语音状态
const toggleSpeech = () => {
if (isSpeaking.value) {
stopSpeaking();
} else {
// const text = getTextToSpeak();
// if (text) {
// speak(text);
// }
speak(showFlag.value);
}
};
// 获取要播报的文本并处理换行停顿和中英文切换
const getTextToSpeak = () => {
let content = page.value?.content || '';
if (!content) {
content = document.querySelector('.vp-doc')?.textContent || '';
}
// 处理换行符,将换行符替换为适当的停顿标记
content = content.replace(/[\s\u000B\u000C\u000A\u000D\u2028\u2029]/g, ',');
// 处理中英文切换,添加空格以明确语言切换点
content = content.replace(/([a-zA-Z0-9]+)([,。!?])/g, '$1 $2');
content = content.replace(/([,。!?])([a-zA-Z0-9]+)/g, '$1 $2');
console.log(content);
return content.trim();
};
// 在组件挂载时添加页面切换监听
onMounted(async () => {
await nextTick() // 等待 DOM 更新完成
calculateStats()
eventBus.toggleSpeech = toggleSpeech
eventBus.stopSpeaking = stopSpeaking
// document.addEventListener('visibilitychange', handleVisibilityChange);
});
const showFlag = ref('');
const calculateStats = () => {
// 优先尝试从页面数据中获取内容(如果 theme 或 page 提供了内容字段)
showFlag.value = getTextToSpeak()
eventBus.showFlag = showFlag.value.length > 10
}
// 在组件卸载时移除监听
// onUnmounted(() => {
// document.removeEventListener('visibilitychange', handleVisibilityChange);
// });
watch(() => page.value.relativePath, async () => {
await nextTick() // 等待 DOM 更新完成
stopSpeaking();
calculateStats()
})
// 处理页面可见性变化
// const handleVisibilityChange = () => {
// if (document.hidden && isSpeaking.value) {
// stopSpeaking();
// }
// };
// 当语音播报结束时自动停止
utterance.onend = () => {
isSpeaking.value = false;
eventBus.isSpeaking = false // 更新事件总线状态
};
</script>
<style>
.tts-button {
background-color: #64acff;
color: white;
/* position: absolute;
top: -5px;
right: 0; */
font-size: 0.8em;
text-align: center;
padding: 4px 10px;
border-radius: 5px;
cursor: pointer;
}
.tts-button:hover {
background-color: #74a5ff;
}
</style>
自定义右键菜单
- ContextMenu.vue
- 计算点击位置、页面窗口和弹出框位置大小关系
- 使用事件总线的方式调用听全文功能
- 添加背景音乐播放功能
......(代码略)