已经几乎很久没有更新文章了,由于工作和生活的原因,仿佛失去了之前在大学时候的样子,变得慵懒起来。刚踏入社会还是需要不停的鞭策自己,有很多东西要学,定期的写作对自己的提升是很大的。在写的过程你依旧在思考,你会想着把这东西变得更好展现到别人眼前。不会像写业务一样,完成了功能和需求很少从头去优化总结。刚好也是因为最近碰到的一个需求,看网上这方面资料很少,于是就有了这篇文章。
前言
随着前端的兴起,越来越多的人加入了这个大家庭,框架和轮子玲琅满目、遍地开花。这样带来了很多好处:减少了开发成本和时间,可能一个功能别人已经造好了,你只需要npm install一下就可以使用了,十分的快捷。但是带来的坏处也不可小嘘:随着项目的增大,轮子越来越多,多的难以掌控,项目的体积也由原来的10m > 20m > 100m…,这是一件很恐怖的事情。另一方面你频繁的使用别人写好的轮子,很少自己思考和实现,长此以往,你的代码能力自然就下降了。所以我经常约束自己的一句话:能不用尽量不用,开发中一个轮子的使用率超低,甚至只有一次,那坚决不用。
刚好一个需求就是:markdow文章编写和呈现。功能差不多类似掘金这种吧,今天给大家带来的是第一节,如何生成一个markdown 目录。
一、锚点设置
makdown中的#,##,##组成的标题经过marked等工具转化渲染到网页中会成变成h标签,所以当拿到文章详情页后可以从中抽离出所有的目录标签即h1,h2,h3….
const toc: string[] = data.content.match(/<[hH][1-6]>.*?<\/[hH][1-6]>/g) // 通过正则的方式
拿到这些标题之后就可以进行锚点的设置。在H5中关于锚点的做法很多,我们会采用下面这种做法进行设计:
①:设置一个锚点链接 <a href="#miao">去找喵星人</a>
(注意:href属性的属性值最前面要加#)
②:在页面中需要的位置设置锚点<h1 id="miao"></h1>
(注意:a标签中要写一个id属性,属性值要与①中的href的属性值一样,不加#)
通过正则匹配到文章中所有的h标签后,循环添加id属性并将div包裹
tocs.forEach((item: string, index: number) => {
let _toc = `<div name='toc-title' id='${index}'>${item} </div>`
data.content = data.content.replace(item, _toc)
})
二、目录转化
我们看到的文章目录一般都是以ul > li > a 标签形式存在的,所以拿到了文章所有的h标签后如何转化为ul或li这类的标签呢?
从控制台中中可以看出文章h标题都被抽离出来,接下来要做的就是将这些h标签转化为ul>li的形式。首先因该知道的一种数据结构–堆栈。 简而言之就是先进后出的数据格式,比如说有一个篮子我们依次往篮子里放鸡蛋,突然有一天这个篮子底部快漏了,为了保护鸡蛋我们要把鸡蛋从篮子里拿出来 ,从篮子最外层依次向内取出鸡蛋,这就是典型的先进后出的例子。我们h标签转化为ul>li其实也是一样的道理。
export default function toToc(data: string[]) {
let levelStack: string[] = []
let result:string = ''
const addStartUL = () => { result += '<ul class="catalog-list">'; }
const addEndUL = () => { result += '</ul>\n'; }
const addLI = (index: number, itemText: string) => { result += '<li><a name="link" class="toc-link'+'-#'+ index + '" href="#' + index + '">' + itemText + "</a></li>\n"; }
data.forEach(function (item: any, index: number) {
let itemText: string = item.replace(/<[^>]+>/g, '') // 匹配h标签的文字
let itemLabel: string = item.match(/<\w+?>/)[0] // 匹配h?标签<h?>
let levelIndex: number = levelStack.indexOf(itemLabel) // 判断数组里有无<h?>
// 没有找到相应<h?>标签,则将新增ul、li
if (levelIndex === -1) {
levelStack.unshift(itemLabel)
addStartUL()
addLI(index, itemText)
}
// 找到了相应<h?>标签,并且在栈顶的位置则直接将li放在此ul下
else if (levelIndex === 0) {
addLI(index, itemText)
}
// 找到了相应<h?>标签,但是不在栈顶位置,需要将之前的所有<h?>出栈并且打上闭合标签,最后新增li
else {
while (levelIndex--) {
levelStack.shift()
addEndUL()
}
addLI(index, itemText)
}
})
// 如果栈中还有<h?>,全部出栈打上闭合标签
while (levelStack.length) {
levelStack.shift()
addEndUL()
}
return result
}
至此所有的h标签都转换成了ui > li的形式并且增加了a链接锚点和之前文章中h标签id相互对应,文章就实现了目录和点击跳转。
三、目录优化
到这里我们的目标基本完成了一半,作为掘金的忠爱粉,当然是选择使用css进行优化一下,css贴起来总觉得像是在拉家常,这里就不详细介绍了,大致是这样的
.catalog-list {
font-weight: 600;
padding-left: 10px;
position: relative;
font-size: 15px;
&:first-child::before {
content: "";
position: absolute;
top: 10px;
left: 12px;
bottom: 0;
width: 2px;
background-color: #ebedef;
opacity: .8;
}
}
& > li > a {
position: relative;
padding-left: 16px;
line-height: 20px;
@include catalogRound(0, 6px);
}
ul, li {
padding: 0;
margin: 0;
list-style: none;
}
ul > li > a {
font-size: 14px;
color: #333333;
padding-left: 36px;
font-weight: 500;
position: relative;
@include catalogRound(20px, 5px);
}
ul > ul > li > a {
line-height: 20px;
font-size: 14px;
color: #333333;
padding-left: 50px;
font-weight: normal;
@include catalogRound;
}
a {
color: #000;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 4px 0 4px 12px;
&:hover {
background-color: #ebedef;
}
}
}
经过改装后的目录效果如下,貌似有几分相像了。至少不会太丑。后面主要介绍如何实现文章到目录的联动和目录到文章的联动。这是必不可少的一个功能
四、动态关联
1、目录到文章
目录如何控制文章显示的位置,可以思考一下🤔:锚点是通过a标签来实现的但是大量的a标签的点击事件是无法捕获的,尤其我们是通过转换出来的a标签,但观察发现锚点的hash值会在url上增加锚点位置,因此想到了一种解决方案可以通过监听url的变化来捕获点击的a标签是哪个。于是我们监听了route
@Watch('$route')
private routechange(val: any) {
const data = document.getElementsByClassName(`toc-link-${val.hash}`)[0] as Element
this.linkLists.forEach((list:Element) => {
data == list ? list.classList.add('active') : list.classList.remove('active')
})
}
至此我们点击目录便可跳转到文章响应的位置,这里有一个小提示。由于页面可能有nav导航定位,往往我们跳转的文章会被导航栏遮住,因此需要改善一下,通过css属性设置margin-top = nav的高度,padding-top = -nav的高度。
2、文章到目录
目录到文章已经讲完了,滚动文章如何实现目录自动跳转呢?不妨也先大致理清楚思路:
- 1、监听浏览器的滚动距离
- 2、计算每个标题距浏览器顶部的高度
- 3、匹配滚动距离在两标题之间的距离实现目录自动跳转
具体实现步骤:
在mounted()生命周期中监听鼠标的滚动
window.addEventListener(‘scroll’, this.handleScroll, true)
获取所有的文章标题和目录
this.$nextTick(async () => {
await this.getTitleHeight()
await this.getCataloglist()
})
// 获取每个文章标题的距顶部的高度
private async getTitleHeight() {
let titlelist = Array.prototype.slice.call((this.$refs.article as Element).getElementsByClassName('toc-title'))
titlelist.forEach((item,index) => {
this.listHeight.push(item.offsetTop)
})
// 滚动的距离无法取到最后一个,因此在数组最后加上上一个两倍达到效果
this.listHeight.push(2 * (titlelist[titlelist.length-1].offsetTop))
}
// 获取目录的所有ul、a标签
private async getCataloglist() {
let catalogList = (this.$refs.catalog as Element).getElementsByClassName('catalog-list')
this.linkLists = document.getElementsByName('link')
this.target = Array.prototype.slice.call(catalogList)
}
在handleScroll函数中监听文章滚动
private handleScroll() {
const scrollY = window.pageYOffset
this.fixed = scrollY > 230 ? true : false
for (let i = 0; i < this.listHeight.length-1; i++) {
let h1: number = this.listHeight[i]
let h2: number = this.listHeight[i + 1]
if (scrollY >= h1 && scrollY <= h2) {
const data: Element = document.getElementsByClassName(`toc-link-#${i}`)[0] as Element // 获取文章滚动到目录的目标元素
this.linkLists.forEach((list: Element) => {
let top: number = 0
top = i > 7 ? -28 * (i-7) : 0
this.target[0].style.marginTop = `${top}px`
data == list ? list.classList.add('active') : list.classList.remove('active') // 其他移除active
})
}
}
}
代码讲解:
this.fixed = scrollY > 230 ? true : false
目录不跟随页面滚动,因此需要添加一个fixed属性,让他固定在文章的右边,230是目录前的一个盒子高度。当鼠标滚动到230px的时候目录就固定了带到了效果。
let top: number = 0
top = i > 7 ? -28 * (i-7) : 0
this.target[0].style.marginTop = `${top}px`
虽然目录不跟随页面滚动,但目录过长可能就显示不出来,因此需要去动态设置目录的margin-top属性,
top = i > 7 ? -28 * (i-7) : 0 // 目录第7条的时候开始向上滚动
结言
功能算是实现了,但还是有很多可以优化的地方,也希望指出给予意见。如果你不喜欢这样的目录,或者根自己的实际需求不一样,那无非就是css的不同罢了。功能实现往往决定了效果,可以根据自己需求去改写ul > li的css了。这只是从我实际项目中抽离出来的一部分。实际要比这难太多。不过这已经够用了,下期将带大家一起学习如何制作实现一个掘金Style的文章编辑器,敬请期待!
今天的文章实现一个掘金Style的markdown目录分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20562.html