VuePress01 - 运行原理研究
服务端视角
启动
启动时首先使用npm run docs:dev命令,即调用的packages.json文件中的vuepress dev docs命令。

vuepress指向@vuepress/core。

创建了一个.node/App程序(即服务端),并且启动了createApp(options),app.process、app.dev()等几个命令。
由于我们直接使用vuepress run dev启动的,所以options为空。

App.js
App.js文件中就是一个 App 类,导入了很多依赖,比如生成Markdown的,生成Page组件的,以及与dev、build相关的进程函数。
仔细看之前的CreateApp(options)函数,其实就是App(options)函数,经过该函数进行初始化。

app.process()函数中用了异步函数,主要用于解析页面之类。

DevProcess
dev函数中主要是使用DevProcess进行,并且会监听本地文件的变动,这里的本地文件是被程序特定的一些文件,主要是markdown、vue之类的。
与之对比,可以看一下生产环境的启动函数。

可以看到,要简洁地多,主要是就是运行,然后渲染。

再看一下这个DevProcess函数。

使用了events包进行事件驱动,而且所有的webpack打包都是在这里进行的,详见如下:

文件监控
接下来看它的监控范围,首先是对.js、.md和.vue的监控。

可以看到.md是根目录下全局监控,而.vue只限于在.vuepress/components下定义的。
其次是对.vuepress文件夹下的config文件进行监控,支持.js、.yml和.toml三种格式,是写死的,所以.yaml、.json这两种格式是不被监控的,这里写的有点死了,用正则会更好一些,不过如果想监控自己的其他文件可以配置siteConfig.extraWatchFiles字段,参见:配置 | VuePress(#extraWatchFiles)
- 我的项目配置的就是 - .yaml格式,在了解到- .yaml配置文件不在监控列表内后,我改成了- .yml结尾,结果发现- js-yaml不能完整解析- .yml格式。
- 基于此,我去修改了 - vuepress的源码,以使它支持- config.yaml的监控。
- 但最后我发现了 - siteConfig.extraWatchFiles这个配置,所以我最后采用了这个,基于此,我现在的- config.js里就多了如下的监控文件:

此外,还对 frontmatter 进行监控,如下:

打包与服务器
接着就是使用webpack进行一些资源的打包与解析,比如html。

接着是使用express创建服务器。

可以看到,公共资源都在./public下,而以/结尾的如果匹配不到文件,则会自动寻找index.html文件,我之前在配置sidebarRecursive的时候,就经常出现index.html匹配不上的报错,我后来的做法是给每个有路由的文件夹都配一个index.md文件占位,并且在解析sidebarRecursive children的时候排除这个.md文件(也可以不排除,就是像官方一样留空就可以,但这样会使该文件夹下的其他文件与它同层级,这不是我想要的)。
最后是一些host和 port等的配置。

至此,服务器就创建成功了,打开浏览器地址可以看到所渲染出来的默认页面。
那从客户端视角来看,这一切又是如何呈现的呢?
客户端视角
组件依赖图
首先,我们分析一下默认的主题,它位于node_modules/@vuepress/theme-default。
首先使用webstorm生成一下组件的依赖图。

可以看到,所有的一级组件都经由util/index.tsx展开,二级组件再被一级组件所单独或共享使用。不过util/index.tsx中只是定义了各个解耦的被调用函数,直接看并没有什么欧诺个,所以我们还是先从layouts/Layout.vue看起。
Layout
可以看到,组件如官网所述,主要分components、global components、layouts三大块,其中layouts/Layout.vue是默认入口。
这个组件在哪定义为入口的,我还没找到,所以先看组件吧。

在这个Layout.vue中,主要由以下组件构成
.theme-container
    - Navbar
    - Sidebar
        - .sidebarRecursive-mask
        - v-slot:sidebarRecursive-top
        - v-slot:sidebarRecursive-bottom
    - <View>
        - Home
        - Page
            - v-slot:page-top
            - v-slot:page-bottom
其中最关键的就是Home与Page的分支判断,这其实就是对应两种View,而依据就是frontmatter中home字段是否为真。
了解到这点之后就很清楚了,如果我们想多设几种View,只要再这里修改if...else...的逻辑就可以了。
基于此,我尝试将Home和Page分离出来独立到View文件夹内,这样直接dev模式运行是没有问题的。但是!打包发布却出现了严重的问题!
所以,还是乖乖地放在components里面吧。。。
最后,关于父(@parenttheme)子(@theme)主题的引用,请参考:主题的继承 | VuePress :::
VuePress02 - 使用体验分析
设计相关
覆写设定不合理
以页面导航为例,在组件中判断一个页面的上一页或者下一页是什么,需要通过多个层级的多次判断,比如先判断主题有没有上下页,再判断页面是否有上下页,判断的位置隐含在links里或者frontmatter中。
作为vuepress的使用者,我经常需要在console中打印出当前页面的变量,以判断什么变量出于什么作用域范围内,我想说,这不是必要的。如果作者采用更加友好的设计方式,在后端与中端过程中将组件完全覆写,可以大大减少前端的逻辑判断。
也许这会束缚前端vue组件开发的灵活度,但框架毕竟是框架,使用vuepress的开发者应有能力在程序的配置阶段把一切设定好(如果框架机制允许的话)。也许我更崇尚配置式的框架吧,例如爬虫领域的python框架scrapy。

配置相关
多语言配置(这是真坑)

使用自己的主题
在没有别人主题或者自己暂时没有经验的情况下,可以先把vuepress默认的主题拷贝出来,路径是在node_modules/@vuepress/theme-default,在配置的时候有几点一定要注意(至少是对于新手来说)。
- 将主题文件夹放在docs/.vuepress下,一定一定!不然dev可以运行,build一定会出错!
- 修改index.tsx里的内容:
module.exports = {
  extend: '@vuepress/theme-default',
};
- 按照官网要求,重命名为vuepress-theme-YOUR-THEME-NAME
- 修改.vuepress/config.js里的内容:
module.exports = {
  theme:  require.resolve('./vuepress-theme-YOUR-THEME-NAME'),
  ...
}
config.yaml 或 config.yml
经过测试,js-yaml对.yml解析的支持有限,因此推荐使用.yaml。
如需使用,请在config.js的extraWatchedFiles字段中加入相应的监控文件路径。
运行相关
navbar 的下划线
在element-ui中,只要成功点击了某导航项的切页,无论是单链接是子链接,下划线都会更新到那一项上。
但我发现vuepress里没有实现,需要解决一下。
例如当我点击了我的博客下拉菜单中某一项后,页面成功跳转,但下划线没了。

与之对比的是,如果是在首页,则会有。

sidebar 的热更新
在我更新我的本地文档时,已经启动的vuepress可以在两秒内监测到内容的变化,并相应地更新前端的呈现,但是对应的siderbar则不会自动更新。
按道理,toc是从文档内容中提取出的,sidebarRecursive基于toc呈现,理应做到同步更新,这是值得优化的一个点。
frontmatter 格式问题

插件相关
@vuepress/plugin-nprogress
这个插件是用来显示页面跳转的时候顶部加载的进度条,如下所示。

当网站刚启动的时候,或者网速很慢的时候,这个进度条就会比较明显。
不过,由于网站以静态内容为主,正常情况下,这个条只会一闪而过。
永久链接
官网对于永久链接的设定不甚详细。
如下,我也搞不清这个文章发布是怎么算的。

翻了一下源代码,在node_modules/@vuepress/shared-utils/lib/getPermalink.js里有如下,生成链接的函数接收五个参数,其中第三个参数就是时间,这些模板变量都是从这个时间中解析出来的。

接着定位到了node_modules/@vuepress/core/lib/node/Page.js

而这个this.date呢,是根据这个函数生成的。

最后到了node_modules/@vuepress/core/lib/node/util/index.tsx

注意这个DATE_RE,我拷贝出来一下: /(\d{4}-\d{1,2}(-\d{1,2})?)-(.*)/,所以这个程序的意思就是,依次从 frontmatter.date、文件名和文件夹名中提取YYYY-M?M-D?D-FILENAME的格式,一旦提取到,就会生成它的永久链接。
尴尬了,我原先素材的取名都是YYMMDD的,例如:

于是,写了个脚本,重命名了一下。

接下来就是永久链接的路由配置,由于我们的 nav 和 sidebar 是自己自己写的,所以需要自己对应上。
按照规则,现在的文章路径,以2020-05-26-杨季.md为例,变成了/2020/05/26/杨季。
但专辑名也要相应地换一下,目前文件夹名是A01-考研专辑,对应的index.md,专辑名显然不能换,但我们也想用一个好记的路径,因此直接在index.md里设置 frontmatter,这里就不要用date变量了,而是直接permalink,如下。

但这样做有个弊端,我们在生成menus和sidebar时需要先解析文件内容,与我一开始简单地通过文件路径生成网络路由的初衷不符,虽然vuepress本身便是基于此的。
config.js中,配置永久路径的模板变量。
这样才能被vuepress解析。 :::
路由的中文转义
程序内部对每个路由会有三种形式:relativePath | regularPath | path,其中relativePath是本地文件路径,regularPath是转义后的路径,而path顾名思义应该是最终的路径。
此外,还有一个permanlink,是用户指定的文件的最终访问路径。
在我们的程序中,直接按照文件路径配置 nav 和 sidebar 是比较符合 common sense 的,操作起来也是比较容易,只要用fs遍历或者递归即可。这样的操作,它本身不应该有问题,如果有问题,那一定是框架的问题。
未配置 permalink 的结果
✅ vuepress 生成了.html结尾的目标节点,该路径的中文形式就是本地文件路径,具体网络路径为/zh/S01-%E6%96%87%E9%9B%86/G01-%E7%A0%94%E7%A9%B6%E7%94%9F%E5%8D%87%E5%AD%A6%E6%A1%88%E4%BE%8B/A02-%E7%95%99%E5%AD%A6%E4%B8%93%E8%BE%91/2020-06-12-%E7%8E%8B%E9%80%B8%E6%88%90.html
✅ 当直接访问上条.html结尾的路径时,会跳转到没有index.html结尾的路径,对应的就是本地文件路径。

✅ 最后,若直接访问中文路径,或者访问该转义路径,是等价的。

配置了 permalink 的结果
✅ 此时,如果直接访问/考研专辑/或者/%E8%80%83%E7%A0%94%E4%B8%93%E8%BE%91/是可以成功跳转的。

❌ 然而虽然下面的路径/%E8%80%83%E7%A0%94%E4%B8%93%E8%BE%91/index.html,根据路由显示也是跳转上面的路径,但是事实上,当我们输入网址后,会被浏览器解析成/考研专辑/index.html,从而失配。

❌ 紧接着,即我们程序中设定的本地文件路径映射,虽然也显示跳转,但会直接转义,导致跳转失败。

值得注意的是,如果文件路径都是英文,配置了permalink之后,路径的跳转都没问题。
有问题的点在于,当使用转义后的本地文件路径去访问时,浏览器指向了转义前的路径(即中文),而该路径是没有跳转项的,导致跳转失败。
所以,问题的解决就在于,能否配置一个转义前(中文)的本地文件路径,使之跳转到最终的 permalink。
或者,不要转义本地文件路径。
修改源代码
如下,对node_modules/@vuepress/core/lib/node/internal-plugins/routes.js转义后的路径重新解码,保证原文件路径对最终路径的映射,即可成功解决中文路径跳转问题。

总结、担忧与考虑
其实标准化 url 也是很好的一种设计,能让程序更加地稳健,但却出现了我遇到的 permalink 强行跳转导致转义失配的结果。我解决的方法是对标准化之前的文件路径建立映射。
在我的项目中,目前还不存在由于 url 编码而产生歧义的可能,其他项目可能要自行把握。也希望看到更优秀的解决方案!
VuePres03 - 我为什么放弃了 VuePress
选择 vuepress 的理由
放弃 vuepress 的理由
展望
回顾
弃更,落差太大了。