VuePress 会在每个标题左侧都加一个锚点链接: - 图1
点击后链接变为:
http://ts.yayujs.com/learn-typescript/handbook/TheBasics.html#降级-downleveling
此时刷新下页面,会发现,页面并不能正确的定位到锚点位置,而且打开开发者工具,还会看到报错:
VuePress 会在每个标题左侧都加一个锚点链接: - 图2
那这个问题该怎么解决呢?

错误定位

在线上由于代码都是压缩混淆后的,不好排查,我们在本地运行项目,然后查看报错信息:
VuePress 会在每个标题左侧都加一个锚点链接: - 图3
可以看到错误是来自于 vuerepss-plugin-smooth-scroll 这个插件中,我们点击查看具体的源码:
VuePress 会在每个标题左侧都加一个锚点链接: - 图4
可以看到,报错是来自于 document.querySelector(to.hash) 这句,为什么会在这里报错呢?
这是因为对于每一个标题,VuePress 都会生成这样一个 DOM 结构:
VuePress 会在每个标题左侧都加一个锚点链接: - 图5
可以看到h2标签的id是以 hash 值命名的,如果我们的 hash 是纯英文,并没有什么问题,但是现在我们的 hash 中有中文,由于中文被编码,document.querySelector报错。

官方方案

这个问题这么容易复现,且这么明显,我想肯定有人提了 issue,查看 VuePress 的 issue,可以看到在 2020 年 10 月 3 号就有人提了 PR,并且 merge 了。
那为什么还会报错呢?
如果我们去掉我们目前在用的 reco主题,我们就会发现,页面并不会有报错,只不过依然不能定位到锚点而已。
实际上,我们的报错是来自于我们使用的 reco 主题使用的 vuerepss-plugin-smooth-scroll 插件。

社区方案

那成吧,那搜索看下其他人是否也遇到过这个问题吧,可以查到这样一个方案
新建一个 docs/.vuepress/theme/layouts/Layout.vue文件,代码写入:

  1. <script>
  2. export default {
  3. methods: {
  4. scrollTo(selector) {
  5. if (!selector || selector === '#') return
  6. const el = document.querySelector(decodeURIComponent(selector))
  7. if (el && el.offsetTop) {
  8. window.scrollTo(0, el.offsetTop)
  9. }
  10. }
  11. },
  12. mounted() {
  13. this.scrollTo(location.hash)
  14. }
  15. }
  16. </script>

使用后,页面是不报错了,但会发现样式全部丢失了,因为使用这种方式相当于新建了一个主题,vuePress 已经改用我们的自定义主题了。
不过 VuePress 提供了主题继承的功能,新建一个 docs/.vuepress/theme/index.js,代码写入:

  1. module.exports = {
  2. extend: '@vuepress/theme-default'
  3. }

样式是恢复了一些,但是因为我们本来使用的是 reco 主题,现在改成了继承 vuepress 默认主题,reco 主题带来的一些功能就全丢失了,那我们能继承 reco 主题吗?
答案是不能,这在 VuePress 官方文档的「主题的继承 」章节里有讲到:
主题继承暂时不支持高阶继承,也就是说,一个派生主题无法被继承。
所以为了解决这个方案,就要放弃 reco 主题,而放弃了 reco 主题,本来也不会有报错了……
那我们换个方案吧。

自己动手

成吧成吧,我自己解决。
梳理下当下的问题,当访问带中文锚点的链接时:

  1. 页面有报错,而报错来自于 reco主题使用的 vuepress-plugin-smooth-scroll主题。
  2. 不能正常跳转到锚点位置

页面报错这个问题最简单粗暴的方式就是修改源码了,我们打开 node_modules/vuepress-plugin-smooth-scroll/lib/enhanceApp.js,直接修改:

  1. const enhanceApp = ({ Vue, router }) => {
  2. router.options.scrollBehavior = (to, from, savedPosition) => {
  3. // ...
  4. else if (to.hash) {
  5. //...
  6. // 加上 decodeURIComponent
  7. const targetElement = document.querySelector(decodeURIComponent(to.hash));
  8. //...
  9. }
  10. // ...
  11. };
  12. };

因为我们并不会提交源码,而是提交编译后的文件到服务器上,所以改了依赖的源码也没有什么影响。
接下来是页面加载完跳转到锚点位置,之前我们在 《VuePress 博客优化之添加数据统计功能》,在 enhanceApp.js 中监听路由的改变,同样我们可以监听路由的 ready 事件,然后跳转到直接的位置,我们修改下 .vuepress/enhanceApp.js中的代码:

  1. export default ({ router }) => {
  2. // ...
  3. router.onReady(() => {
  4. const { hash } = document.location;
  5. setTimeout(() => {
  6. if (hash.length > 1) {
  7. const id = decodeURIComponent(hash);
  8. const el = document.querySelector(`.reco-side-${decodeURIComponent(id).substring(1)}`);
  9. el.click();
  10. }
  11. }, 1000);
  12. });
  13. };

在这里并没有直接获取标题 DOM 元素的位置,然后使用 window.scrollTo 跳转,这是因为在 reco 主题下,随着用户不断的浏览,其实路由也在不停的切换,右侧的目录才会随之改变,这里直接模拟了右侧目录的点击操作,也是为了将具体跳转的行为交给 reco 来实现。