Skip to content

使用 VuePress 2

🏷️ VuePress

由于服务器费用渐涨,最近尝试将博客迁移到可以使用 OSS 作为站点的静态部署方式,于是就发现了 VuePress。

VuePress 最新的版本是 v2.0.0-rc.0 ,还是 RC 版。使用中遇到了一些问题,不过好在基本功能都可以用。其中一个比较多大的问题在官方 issue 中也找到了一个暂时的解决方案 [1]

之所以选择 VuePress,主要还是感觉默认主题样式整体比较简洁,也支持我之前在自己的 .NET MVC 版本的博客中使用的 markdown-it 插件及其扩展插件。

另外也尝试用了一下 vue-hope 主题 [2] 。虽然功能很强大,更加适合作为博客来展示,但总归觉得颜值还是差了点意思。

这里记录一下最近使用中遇到的一些问题及解决方法。

1. Error: pageData() is called without provider.

完整日志如下:

txt
Error: pageData() is called without provider.
    at usePageData (chunk-DHNSDB55.js?v=b0e43099? [sm]:30)
    at resolveArraySidebarItems (useSidebarItems.js?v=b0e43099? [sm]:79)
    at resolveSidebarItems (useSidebarItems.js?v=b0e43099? [sm]:44)
    at useSidebarItems.js?v=b0e43099? [sm]:24
    at ReactiveEffect.fn (reactivity.esm-bundler.js:992)
    at ReactiveEffect.run (reactivity.esm-bundler.js:176)
    at ComputedRefImpl.get value [as value] (reactivity.esm-bundler.js:1003)
    at triggerComputed (reactivity.esm-bundler.js:195)
    at ReactiveEffect.get dirty [as dirty] (reactivity.esm-bundler.js:148)
    at ComputedRefImpl.get value [as value] (reactivity.esm-bundler.js:1002)

有时候报的是这个错误:Cannot read properties of undefined (reading 'path')

这个是在点击 sidebar 中的文章目录时会触发这个错误,并且,如果 activeHeaderLinkstrue 时,只要页面滚动时触发了文章目录的切换,也会触发该错误。

该错误发生后,页面很多地方的点击都不能正常工作,如:

  • 点击 sidebar 切换页面时不再显示当前页面的目录;
  • 切换到没有 sidebar 的页面时仍然会显示之前页面的 sidebar;
    • 之前一度以为 VuePress 就是这样显示的。
  • 在手机上点击 sidebar 中的目录时不会自动关闭 sidebar;
  • 貌似也会影响 navbar 的点击(我当时没有配置菜单,但是官方的 issue 中有提到这个现象);

一度以为是我的 sidebar 使用方式不对导致的,不过根据官方 issue [1:1] 中的 Bug 报告,可能是由于 VuePress 和 Vue 3.4 的兼容性问题导致的。回复中暂时的对策就是使用 3.3.x 版本的 Vue。

bash
npm install vue@3.3.13 --save-dev

另外在使用云效 Flow 部署时发现 build 的静态文件仍然有这个问题,后来尝试了几次,做了如下改动:

  1. package.json 中的 "vue": "^3.3.13", 改成了 "vue": "3.3.13",

  2. 删除了提交到仓库中的 package-lock.json 文件。

具体是哪个改动有效,我也不太确定。

2. FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

这个是内存不够导致的。由于是迁移过来的博客,文章比较多,大概有 1100 多个页面。

参考网上的文章,本地开发时采用的是使用 increase-memory-limit 插件的方式。

  1. 安装 increase-memory-limit 组件

    bash
    npm i increase-memory-limit -D
  2. package.jsonscripts 中添加 fix-memory-limit 脚本

    json
    "fix-memory-limit": "cross-env LIMIT=8192 increase-memory-limit"
  3. 运行一次 fix-memory-limit 脚本

    本地开发只需要运行一次就可以了,不必每次都运行。

    bash
    npm run fix-memory-limit
  4. 替换 node_modules/.bin 目录下所有 .cmd 文件中的 "%_prog%"%_prog% [3]

    这一步是在 build 时出现 "node --max-old-space-size=8192"’不是内部或外部命令,也不是可运行的程序或批处理文件。 错误时才需要执行。

    .cmd 应该是在 Windows 中才会被用到,估计是 Windows 系统独有的问题。

上面的操作执行后,本地环境就可以正常 build 了。

在云效 Flow 中执行 npm run docs:build 时还是会触发该错误。将编译脚本改为 npm run fix-memory-limit && npm run docs:build 后,打包会卡住。没有看到任何报错信息,不确定是哪里的问题。

最后将 build 脚本改成了如下方式 [4] 才可以在云效 Flow 中正常打包:

json
"docs:build": "cross-env NODE_OPTIONS='--max_old_space_size=4096' vuepress build docs",

3. 年月日目录结构时的 sidebar 设置

默认主题不支持标签、分类之类的设置,而 config 文件中的 sidebar 配置貌似只支持路径为单位。年月日为目录结构时,同一个系列的文章可能会分散在多个目录。

