實現一個掘金Style的markdown目錄

已經幾乎好久沒有更新文章了,因爲工做和生活的緣由,彷彿失去了以前在大學時候的樣子,變得慵懶起來。剛踏入社會仍是須要不停的鞭策本身,有不少東西要學,按期的寫做對本身的提高是很大的。在寫的過程你依舊在思考,你會想着把這東西變得更好展示到別人眼前。不會像寫業務同樣,完成了功能和需求不多從頭去優化總結。恰好也是由於最近碰到的一個需求,看網上這方面資料不多,因而就有了這篇文章。css

前言

隨着前端的興起,愈來愈多的人加入了這個你們庭,框架和輪子玲琅滿目、遍地開花。這樣帶來了不少好處:減小了開發成本和時間,可能一個功能別人已經造好了,你只須要npm install一下就可使用了,十分的快捷。可是帶來的壞處也不可小噓:隨着項目的增大,輪子愈來愈多,多的難以掌控,項目的體積也由原來的10m > 20m > 100m...,這是一件很恐怖的事情。另外一方面你頻繁的使用別人寫好的輪子,不多本身思考和實現,久而久之,你的代碼能力天然就降低了。因此我常常約束本身的一句話:能不用盡可能不用,開發中一個輪子的使用率超低,甚至只有一次,那堅定不用。前端

恰好一個需求就是:markdow文章編寫和呈現。功能差很少相似掘金這種吧,今天給你們帶來的是第一節,如何生成一個markdown 目錄。
npm

1、錨點設置

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屬性的屬性值最前面要加#)markdown

②:在頁面中須要的位置設置錨點`<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)
})

2、目錄轉化

咱們看到的文章目錄通常都是以ul > li > a 標籤形式存在的,因此拿到了文章全部的h標籤後如何轉化爲ul或li這類的標籤呢?
async


從控制檯中中能夠看出文章h標題都被抽離出來,接下來要作的就是將這些h標籤轉化爲ul>li的形式。首先因該知道的一種數據結構<font color=orange size=3>--堆棧</font>。
簡而言之就是先進後出的數據格式,好比說有一個籃子咱們依次往籃子裏放雞蛋,忽然有一天這個籃子底部快漏了,爲了保護雞蛋咱們要把雞蛋從籃子裏拿出來
,從籃子最外層依次向內取出雞蛋,這就是典型的先進後出的例子。咱們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相互對應,文章就實現了目錄和點擊跳轉。

3、目錄優化

到這裏咱們的目標基本完成了一半,做爲掘金的忠愛粉,固然是選擇使用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;
    }
  }
}

通過改裝後的目錄效果以下,貌似有幾分相像了。至少不會太醜。後面主要介紹如何實現文章到目錄的聯動和目錄到文章的聯動。這是必不可少的一個功能

4、動態關聯

一、目錄到文章

目錄如何控制文章顯示的位置,能夠思考一下🤔:錨點是經過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的高度。

二、文章到目錄

目錄到文章已經講完了,滾動文章如何實現目錄自動跳轉呢?不妨也先大體理清楚思路:

  • 一、監聽瀏覽器的滾動距離
  • 二、計算每一個標題距瀏覽器頂部的高度
  • 三、匹配滾動距離在兩標題之間的距離實現目錄自動跳轉

具體實現步驟:

在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的文章編輯器,敬請期待!

相關文章
相關標籤/搜索