<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          文章頁(yè)面目錄自動(dòng)生成方案

          共 5268字,需瀏覽 11分鐘

           ·

          2020-12-06 10:01


          一、前言

          前兩天項(xiàng)目遇到一個(gè)需要給頁(yè)面添加大綱導(dǎo)航的功能,要求把頁(yè)面中的特定標(biāo)簽加入到大綱導(dǎo)航中。類似這樣:


          需求本身并不難,不過(guò)想把這個(gè)東西做得通用一些,也就是以后再有別的頁(yè)面需要加導(dǎo)航,不用再重新寫很復(fù)雜的邏輯了。下面說(shuō)一下具體實(shí)現(xiàn)思路,并且文末會(huì)給出簡(jiǎn)便易用的導(dǎo)航生成工具。

          二、實(shí)現(xiàn)思路

          1、需求分析

          做之前想到之前接觸過(guò)的markdown編輯器mavon-editor有一個(gè)導(dǎo)航,不過(guò)那個(gè)導(dǎo)航只能用于編輯器自身,我去看了一下它的表現(xiàn):




          點(diǎn)擊右邊的導(dǎo)航節(jié)點(diǎn),會(huì)自動(dòng)定位到對(duì)應(yīng)標(biāo)題元素。當(dāng)時(shí)思考了一下它是怎么記錄標(biāo)題元素的,會(huì)不會(huì)是給標(biāo)題元素加了一個(gè)什么id之類的屬性?于是我看了一下生成的DOM:


          竟然是給標(biāo)題元素加了一個(gè)帶有id屬性的a標(biāo)簽的子節(jié)點(diǎn)。不過(guò)它生成id的方式比較簡(jiǎn)單,單純的"字符串_編號(hào)"而已,想來(lái)并不是那么可靠(難于保證編輯器外有相同id的元素)。
          我大體有了一個(gè)基本的思路:
          • 既然是對(duì)于任意頁(yè)面都可用,那可以遍歷DOM樹,尋找需要導(dǎo)航的標(biāo)簽,然后把相關(guān)節(jié)點(diǎn)位置信息存儲(chǔ)起來(lái)。這里也可一類似mavon-editor給dom樹中插入一個(gè)元素作為一個(gè)錨點(diǎn)。遍歷DOM樹的方法應(yīng)該與DOM渲染后從上到下的順序一致,即采用深度優(yōu)先的先序遍歷方法(先序遍歷即先檢查根元素,再檢查子元素;后序遍歷則相反;如果是二叉樹,還有中序遍歷)。

          • 在所有頁(yè)面中,并不能單純根據(jù)h1,h2等標(biāo)簽名來(lái)判別一個(gè)元素是否要導(dǎo)航,所以想到了用選擇器來(lái)確定,同時(shí)添加根據(jù)選擇器來(lái)排除一些例外的元素。

          • 最終的導(dǎo)航應(yīng)該是一個(gè)樹形結(jié)構(gòu),并且每一個(gè)節(jié)點(diǎn)對(duì)應(yīng)一個(gè)插入的錨點(diǎn),即每一個(gè)樹節(jié)點(diǎn)應(yīng)該包含一個(gè)錨點(diǎn)信息。

          2、實(shí)現(xiàn)思路

          因?yàn)轫?xiàng)目是采用Vue來(lái)實(shí)現(xiàn),數(shù)據(jù)控制視圖,所以通常不需要直接操作DOM。但是這里需要在DOM中插入錨點(diǎn),Vue自定義指令是一個(gè)不錯(cuò)的選擇。于是可以寫一個(gè)指令,通過(guò)需求分析,大體確定可以這個(gè)指令值可以綁定的一個(gè)包含以下三個(gè)信息的對(duì)象:
          • 一個(gè)列表selectors:列表中的每一項(xiàng)是一層導(dǎo)航對(duì)應(yīng)的選擇器,比如下標(biāo)為0的元素是第一級(jí)導(dǎo)航,通常可以用選擇器'h1',下標(biāo)為1的元素是第二級(jí)導(dǎo)航,通常可以用選擇器'h2';

          • 一個(gè)字符串exceptSelector,用于排除例外元素的選擇器;

          • 一個(gè)回調(diào)函數(shù)callback,用于接收生成的導(dǎo)航樹形數(shù)據(jù)。

          三、具體實(shí)現(xiàn)

          1、錨點(diǎn)生成函數(shù)

          需要在每一個(gè)導(dǎo)航元素臨近位置插入一個(gè)錨點(diǎn),我這里插在導(dǎo)航元素前面,所以這個(gè)函數(shù)接收一個(gè)導(dǎo)航元素dom參數(shù),并生成一個(gè)元素插入到dom之前。代碼如下:
          import uuidv4 from 'uuid/v4'let ATTR_NAME = 'navigation_anchor'function createLinkElement (dom) { let id = uuidv4() let element = document.createElement('a') element.setAttribute('id', id) element.setAttribute(ATTR_NAME, true) dom.parentNode.insertBefore(element, dom) return id}
          這個(gè)函數(shù)接收導(dǎo)航元素dom作為參數(shù),生成一個(gè)a標(biāo)簽,并且給a標(biāo)簽設(shè)置了一個(gè)uuid(確保唯一性)作為id,同時(shí)設(shè)置了一個(gè)特殊屬性'navigation_anchor'(盡可能復(fù)雜,你甚至可以用uuid,不要與DOM中其他元素屬性相同)便于清理所有生成的錨點(diǎn)。

          2、錨點(diǎn)清理函數(shù)

          用于清除生成的錨點(diǎn)元素。代碼如下:
          function clearLinkElement (dom) { dom = dom || document let domList = dom.querySelectorAll(`a[${ATTR_NAME}]`) for (let idx = domList.length - 1; idx > -1; idx--) { let element = domList[idx] element.parentNode.removeChild(element) }}
          可以看到,通過(guò)給錨點(diǎn)元素設(shè)置一個(gè)特殊屬性,在清除的時(shí)候非常容易。這里用到一個(gè)非常重要的函數(shù)querySelectorAll,它會(huì)根據(jù)調(diào)用的根節(jié)點(diǎn)遍歷該節(jié)點(diǎn)的子DOM樹,返回符合某個(gè)選擇器的NodeList(一個(gè)類數(shù)組的對(duì)象,但不是數(shù)組!),而且遍歷方式就是上文所述的深度優(yōu)先先序遍歷!真是激動(dòng)人心!接下來(lái)我們可以用這個(gè)元素獲取所有需要導(dǎo)航的元素列表。

          3、生成樹形導(dǎo)航數(shù)據(jù)函數(shù)

          通過(guò)傳入的導(dǎo)航元素DOM根節(jié)點(diǎn)、導(dǎo)航元素選擇器列表、導(dǎo)航元素排除選擇器,返回一個(gè)樹形數(shù)據(jù)的列表list。查找出所有導(dǎo)航元素,插入對(duì)應(yīng)錨點(diǎn),并將錨點(diǎn)信息和導(dǎo)航元素標(biāo)題存到list中。
          function generateNavTree (dom, selectors, exceptSelector) { clearLinkElement(dom) let list = [] if (exceptSelector) { let exceptList = dom.querySelectorAll(exceptSelector) exceptList.forEach(element => { element.__nav_except = true }) } for (let idx in selectors) { let elementList = dom.querySelectorAll(selectors[idx]) elementList.forEach(element => { if (element.__nav_except || element.offsetParent === null) return element.__nav_level = idx }) } let selector = selectors.join(',') let domList = dom.querySelectorAll(selector) for (let element of domList) { if (!element.__nav_level) { delete element.__nav_except continue } let pushList = list while (element.__nav_level > 0) { pushList = pushList.length ? pushList[pushList.length - 1].children : null if (!pushList) break element.__nav_level-- } let data = { title: element.textContent, children: [], id: createLinkElement(element) } pushList && pushList.push(data) delete element.__nav_level } return list}
          到這一步有個(gè)很有必要注意的地方,導(dǎo)航數(shù)據(jù)里的title我最開始用了一個(gè)超級(jí)慢的屬性innerText,然后整個(gè)頁(yè)面生成導(dǎo)航(大約50個(gè)導(dǎo)航節(jié)點(diǎn))竟然要2s左右,后面改為了才textContent。
          經(jīng)過(guò)我的測(cè)試,兩個(gè)屬性的訪問(wèn)時(shí)間相差n個(gè)數(shù)量級(jí),訪問(wèn)innerText大約要30ms,而訪問(wèn)textContent大約要0.05ms左右。就是這么大的差別,查閱了相關(guān)資料,原因應(yīng)該是innerText會(huì)引起瀏覽器重排,耗時(shí)超級(jí)多。

          4、調(diào)用導(dǎo)航數(shù)據(jù)生成函數(shù)并通過(guò)回調(diào)傳給組件。

          現(xiàn)在生成導(dǎo)航數(shù)據(jù)的函數(shù)已經(jīng)有了,一個(gè)問(wèn)題就是何時(shí)調(diào)用此函數(shù)呢?我們通過(guò)Vue指令來(lái)實(shí)現(xiàn),可以在相應(yīng)的鉤子函數(shù)中調(diào)用。
          一個(gè)時(shí)機(jī)是當(dāng)指令綁定的元素所在模板更新完成之時(shí),另一個(gè)時(shí)機(jī)是指令綁定元素插入之時(shí)。
          指令部分代碼如下:
          export default { bind (el, binding, vNode) { el.__navigationGenerateFunction = () => { if (el.__generating) return let selectors = binding.value.selectors || ['h1', 'h2'] let exceptSelector = binding.value.exceptSelector el.__generating = true let list = [] generateNavTree(el, selectors, exceptSelector, list) binding.value.callback(list) vNode.context.$nextTick(() => { delete el.__generating }) } }, inserted (el, binding, vNode) { el.__navigationGenerateFunction && el.__navigationGenerateFunction() }, componentUpdated (el, binding, vNode) { el.__navigationGenerateFunction && el.__navigationGenerateFunction() }, unbind (el, binding, vNode) { clearLinkElement() if (el.__navigationGenerateFunction) { delete el.__navigationGenerateFunction } }}
          需要注意的是,我們?cè)谀0甯峦瓿蓵r(shí)插入錨點(diǎn)元素,而這本身又是會(huì)觸發(fā)模板更新的,所以需要打個(gè)標(biāo)記避死循環(huán)。

          5、導(dǎo)航數(shù)據(jù)的展示

          導(dǎo)航數(shù)據(jù)是一個(gè)樹形數(shù)據(jù),所以可以用樹形組件來(lái)展示之。比如element或者iview的樹組件都可以。不過(guò)因?yàn)樵?jīng)對(duì)element和iview的樹形組件不甚滿意,自己寫過(guò)一個(gè)樹形組simple-vue-tree件并且發(fā)布到了npm。
          這里我就使用這個(gè)組件來(lái)展示,下面是一個(gè)完整的示例:
          <template> <div class="hello"> <div v-outline="{ callback: refreshNavTree, selectors: ['h1', 'h2'], exceptSelector: '[un-nav]' }" class="content"> <div> <h1>一級(jí)標(biāo)題1h1> <div :style="{ margin: '.5rem 2rem' }">內(nèi)容不出現(xiàn)在導(dǎo)航div> <h2>二級(jí)標(biāo)題h2> <div :style="{ margin: '.5rem 2rem' }">內(nèi)容不出現(xiàn)在導(dǎo)航div> div> div> <div class="navigation"> <div class="title">導(dǎo)航目錄div> <simple-tree :treeData="navTree" :expand="false" class="tree"> <div slot-scope="{ data, parentData }"> <div class="node-render-content" @click.stop="jumpToAnchor(data.id)"> {{ data.title }} div> div> simple-tree> div> div>template><script>export default { data () { return { navTree: [] } }, methods: { refreshNavTree (treeData) { this.navTree = treeData }, jumpToAnchor (id) { let element = document.getElementById(id) if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }) } } }}script>

          四、npm插件

          這個(gè)導(dǎo)航工具我已經(jīng)發(fā)布到npm了,地址為vue-outline。如果你需要用到并且不想造輪子的話,可以通過(guò)npm或者yarn等包管理工具安裝,并且可以在npm上查看使用方法
          本文完~

          最后


          • 歡迎加我微信(winty230),拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧
          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美日高清视频免费在线播放 | 日逼日| 午夜精品久久99热蜜桃剧情介绍 | 一级一片免费观看 | 久99热在线 |