最终采用的方案是在文章的 frontmatter 中配置 sidebar。

写法示例如下:

yaml
sidebar:
- text: Sidebar Title
  children:
  - ../1/blog-1.md
  - ../../11/1/blog-2.md
  - ../../../2023/11/2/blog-3.md
  - ./blog-4.md

缺点就是每次添加文章时,所有相关文章的 frontmatter 都需要修改。

4. 年月日目录结构时的文章索引

由于个人博客的文章内容比较松散,分类也很凌乱,而且又是以年月日为单位的目录结构,不太好像 VuePress 官网一样制作一个比较简单,但是又包含所有文章的 sidebar 和 navbar。

最后采用的方案是:

  1. 每个页面通过 frontmatter 中的 prevnext 指定前后页面;
  2. 在每个年和月的目录下添加一个索引页面。

中间也尝试使用了 vuepress-plugin-auto-catalog 插件,但是效果不太理想,自动生成的索引页面中有很多重复的内容,不知道是什么原因导致的。

由于文章较多,自己手动生成显然太过麻烦,于是使用 ChatGPT 生成了一段 Python 脚本,然后自己再改一改。

点击查看脚本
python
import os
import yaml

base = "docs"

def generate_list_readme():
    # 构建头部内容
    title = "博客归档"
    
    # 构建头部字典
    header = {
        "title": title
    }
    
    # 构建头部字符串
    header_str = yaml.safe_dump(header, sort_keys=False, allow_unicode=True)
    
    # 构建内容字符串
    content = f"# {title}\n\n"

    subdirectories = [subdir for subdir in os.listdir(f"./{base}") if os.path.isdir(f"./{base}/{subdir}") and subdir.isnumeric()]
    subdirectories = sorted(subdirectories, key=lambda x: int(x))
    for subdir in subdirectories:
        year = subdir.split("/")[-1]
        content += f"-   [{year} 年](./{year}/README.md)\n\n"
    
    # 生成 list.md 文件
    readme_content = f"---\n{header_str}---\n\n{content}"
    with open(f"./{base}/years.md", "w", encoding="utf-8") as f:
        f.write(readme_content)

def generate_year_readme(year):
    # 构建头部内容
    title = f"{year} 年"
    sidebar_text = f"博客归档"
    sidebar_children = []
    
    # 遍历子目录,获取子目录下的 README.md 文件相对路径
    yeardirectories = [subdir for subdir in os.listdir(f"./{base}") if os.path.isdir(f"./{base}/{subdir}") and subdir.isnumeric()]
    yeardirectories = sorted(yeardirectories, key=lambda x: int(x))
    for subdir in yeardirectories:
        sidebar_children.append(f"../{subdir}/README.md")
    
    # 构建头部字典
    header = {
        "title": title,
        "sidebar": [
            {
                "text": sidebar_text,
                "children": sidebar_children
            }
        ]
    }
    
    # 构建头部字符串
    header_str = yaml.safe_dump(header, sort_keys=False, allow_unicode=True)
    
    # 构建内容字符串
    content = f"# {title}\n\n"

    subdirectories = [subdir for subdir in os.listdir(f"./{base}/{year}") if os.path.isdir(f"./{base}/{year}/{subdir}") and subdir.isnumeric()]
    subdirectories = sorted(subdirectories, key=lambda x: int(x))
    for subdir in subdirectories:
        month = subdir.split("/")[-1]
        content += f"-   [{title} {month} 月](./{subdir}/README.md)\n\n"
    
    # 生成 README.md 文件
    readme_content = f"---\n{header_str}---\n\n{content}"
    with open(f"./{base}/{year}/README.md", "w", encoding="utf-8") as f:
        f.write(readme_content)

def generate_month_readme(year, month):
    # 构建头部内容
    title = f"{year}{month} 月"
    sidebar_text = f"{year} 年"
    sidebar_children = []
    
    # 遍历子目录,获取子目录下的 README.md 文件相对路径
    subdirectories = [subdir for subdir in os.listdir(f"./{base}/{year}/{month}") if os.path.isdir(f"./{base}/{year}/{month}/{subdir}") and subdir.isnumeric()]
    subdirectories = sorted(subdirectories, key=lambda x: int(x))
    yeardirectories = [subdir for subdir in os.listdir(f"./{base}/{year}") if os.path.isdir(f"./{base}/{year}/{subdir}") and subdir.isnumeric()]
    yeardirectories = sorted(yeardirectories, key=lambda x: int(x))
    for subdir in yeardirectories:
        sidebar_children.append(f"../{subdir}/README.md")
    
    # 构建头部字典
    header = {
        "title": title,
        "sidebar": [
            {
                "text": sidebar_text,
                "children": sidebar_children
            }
        ]
    }
    
    # 构建头部字符串
    header_str = yaml.safe_dump(header, sort_keys=False, allow_unicode=True)
    
    # 构建内容字符串
    content = f"# {title}\n\n"
    for subdir in subdirectories:
        day = subdir.split("/")[-1]
        content += f"## {month}{day}\n\n"
        
        files = [file for file in os.listdir(f"./{base}/{year}/{month}/{subdir}") if file.endswith(".md") and subdir.isnumeric()]
        for file in files:
            file_path = f"./{base}/{year}/{month}/{subdir}/{file}"
            with open(file_path, "r", encoding="utf-8") as f:
                metadata = f.read().split("---\n")[1]
                md_title = yaml.safe_load(metadata)["title"]
                relative_file_path = f"./{subdir}/{file}"
                link = f"[{md_title}]({relative_file_path})"
                content += f"-   {link}\n\n"
    
    # 生成 README.md 文件
    readme_content = f"---\n{header_str}---\n\n{content}"
    with open(f"./{base}/{year}/{month}/README.md", "w", encoding="utf-8") as f:
        f.write(readme_content)

