使用 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.
完整日志如下:
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 中的文章目录时会触发这个错误,并且,如果 activeHeaderLinks
为 true
时,只要页面滚动时触发了文章目录的切换,也会触发该错误。
该错误发生后,页面很多地方的点击都不能正常工作,如:
- 点击 sidebar 切换页面时不再显示当前页面的目录;
- 切换到没有 sidebar 的页面时仍然会显示之前页面的 sidebar;
- 之前一度以为 VuePress 就是这样显示的。
- 在手机上点击 sidebar 中的目录时不会自动关闭 sidebar;
- 貌似也会影响 navbar 的点击(我当时没有配置菜单,但是官方的 issue 中有提到这个现象);
一度以为是我的 sidebar 使用方式不对导致的,不过根据官方 issue [1:1] 中的 Bug 报告,可能是由于 VuePress 和 Vue 3.4 的兼容性问题导致的。回复中暂时的对策就是使用 3.3.x 版本的 Vue。
npm install vue@3.3.13 --save-dev
另外在使用云效 Flow 部署时发现 build 的静态文件仍然有这个问题,后来尝试了几次,做了如下改动:
将 package.json 中的
"vue": "^3.3.13",
改成了"vue": "3.3.13",
。删除了提交到仓库中的
package-lock.json
文件。
具体是哪个改动有效,我也不太确定。
2. FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
这个是内存不够导致的。由于是迁移过来的博客,文章比较多,大概有 1100 多个页面。
参考网上的文章,本地开发时采用的是使用 increase-memory-limit 插件的方式。
安装 increase-memory-limit 组件
bashnpm i increase-memory-limit -D
在 package.json 的
scripts
中添加 fix-memory-limit 脚本json"fix-memory-limit": "cross-env LIMIT=8192 increase-memory-limit"
运行一次 fix-memory-limit 脚本
本地开发只需要运行一次就可以了,不必每次都运行。
bashnpm run fix-memory-limit
替换
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 中正常打包:
"docs:build": "cross-env NODE_OPTIONS='--max_old_space_size=4096' vuepress build docs",
3. 年月日目录结构时的 sidebar 设置
默认主题不支持标签、分类之类的设置,而 config 文件中的 sidebar 配置貌似只支持路径为单位。年月日为目录结构时,同一个系列的文章可能会分散在多个目录。
最终采用的方案是在文章的 frontmatter 中配置 sidebar。
写法示例如下:
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。
最后采用的方案是:
- 每个页面通过 frontmatter 中的
prev
和next
指定前后页面; - 在每个年和月的目录下添加一个索引页面。
中间也尝试使用了 vuepress-plugin-auto-catalog 插件,但是效果不太理想,自动生成的索引页面中有很多重复的内容,不知道是什么原因导致的。
由于文章较多,自己手动生成显然太过麻烦,于是使用 ChatGPT 生成了一段 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 追记
最近一段时间没更新博客,今天发现项目发布失败了。报了如下错误:
[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 移除了好多配置项。官方文档里已经找不到我这里一直在使用的 prev
、next
和 sidebar
了,只剩下了几个基础的配置,导致可自定义性低了好多。
上面这个错误只要移除 frontmatter 里的 prev
、next
后就可以修复了。
还是希望小版本升级时不要有这么大的改动,不然文章过多时,更新起来实在是很麻烦。
更新过程中发现新版的 create-vuepress 提供了一个 blog 模式的选项。该模式下默认提供了博客列表、分类、标签和时间线页面,比较适合博客的场景。不过功能比较基础,没有分页,在文章、分类比较多时页面展示效果并不好,而且页面也会比较大。上千个文章显示在同一个页面,即使只有文章摘要,页面加载起来还是明显感觉会慢很多。