「什么是锚点」
- 锚点是一种超级链接,能快速将访问者带到指定位置。
- 在 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编码的一个、两个、三个或四个转义序列
- 其中a标签的href会
- 我们看渲染后的html:
-
我们再举一个例子:
*markdown# Button按钮 [跳转到Button按钮](#Button按钮)
- 我们会发现无法跳转了,让我们再来看看渲染后的html:
- 观察上图,我们可以发现,标题在将内容转化为id的时候,把大写字母变小写了。
所以,标题内容转id并不是完全把内容复制过去的
- 我们会发现无法跳转了,让我们再来看看渲染后的html:
「markdown标题内容 转ID 的规则」
- 大写字母转小写
- 比如:
# BUTTON
- 会转化成:
<h1 id="button">BUTTON</h1>
- 比如:
- 前后空格去除
- 比如:
# BUTTON
- 会转化成:
<h1 id="button">BUTTON</h1>
- 比如:
- 内容中间的空格会转化成 -
- 比如:
# BUTTON 按钮
- 会转化成:
<h1 id="button--按钮">BUTTON 按钮</h1>
- 比如:
- 特殊字符去除
-
以下特殊字符会被去除:
ini!$^()+={}|[]:"';<>?,./@#%......&*------+=""''~`
-
比如:
# BUTTON 按钮;<>?,
-
会被转化成:
<h1 id="button--按钮">BUTTON 按钮;<>?,</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.querySelector
或document.querySelectorAll
方法,如果选择器是以数字开头的id
,那么你需要在选择器字符串前加上转义字符\
,因为CSS选择器规范要求标识符(包括元素的类名、id
和属性名)不能以未转义的数字开头 - 例如:
<div id="123">Some content</div>
- 使用
querySelector
选取上面的div
时,你需要这样写:let element = document.querySelector("#\\31 23");
- 在这个选择器中,
\3
表示转义序列的开始,1
是要转义的字符,而后面紧跟的23
是id
的其余部分。 - 注意,转义序列后面必须跟一个空格或其它分隔符 (在这里是数字),因此
\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'});
}
});
}
})
}