generate_list_readme()
for year in [subdir for subdir in os.listdir(f"./{base}") if os.path.isdir(f"./{base}/{subdir}") and subdir.isnumeric()]:
    generate_year_readme(year)
    for month in [subdir for subdir in os.listdir(f"./{base}/{year}") if os.path.isdir(f"./{base}/{year}/{subdir}") and subdir.isnumeric()]:
        generate_month_readme(year, month)

将脚本放在仓库根目录下运行即可。

运行后会在每个年和月的目录下生成 README.md 文件,最外层的 README.md 由于是首页,所以改为写入到了 years.md 文件。

WARNING

运行脚本会覆盖同名文件,如果之前已经存在这些文件,请注意备份或提交。

代码也比较简单,可以根据需要自行修改即可。

2024-02-22 追记

最近一段时间没更新博客,今天发现项目发布失败了。报了如下错误:

txt
[19:18:17] TypeError: path.split is not a function or its return value is not iterable
[19:18:17]     at normalizeRoutePath (file:///root/workspace/blog-vue-press_HPGY/node_modules/@vuepress/shared/dist/index.js:82:44)
[19:18:17]     at resolveRoutePath (file:///root/workspace/blog-vue-press_HPGY/docs/.vuepress/.temp/.server/app.CPu5aRqi.mjs:10062:26)
[19:18:17]     at resolveRoute (file:///root/workspace/blog-vue-press_HPGY/docs/.vuepress/.temp/.server/app.CPu5aRqi.mjs:10071:21)
[19:18:17]     at getNavLink (file:///root/workspace/blog-vue-press_HPGY/docs/.vuepress/.temp/.server/app.CPu5aRqi.mjs:10933:36)
[19:18:17]     at resolveFromFrontmatterConfig (file:///root/workspace/blog-vue-press_HPGY/docs/.vuepress/.temp/.server/app.CPu5aRqi.mjs:12007:16)
[19:18:17]     at file:///root/workspace/blog-vue-press_HPGY/docs/.vuepress/.temp/.server/app.CPu5aRqi.mjs:12052:26
[19:18:17]     at ReactiveEffect.fn (/root/workspace/blog-vue-press_HPGY/node_modules/@vue/reactivity/dist/reactivity.cjs.prod.js:927:13)
[19:18:17]     at ReactiveEffect.run (/root/workspace/blog-vue-press_HPGY/node_modules/@vue/reactivity/dist/reactivity.cjs.prod.js:162:19)
[19:18:17]     at get value [as value] (/root/workspace/blog-vue-press_HPGY/node_modules/@vue/reactivity/dist/reactivity.cjs.prod.js:939:109)
[19:18:17]     at file:///root/workspace/blog-vue-press_HPGY/docs/.vuepress/.temp/.server/app.CPu5aRqi.mjs:12066:44

最后发现是 VuePress 更新导致的,貌似 frontmatter 移除了好多配置项。官方文档里已经找不到我这里一直在使用的 prevnextsidebar 了,只剩下了几个基础的配置,导致可自定义性低了好多。

上面这个错误只要移除 frontmatter 里的 prevnext 后就可以修复了。

还是希望小版本升级时不要有这么大的改动,不然文章过多时,更新起来实在是很麻烦。

更新过程中发现新版的 create-vuepress 提供了一个 blog 模式的选项。该模式下默认提供了博客列表、分类、标签和时间线页面,比较适合博客的场景。不过功能比较基础,没有分页,在文章、分类比较多时页面展示效果并不好,而且页面也会比较大。上千个文章显示在同一个页面,即使只有文章摘要,页面加载起来还是明显感觉会慢很多。


  1. [Bug report] Vuepress cannot use Responsive sidebar & navbar in Vue 3.4 or higher ↩︎ ↩︎

  2. vuepress-theme-hope ↩︎

  3. --max-old-space-size=8192 报错 ↩︎

  4. Vue-cli3 执行 serve 和 build 命令时 nodejs 内存溢出问题及解决 ↩︎