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

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

二、實(shí)現(xiàn)思路
1、需求分析


竟然是給標(biāo)題元素加了一個(gè)帶有id屬性的a標(biāo)簽的子節(jié)點(diǎn)。不過(guò)它生成id的方式比較簡(jiǎn)單,單純的"字符串_編號(hào)"而已,想來(lái)并不是那么可靠(難于保證編輯器外有相同id的元素)。
既然是對(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)思路
一個(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ù)
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}
2、錨點(diǎn)清理函數(shù)
function clearLinkElement (dom) {dom = dom || documentlet domList = dom.querySelectorAll(`a[${ATTR_NAME}]`)for (let idx = domList.length - 1; idx > -1; idx--) {let element = domList[idx]element.parentNode.removeChild(element)}}
3、生成樹形導(dǎo)航數(shù)據(jù)函數(shù)
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) returnelement.__nav_level = idx})}let selector = selectors.join(',')let domList = dom.querySelectorAll(selector)for (let element of domList) {if (!element.__nav_level) {delete element.__nav_exceptcontinue}let pushList = listwhile (element.__nav_level > 0) {pushList = pushList.length ? pushList[pushList.length - 1].children : nullif (!pushList) breakelement.__nav_level--}let data = {title: element.textContent,children: [],id: createLinkElement(element)}pushList && pushList.push(data)delete element.__nav_level}return list}
4、調(diào)用導(dǎo)航數(shù)據(jù)生成函數(shù)并通過(guò)回調(diào)傳給組件。
指令部分代碼如下:
export default {bind (el, binding, vNode) {el.__navigationGenerateFunction = () => {if (el.__generating) returnlet selectors = binding.value.selectors || ['h1', 'h2']let exceptSelector = binding.value.exceptSelectorel.__generating = truelet 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}}}
5、導(dǎo)航數(shù)據(jù)的展示
這里我就使用這個(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 }"><divclass="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插件
最后
歡迎加我微信(winty230),拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...


