Skip to content

Vitepress 相关问题优化

非根目录部署

下列配置在./vitepress.config.js中

配置 base

默认情况下,我们假设站点将部署在域名 (/) 的根路径上。如果站点在子路径中提供服务,例如 https://mywebsite.com/blog/,则需要在 VitePress 配置中将 base 选项设置为 '/blog/'。

例:如果你使用的是 Github(或 GitLab)页面并部署到 user.github.io/repo/,请将 base 设置为 /repo/。

取消生成简洁的 URL

注意:

  1. 非根目录时, 需要将cleanUrls 设置成 false, 因为默认情况下,VitePress 会生成简洁的 URL,例如 /vite/bar.html 会被重定向到 /vite/bar。但是,当您在该页面上执行原地刷新时,浏览器会将相对路径与当前 URL 组合,导致它尝试加载 /vite/index.md 这样的路径。

  2. 根目录部署时,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
  • 计算点击位置、页面窗口和弹出框位置大小关系
  • 使用事件总线的方式调用听全文功能
  • 添加背景音乐播放功能

......(代码略)

粤ICP备20009776号