前两天项目遇到一个须要给页面添加大纲导航的功能,要求把页面中的特定标签加入到大纲导航中。相似这样: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太旧了,之后就转到思否吧。