前兩天項目遇到一個須要給頁面添加大綱導航的功能,要求把頁面中的特定標籤加入到大綱導航中。相似這樣:vue
需求自己並不難,不過想把這個東西作得通用一些,也就是之後再有別的頁面須要加導航,不用再從新寫很複雜的邏輯了。下面說一下具體實現思路,而且文末會給出簡便易用的導航生成工具。node
作以前想到以前接觸過的markdown編輯器mavon-editor有一個導航,不過那個導航只能用於編輯器自身,我去看了一下它的表現:npm
點擊右邊的導航節點,會自動定位到對應標題元素。當時思考了一下它是怎麼記錄標題元素的,會不會是給標題元素加了一個什麼id之類的屬性?因而我看了一下生成的DOM:數組
居然是給標題元素加了一個帶有id屬性的a標籤的子節點。不過它生成id的方式比較簡單,單純的"字符串_編號"而已,想來並非那麼可靠(難於保證編輯器外有相同id的元素)。瀏覽器
我大致有了一個基本的思路:markdown
由於項目是採用Vue來實現,數據控制視圖,因此一般不須要直接操做DOM。可是這裏須要在DOM中插入錨點,Vue自定義指令是一個不錯的選擇。因而能夠寫一個指令,經過需求分析,大致肯定能夠這個指令值能夠綁定的一個包含如下三個信息的對象:iview
須要在每個導航元素臨近位置插入一個錨點,我這裏插在導航元素前面,因此這個函數接收一個導航元素dom參數,並生成一個元素插入到dom以前。代碼以下: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 }
這個函數接收導航元素dom做爲參數,生成一個a標籤,而且給a標籤設置了一個uuid(確保惟一性)做爲id,同時設置了一個特殊屬性'navigation_anchor'(儘量複雜,你甚至能夠用uuid,不要與DOM中其餘元素屬性相同)便於清理全部生成的錨點。編輯器
用於清除生成的錨點元素。代碼以下:函數
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) } }
能夠看到,經過給錨點元素設置一個特殊屬性,在清除的時候很是容易。這裏用到一個很是重要的函數querySelectorAll,它會根據調用的根節點遍歷該節點的子DOM樹,返回符合某個選擇器的NodeList(一個類數組的對象,但不是數組!),並且遍歷方式就是上文所述的深度優先先序遍歷!真是激動人心!接下來咱們能夠用這個元素獲取全部須要導航的元素列表。
經過傳入的導航元素DOM根節點、導航元素選擇器列表、導航元素排除選擇器,返回一個樹形數據的列表list。查找出全部導航元素,插入對應錨點,並將錨點信息和導航元素標題存到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 }
到這一步有個頗有必要注意的地方,導航數據裏的title我最開始用了一個超級慢的屬性innerText,而後整個頁面生成導航(大約50個導航節點)居然要2s左右,後面改成了才textContent。通過個人測試,兩個屬性的訪問時間相差n個數量級,訪問innerText大約要30ms,而訪問textContent大約要0.05ms左右。就是這麼大的差異,查閱了相關資料,緣由應該是innerText會引發瀏覽器重排,耗時超級多。
如今生成導航數據的函數已經有了,一個問題就是什麼時候調用此函數呢?咱們經過Vue指令來實現,能夠在相應的鉤子函數中調用。一個時機是當指令綁定的元素所在模板更新完成之時,另外一個時機是指令綁定元素插入之時。
指令部分代碼以下:
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 } } }
須要注意的是,咱們在模板更新完成時插入錨點元素,而這自己又是會觸發模板更新的,因此須要打個標記避死循環。
導航數據是一個樹形數據,因此能夠用樹形組件來展現之。好比element或者iview的樹組件均可以。不過由於曾經對element和iview的樹形組件不甚滿意,本身寫過一個樹形組simple-vue-tree件而且發佈到了npm。
這裏我就使用這個組件來展現,下面是一個完整的示例:
<template> <div class="hello"> <div v-outline="{ callback: refreshNavTree, selectors: ['h1', 'h2'], exceptSelector: '[un-nav]' }" class="content"> <!-- 須要導航的內容 --> <div> <h1>一級標題1</h1> <div :style="{ margin: '.5rem 2rem' }">內容不出如今導航</div> <h2>二級標題</h2> <div :style="{ margin: '.5rem 2rem' }">內容不出如今導航</div> </div> </div> <div class="navigation"> <div class="title">導航目錄</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了,地址爲vue-outline。若是你須要用到而且不想造輪子的話,能夠經過npm或者yarn等包管理工具安裝,而且能夠在npm上查看使用方法。
就這樣吧,感謝閱讀。第一次在思否寫文章,以前一直都在CSDN寫博客,不過CSDN太舊了,之後就轉到思否吧。