摘要:這里用到一個(gè)非常重要的函數(shù),它會根據(jù)調(diào)用的根節(jié)點(diǎn)遍歷該節(jié)點(diǎn)的子樹,返回符合某個(gè)選擇器的一個(gè)類數(shù)組的對象,但不是數(shù)組,而且遍歷方式就是上文所述的深度優(yōu)先先序遍歷真是激動人心接下來我們可以用這個(gè)元素獲取所有需要導(dǎo)航的元素列表。
一、前言
前兩天項(xiàng)目遇到一個(gè)需要給頁面添加大綱導(dǎo)航的功能,要求把頁面中的特定標(biāo)簽加入到大綱導(dǎo)航中。類似這樣:
需求本身并不難,不過想把這個(gè)東西做得通用一些,也就是以后再有別的頁面需要加導(dǎo)航,不用再重新寫很復(fù)雜的邏輯了。下面說一下具體實(shí)現(xiàn)思路,并且文末會給出簡便易用的導(dǎo)航生成工具。
二、實(shí)現(xiàn)思路 1、需求分析做之前想到之前接觸過的markdown編輯器mavon-editor有一個(gè)導(dǎo)航,不過那個(gè)導(dǎo)航只能用于編輯器自身,我去看了一下它的表現(xiàn):
點(diǎn)擊右邊的導(dǎo)航節(jié)點(diǎn),會自動定位到對應(yīng)標(biāo)題元素。當(dāng)時(shí)思考了一下它是怎么記錄標(biāo)題元素的,會不會是給標(biāo)題元素加了一個(gè)什么id之類的屬性?于是我看了一下生成的DOM:
竟然是給標(biāo)題元素加了一個(gè)帶有id屬性的a標(biāo)簽的子節(jié)點(diǎn)。不過它生成id的方式比較簡單,單純的"字符串_編號"而已,想來并不是那么可靠(難于保證編輯器外有相同id的元素)。
我大體有了一個(gè)基本的思路:
既然是對于任意頁面都可用,那可以遍歷DOM樹,尋找需要導(dǎo)航的標(biāo)簽,然后把相關(guān)節(jié)點(diǎn)位置信息存儲起來。這里也可一類似mavon-editor給dom樹中插入一個(gè)元素作為一個(gè)錨點(diǎn)。遍歷DOM樹的方法應(yīng)該與DOM渲染后從上到下的順序一致,即采用深度優(yōu)先的先序遍歷方法(先序遍歷即先檢查根元素,再檢查子元素;后序遍歷則相反;如果是二叉樹,還有中序遍歷)。
在所有頁面中,并不能單純根據(jù)h1,h2等標(biāo)簽名來判別一個(gè)元素是否要導(dǎo)航,所以想到了用選擇器來確定,同時(shí)添加根據(jù)選擇器來排除一些例外的元素。
最終的導(dǎo)航應(yīng)該是一個(gè)樹形結(jié)構(gòu),并且每一個(gè)節(jié)點(diǎn)對應(yīng)一個(gè)插入的錨點(diǎn),即每一個(gè)樹節(jié)點(diǎn)應(yīng)該包含一個(gè)錨點(diǎn)信息。
2.實(shí)現(xiàn)思路因?yàn)轫?xiàng)目是采用Vue來實(shí)現(xiàn),數(shù)據(jù)控制視圖,所以通常不需要直接操作DOM。但是這里需要在DOM中插入錨點(diǎn),Vue自定義指令是一個(gè)不錯(cuò)的選擇。于是可以寫一個(gè)指令,通過需求分析,大體確定可以這個(gè)指令值可以綁定的一個(gè)包含以下三個(gè)信息的對象:
一個(gè)列表selectors:列表中的每一項(xiàng)是一層導(dǎo)航對應(yīng)的選擇器,比如下標(biāo)為0的元素是第一級導(dǎo)航,通常可以用選擇器"h1",下標(biāo)為1的元素是第二級導(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) } }
可以看到,通過給錨點(diǎn)元素設(shè)置一個(gè)特殊屬性,在清除的時(shí)候非常容易。這里用到一個(gè)非常重要的函數(shù)querySelectorAll,它會根據(jù)調(diào)用的根節(jié)點(diǎn)遍歷該節(jié)點(diǎn)的子DOM樹,返回符合某個(gè)選擇器的NodeList(一個(gè)類數(shù)組的對象,但不是數(shù)組!),而且遍歷方式就是上文所述的深度優(yōu)先先序遍歷!真是激動人心!接下來我們可以用這個(gè)元素獲取所有需要導(dǎo)航的元素列表。
3. 生成樹形導(dǎo)航數(shù)據(jù)函數(shù)通過傳入的導(dǎo)航元素DOM根節(jié)點(diǎn)、導(dǎo)航元素選擇器列表、導(dǎo)航元素排除選擇器,返回一個(gè)樹形數(shù)據(jù)的列表list。查找出所有導(dǎo)航元素,插入對應(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è)超級慢的屬性innerText,然后整個(gè)頁面生成導(dǎo)航(大約50個(gè)導(dǎo)航節(jié)點(diǎn))竟然要2s左右,后面改為了才textContent。經(jīng)過我的測試,兩個(gè)屬性的訪問時(shí)間相差n個(gè)數(shù)量級,訪問innerText大約要30ms,而訪問textContent大約要0.05ms左右。就是這么大的差別,查閱了相關(guān)資料,原因應(yīng)該是innerText會引起瀏覽器重排,耗時(shí)超級多。
4. 調(diào)用導(dǎo)航數(shù)據(jù)生成函數(shù)并通過回調(diào)傳給組件。現(xiàn)在生成導(dǎo)航數(shù)據(jù)的函數(shù)已經(jīng)有了,一個(gè)問題就是何時(shí)調(diào)用此函數(shù)呢?我們通過Vue指令來實(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 } } }
需要注意的是,我們在模板更新完成時(shí)插入錨點(diǎn)元素,而這本身又是會觸發(fā)模板更新的,所以需要打個(gè)標(biāo)記避死循環(huán)。
5. 導(dǎo)航數(shù)據(jù)的展示導(dǎo)航數(shù)據(jù)是一個(gè)樹形數(shù)據(jù),所以可以用樹形組件來展示之。比如element或者iview的樹組件都可以。不過因?yàn)樵?jīng)對element和iview的樹形組件不甚滿意,自己寫過一個(gè)樹形組simple-vue-tree件并且發(fā)布到了npm。
這里我就使用這個(gè)組件來展示,下面是一個(gè)完整的示例:
四、npm插件一級標(biāo)題1
內(nèi)容不出現(xiàn)在導(dǎo)航二級標(biāo)題
內(nèi)容不出現(xiàn)在導(dǎo)航
這個(gè)導(dǎo)航工具我已經(jīng)發(fā)布到npm了,地址為vue-outline。如果你需要用到并且不想造輪子的話,可以通過npm或者yarn等包管理工具安裝,并且可以在npm上查看使用方法。
就這樣吧,感謝閱讀。第一次在思否寫文章,之前一直都在CSDN寫博客,不過CSDN太舊了,以后就轉(zhuǎn)到思否吧。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/108766.html
摘要:如何挑選合適的導(dǎo)航結(jié)構(gòu)導(dǎo)航設(shè)計(jì)是應(yīng)用設(shè)計(jì)的關(guān)鍵,設(shè)計(jì)規(guī)范以下簡稱規(guī)范中將導(dǎo)航元素分為對等層次和歷史導(dǎo)航等幾類,例如表和透視表導(dǎo)航窗格是對等導(dǎo)航元素,中心大綱細(xì)節(jié)屬于分層導(dǎo)航元素,返回則屬于歷史導(dǎo)航元素。 此文已由作者楊凱明授權(quán)網(wǎng)易云社區(qū)發(fā)布。 歡迎訪問網(wǎng)易云社區(qū),了解更多網(wǎng)易技術(shù)產(chǎn)品運(yùn)營經(jīng)驗(yàn)。 繼Windows 10系統(tǒng)發(fā)布之后,很多Windows用戶更新了系統(tǒng)。win10系統(tǒng)的發(fā)布,...
摘要:元素表示主導(dǎo)鏈接的區(qū)域。已經(jīng)習(xí)慣使用或元素對鏈接進(jìn)行結(jié)構(gòu)化的情況下,并沒有取代這種最佳實(shí)踐,只不過在它們外圍包上了一個(gè)。不允許將嵌套在內(nèi)。 第一章 網(wǎng)頁的構(gòu)造塊 一個(gè)網(wǎng)頁主要包括文本內(nèi)容、對其它文件的引用和標(biāo)記。 語義化HTML:有含義的標(biāo)記 HTML包含關(guān)于文檔中內(nèi)容的信息,這些信息稱作標(biāo)記,用以描述內(nèi)容的含義,即語義。也就是說,HTML僅僅關(guān)心網(wǎng)頁中要展示的內(nèi)容,至于如何展示,那是...
摘要:體驗(yàn)并不好在中,有這個(gè)例子,參考使用即可做出類似微信通訊錄的頁面。啟動頁計(jì)劃是不顯示導(dǎo)航欄的,為了跳過啟動頁,添加了一個(gè)跳過按鈕。 本人微信公眾號:前端修煉之路,歡迎關(guān)注 背景介紹 經(jīng)過上一篇文章uni-app官方教程學(xué)習(xí)手記的學(xué)習(xí)之后,我就著手做這個(gè)項(xiàng)目了。 目前已經(jīng)初步搭出了整體的框架,秉著取之于社會,回饋于社會的原則,我將這個(gè)項(xiàng)目開源到GitHub uni-shop,發(fā)展壯大un...
閱讀 533·2023-04-25 14:26
閱讀 1291·2021-11-25 09:43
閱讀 3483·2021-09-22 15:25
閱讀 1450·2019-08-30 15:54
閱讀 525·2019-08-30 12:57
閱讀 769·2019-08-29 17:24
閱讀 3169·2019-08-28 18:13
閱讀 2685·2019-08-28 17:52