在hash模式下markdown锚点跳转

「什么是锚点」

  • 锚点是一种超级链接,能快速将访问者带到指定位置。
  • 在 HTML 中, 一个带有 id 的 DOM 节点 ,就是一个锚点,通过在 url 的 hash 部分携带 id 就可以链接到该锚点。

「markdown的页内锚点跳转方式」

  • 首先要定义一个锚

    • <span id="jump1>锚点1</span>可以定义一个锚
    • 在markdown中标题也会转化为锚(带id的标签),具体转化规则下面讲
      • 标题即# 标题一(这是h1)## 标题二(这是h2)这样的语法
  • 然后要定义一个引用方式

    • 方式1:用a标签

      • <a href="#jump1">跳转到锚点1</a>
    • 方式2:用markdown的语法

      • [跳转到锚点1](#jump1)
      • 上面这个语法最后到html中其实会被渲染为方式1中的a标签 <a href="#jump1">跳转到锚点1</a>

总结

  • 锚可以是 带id的标签 或者是 markdown的标题语法
  • 引用锚点可以用 href属性为目标锚点id的a标签 或者是 markdown的引用语法

「问题背景」

  • 我们在使用现代框架开发 SPA 的时候都会使用能和框架集成的路由管理器, 例如: vue-router, react-router等等。
  • 而有时候为了兼容一些情况, 例如: 旧浏览器(IE 8/9)markdown跳转 等. 我们会使用 Hash 的路由模式来实现前端路由.
  • 这种时候, 因为 hash 被路由占据了, 锚点功能就会和路由冲突.
  • 比如在一个组件库文档中用了hashRouter,这个时候要使用markdown中的锚点跳转改变了hash,就会跳转到404页面

「解决思路」

  • 锚点跳转的实现原理 是 在点击a标签时会检查a标签的href ,然后查找页面上是否有对应id的元素 ,如果有,就将这个元素滚动到可视区域
  • 那由于我们的#已经被占用,我们无法利用a标签的默认跳转,所以需要我们来进行手动跳转
    • 在解析了 markdown 之后,对所有能链接到锚点的元素附加点击事件,阻止默认事件,让页面滚动到锚点所在位置。
    • 滚动使用scrollIntoViewAPI

「实现」

js 复制代码
  function anchorChange() {
  // 获取的装载文章的 DOM 节点content.
  const postBody = document.getElementById('content');

  // 拿到当前content内所有的a标签, 判断是否是本页面的跳转.
  postBody?.querySelectorAll("a").forEach((a: HTMLAnchorElement) => {
      //  跳转的页面 host 需和本页面一致, 并且带有 hash.
      if (a.hostname === window.location.hostname && !!a.hash) {
          // 为每一个符合本页跳转的a标签添加点击事件,组织默认跳转,用scrollIntoView实现跳转.
          a.addEventListener('click', e => {
              // 阻止默认跳转行为
              e.preventDefault();
              // 获取a标签的href
              const anchor = decodeURIComponent(a.hash.replace('#',''))
              // 拿到id为 a标签的href 的标签,然后用scrollIntoView跳转到那个标签,behavior:'smooth'是平滑滚动           
              postBody?.querySelector(`#${anchor}`)?.scrollIntoView({behavior: 'smooth'});
          });
      }
  })
}
  • 上述代码实现了一个方法对a标签的跳转行为改为手动跳转,这个方法在每次文档内容更新的时候调用(新文档会有新的a标签,所以要重新调用)

    • 调用后就可以为新文档内的所有a标签进行处理
  • 调用示例
    *

    markdown 复制代码
    <span id="jump1">锚点1</span>
    
    [跳转到锚点1](#jump1)
    或者
    <a href="#jump1">跳转到锚点1</a>
    • 在经过anchorChange函数处理过后的a标签就可以正确跳转到对应的锚点而不会进入404页面啦

「标题锚点的使用与问题」

  • 上面我们说过,锚点可以用带id的标签 或者是 markdown的标题语法声明

  • 那下面我们来举个例子用markdown的标题语法声明锚点并跳转
    *

    markdown 复制代码
    # button按钮
    
    标题语法的引用的话,就将标题内容放到(#xxx)中
    或者放到a标签的href中
    [跳转到button按钮](#button按钮)
    <a href="#button按钮">跳转到button按钮</a>
    • 我们看渲染后的html:
    • 我们可以看到,标题是被渲染为以内容为id的标签
    • 而下面的引用语法则是被渲染为href值为括号内 内容的a标签
      • 其中a标签的href会 将某些字符的每个实例替换为表示字符的UTF-8编码的一个、两个、三个或四个转义序列
  • 我们再举一个例子:
    *

    markdown 复制代码
     # Button按钮
    
     [跳转到Button按钮](#Button按钮)
    • 我们会发现无法跳转了,让我们再来看看渲染后的html:
    • 观察上图,我们可以发现,标题在将内容转化为id的时候,把大写字母变小写了。所以,标题内容转id并不是完全把内容复制过去的

「markdown标题内容 转ID 的规则」

  1. 大写字母转小写
    • 比如:# BUTTON
    • 会转化成:<h1 id="button">BUTTON</h1>
  2. 前后空格去除
    • 比如:# BUTTON
    • 会转化成:<h1 id="button">BUTTON</h1>
  3. 内容中间的空格会转化成 -
    • 比如:# BUTTON 按钮
    • 会转化成:<h1 id="button--按钮">BUTTON 按钮</h1>
  4. 特殊字符去除
    • 以下特殊字符会被去除:

      ini 复制代码
      !$^()+={}|[]:"';<>?,./@#%......&*------+=""''~`
    • 比如:# BUTTON 按钮;<>?,

    • 会被转化成:<h1 id="button--按钮">BUTTON 按钮;&lt;&gt;?,</h1>

  • 现在我们知道了markdown标题内容转id的规则了,接下来我们在anchorChange函数中对拿到的href进行处理,让它符合转化后的id

「重新实现」

js 复制代码
function anchorChange() {
  // 获取的装载文章的 DOM 节点content.
  const postBody = document.getElementById('content');

  // 拿到当前content内所有的a标签, 判断是否是本页面的跳转.
  postBody?.querySelectorAll("a").forEach((a: HTMLAnchorElement) => {
      //  跳转的页面 host 需和本页面一致, 并且带有 hash.
      if (a.hostname === window.location.hostname && !!a.hash) {
          // 为每一个符合本页跳转的a标签添加点击事件,组织默认跳转,用scrollIntoView实现跳转.
          a.addEventListener('click', e => {
              // 阻止默认跳转行为
              e.preventDefault();
              // markdown标题内容转id规则:前后空格去除,空格转-,特殊字符去除,大写字母转小写;
              const anchor = decodeURIComponent(a.hash.replace('#',''))
              .trim()
              .replace(/\s/g, '-')
              .replace(/[\!\$\^\(\)\+\=\{\}\|\[\]\\\:\"\'\;\<\>\?\,\.\/\@\#\%......\&\*------\+\=""''\~\`]/g, '')
              .toLowerCase();
              
              postBody?.querySelector(`#${anchor}`)?.scrollIntoView({behavior: 'smooth'});
          });
      }
  })
}
  • 上方代码中
    • 通过decodeURIComponent对字符的转义序列进行转回
      • 比如将%E6%8C%89%E9%92%AE转回按钮
    • 通过trim()对前后空格进行去除
    • 通过replace(/\s/g, '-')将内容间的空格转成-
    • 通过replace(/[\!\$\^\(\)\+\=\{\}\|\[\]\\\:\"\'\;\<\>\?\,\.\/\@\#\%......\&\*------\+\=""''\~\]/g, '')`将特殊字符去除
    • 最后通过toLowerCase()将所有大写字母转为小写
  • 至此,a标签的href值处理已经完成。

id属性的限制 以及 querySelector的局限

  • 写完上面的代码后,我本以为大功告成了,但是在我随便测试打出一个字符串345@4h时,我发现又无法跳转了😭
  • 经过查阅资料:

在HTML5之前,根据HTML4和XHTML的规范,id属性的值确实不能以数字开头。它们必须以字母开头,后面可以跟字母、数字、下划线、连字符和其他一些字符。这一限制是由于文档对象模型(DOM)使用HTML的id属性值来创建JavaScript可访问的属性,而在JavaScript中变量名不能以数字开头。

然而,HTML5放宽了这一限制,允许id属性的值以数字开头 。但是,即使如此,在使用CSS选择器和一些JavaScript方法时,以数字开头的id仍然可能导致问题

使用querySelector时对 以数字开头的id 的处理

  • 当你使用document.querySelectordocument.querySelectorAll方法,如果选择器是以数字开头的id,那么你需要在选择器字符串前加上转义字符\,因为CSS选择器规范要求标识符(包括元素的类名、id和属性名)不能以未转义的数字开头
  • 例如:
    • <div id="123">Some content</div>
    • 使用querySelector选取上面的div时,你需要这样写:let element = document.querySelector("#\\31 23");
      • 在这个选择器中,\3 表示转义序列的开始,1 是要转义的字符,而后面紧跟的 23id的其余部分。
      • 注意,转义序列后面必须跟一个空格或其它分隔符 (在这里是数字),因此\31 转义了数字1,紧跟的 23 被当作普通字符处理。

  • 因此,对anchorChange函数再做一些处理
js 复制代码
function anchorChange() {
    // 获取的装载文章的 DOM 节点content.
    const postBody = document.getElementById('content');
  
    // 拿到当前content内所有的a标签, 判断是否是本页面的跳转.
    postBody?.querySelectorAll("a").forEach((a: HTMLAnchorElement) => {
        //  跳转的页面 host 需和本页面一致, 并且带有 hash.
        if (a.hostname === window.location.hostname && !!a.hash) {
            // 为每一个符合本页跳转的a标签添加点击事件,组织默认跳转,用scrollIntoView实现跳转.
            a.addEventListener('click', e => {
                // 阻止默认跳转行为
                e.preventDefault();
                // markdown标题内容转id规则:前后空格去除,空格转-,特殊字符去除,大写字母转小写;
                const anchor = decodeURIComponentSafe(a.hash.replace('#',''))
                .trim()
                .replace(/\s/g, '-')
                .replace(/[\!\$\^\(\)\+\=\{\}\|\[\]\\\:\"\'\;\<\>\?\,\.\/\@\#\%......\&\*------\+\=""''\~\`]/g, '')
                .toLowerCase();

                if(!isNaN(+anchor[0])) {
                    // 如果anchor开头是数字,则需要加转义符:\\3 ,否则querySelector会报错
                    let newAnchor = '\\3' + anchor[0] + ' ' + anchor.slice(1);
                    postBody?.querySelector(`#${newAnchor}`)?.scrollIntoView({behavior: 'smooth'});
                } else {
                    postBody?.querySelector(`#${anchor}`)?.scrollIntoView({behavior: 'smooth'});
                }
            });
        }
    })
}
相关推荐
前端 贾公子6 小时前
pnpm 的 resolution-mode 配置 ( pnpm 的版本解析)
前端
伍哥的传说7 小时前
React 自定义Hook——页面或元素滚动到底部监听 Hook
前端·react.js·前端框架
麦兜*9 小时前
Spring Boot 集成Reactive Web 性能优化全栈技术方案,包含底层原理、压测方法论、参数调优
java·前端·spring boot·spring·spring cloud·性能优化·maven
知了一笑9 小时前
独立开发第二周:构建、执行、规划
java·前端·后端
UI前端开发工作室9 小时前
数字孪生技术为UI前端提供新视角:产品性能的实时模拟与预测
大数据·前端
Sapphire~9 小时前
重学前端004 --- html 表单
前端·html
遇到困难睡大觉哈哈10 小时前
CSS中的Element语法
前端·css
Real_man10 小时前
新物种与新法则:AI重塑开发与产品未来
前端·后端·面试
小彭努力中10 小时前
147.在 Vue3 中使用 OpenLayers 地图上 ECharts 模拟飞机循环飞行
前端·javascript·vue.js·ecmascript·echarts
老马聊技术10 小时前
日历插件-FullCalendar的详细使用
前端·javascript