廢話部分,不想看的跳過就行了,發發牢騷
本來是不想發出來的,但是呢,最後出於裝逼,討論和分享的想法下還是拿出來。
而且我對於樹形組件的自定義節點這塊,還是沒有理解透徹。也希望有大神幫忙解惑。
然後其實我的眼界還是有限,一直都停留在ui組件上面,但是作爲一個普通的前端,大家不都幹這樣的事情嗎?既然你覺得我眼界小,那麼請說說你在幹什麼。(一次偏激的回覆)
感慨:
如果沒有網絡的世界我想我會抑鬱而死。其實作家不是因爲他想當作家,他是爲了發泄自己心中的不快。爲什麼網上肆無忌憚,因爲沒有生活中那麼多的指責。現在大家都活成了人面獸心的人了吧。
說明:
我不是一個大神,我真實的寫代碼的經歷只有一年,一開始是微信小程序開發,後面年初的時候開始寫的web網頁。我不知道爲什麼,我只有一年的代碼經歷,但是卻超過了很多人(同樣也不如很多人)。但是在這樣一個社會環境和南京這樣一個哪怕是互聯網不怎麼地的環境下,我依舊是一個菜鳥。而且我是一個耿直的人,所以我喜歡直抒胸臆,但是這個社會不讓,生活不讓。很苦惱。
一、遞歸思想
遞歸:自己調用自己
寫樹形組件肯定是可以無限嵌套的,我記得之前我寫過一篇關於遞歸組件的文章,但是那個是兩個組件相互調用從而實現樹形組件的遞歸。
這次不一樣,是一個比較省事的方式來寫。更少的代碼。
二、先寫你的容器組件
1、代碼先全部放上來
<template> <div class="dht-tree-main"> <twig-node v-for="(item, index) in data" :key="getNodeKey(item, index)" :child="item" :level="1" :data-location="[1, index]" :indent="indent" > </twig-node> </div> </template> <script> export default { name: "dhtTree", components: { twigNode: () => import("./twig-node.vue") }, props: { data: { type: Array }, indent: { // 每一層縮進多少像素 type: Number, default: 18 } }, data() { return { active: true }; }, beforeCreate() {}, created() { this.isTree = true; }, beforeMount() {}, mounted() { //是否有子元素作爲模板 this.slot = !!this.$scopedSlots.default; }, destroyed() {}, methods: { getNodeKey(node, index) { return node.id ? node.id : index; } } }; </script>
2、解析
這裏其實很簡單,就是寫一個容器,用來存放你的遞歸組件。
參數解析:
:key="getNodeKey(item, index)" :child="item" :level="1" :data-location="[1, index]" :indent="indent"
key:這個這樣寫的意義在於讓客戶可以自定義key值
child:子節點數據
level:層級,在main下是定義初始層級
data-location:用於表示每一個節點的位置,也就是定位的作用
indent:節點是需要縮進的,定義一個初始縮進值,後面將按這個計算每一層需要縮進多少
三、核心的遞歸子節點部分
這裏我會逐步拆分的來講
先放全部的html部分
<div class="dht-tree-twig-one"> <div class="dht-tree-node-content" :style="{ paddingLeft: level * indent + 'px' }" @click="showNode" > <!--箭頭--> <span v-if="child.children.length > 0" class="iconfont icon-jiantou arrow" :style="{ transform: 'rotate(' + rotate + 'deg)' }" ></span> <!--icon圖標--> <span v-if="child.icon" :class="child.icon" class="icon"></span> <!--可自定義部分--> <node-content :node="child"></node-content> </div> <transition-group name="dht-tree-node"> <twig-node v-for="(item, index) in child.children" v-show="isShow" :key="getNodeKey(item, index)" :child="item" :level="level + 1" :data-location="[level + 1, index]" :indent="indent" ></twig-node> </transition-group>
這裏主要分爲兩部分,在transition-group上面爲當前節點展示的效果,而其中的部分是組件的遞歸部分。
1、當前節點的縮進
<div class="dht-tree-node-content" :style="{ paddingLeft: level * indent + 'px' }" @click="showNode" >
這裏我很簡單,就是按層級計算縮進的像素,然後算就行了。
這個click函數是當前節點關閉或者打開。這塊後面需要單獨展開
2、實現自定義子節點,並且實現類似elementUI的插槽作用域
作用域插槽部分請看vue文檔:
https://cn.vuejs.org/v2/guide/components-slots.html#作用域插槽
<!--箭頭--> <span v-if="child.children.length > 0" class="iconfont icon-jiantou arrow" :style="{ transform: 'rotate(' + rotate + 'deg)' }" ></span> <!--icon圖標--> <span v-if="child.icon" :class="child.icon" class="icon"></span> <!--可自定義部分--> <node-content :node="child"></node-content>
這裏的箭頭還有icon都是修飾作用,意義不大。核心在於
node-content這個組件,這個組件是jsx組件。說實在,這塊我看了半天還是對於elementUI中的一些東西不理解。
這是組件的實現:
components: { nodeContent: { props: { node: { required: true } }, render(ce) { const parent = this.$parent; const tree = parent.tree; const node = this.node; // return ce("span", node.label); // console.log(tree); if (tree.slot) { return tree.$scopedSlots.default ? tree.$scopedSlots.default({ node }) : (parent.$scopedSlots = { default: () => { return node; } }); } else { return ce("span", node.label); } } } },
這裏很關鍵在於需要判斷是否是父節點,是否是當前節點的父節點,然後設置$scopedSlots。這樣就是把組件本身設置成了作用域插槽。不過說實在,我這塊代碼不是很理解。我是參考elementUI部分,寫出來的。(希望看過elementUI源碼的,或者懂的人能給我解惑下。)
這塊我只有一個模糊的概念,應該這麼寫,但是我不能自信的寫出來。
內部拆解
三個聲明:
parent 父節點
tree 一級節點,也就是第一菜單
node 當前傳入的數據編輯數據
if (tree.slot) { return tree.$scopedSlots.default ? tree.$scopedSlots.default({ node }) : (parent.$scopedSlots = { default: () => { return node; } }); } else { return ce("span", node.label); }
這裏的if部分是判斷一級菜單是否有編寫自定義的節點數據。
slot的判斷語句是這樣的,在main組件下的mouted下面
this.slot = !!this.$scopedSlots.default;
然後如果有自定義的節點數據,那麼渲染的時候就根據當前是哪一級的節點進行作用域數據渲染。
這裏我最不理解的是,我明明自定義的節點數據在main組件下,但是渲染的時候卻可以實現每一級遞歸的數據都變成一樣的節點。明明自定義的節點數據在外層啊。望大神解惑
三、當前這一層的菜單開啓和關閉
這個比較簡單,但是處理方式有兩種。
第一種:這種很麻煩,需要層層遞遞歸計算當前的菜單開啓關閉的高度。
第二種:利用vue的transition-group組件,包裹你的遞歸組件,必要的時候開啓關閉就行了
<transition-group name="dht-tree-node"> <twig-node v-for="(item, index) in child.children" v-show="isShow" :key="getNodeKey(item, index)" :child="item" :level="level + 1" :data-location="[level + 1, index]" :indent="indent" ></twig-node> </transition-group>
看v-show,就這麼簡單了。
三、遞歸子節點的代碼和性能提醒
1、先說性能
這是我剛寫博客的時候,一位掘金大神(好像還是一個漂亮妹子)提供的。
掘金暱稱:無恙作別
他說:在自定義組件渲染的時候(也就是我剛纔的那段jsx語法)如果,渲染數據超過100條會感覺到卡頓,500條就會非常明顯,但是如果是換成普通的html元素就不會卡頓。
所以,自己會寫組件也很重要的。
2、代碼部分
<template> <div class="dht-tree-twig-one"> <div class="dht-tree-node-content" :style="{ paddingLeft: level * indent + 'px' }" @click="showNode" > <!--箭頭--> <span v-if="child.children.length > 0" class="iconfont icon-jiantou arrow" :style="{ transform: 'rotate(' + rotate + 'deg)' }" ></span> <!--icon圖標--> <span v-if="child.icon" :class="child.icon" class="icon"></span> <!--可自定義部分--> <node-content :node="child"></node-content> </div> <transition-group name="dht-tree-node"> <twig-node v-for="(item, index) in child.children" v-show="isShow" :key="getNodeKey(item, index)" :child="item" :level="level + 1" :data-location="[level + 1, index]" :indent="indent" ></twig-node> </transition-group> </div> </template> <script> export default { name: "twigNode", components: { nodeContent: { props: { node: { required: true } }, render(ce) { const parent = this.$parent; const tree = parent.tree; const node = this.node; // return ce("span", node.label); // console.log(tree); if (tree.slot) { return tree.$scopedSlots.default ? tree.$scopedSlots.default({ node }) : (parent.$scopedSlots = { default: () => { return node; } }); } else { return ce("span", node.label); } } } }, data() { return { tree: null, rotate: 0, // 三角形標記 isShow: false //操作子元素關閉 }; }, props: { indent: { // 縮進 type: Number, default: 18 }, dataLocation: Array, //數據定位,表示層級和數據位置 level: Number, //當前層級 child: Object //子節點數據 }, beforeCreate() {}, created() { const parent = this.$parent; if (parent.isTree) { this.tree = parent; } else { this.tree = parent.$parent.tree; } /*if (this.child.children.length > 0 || this.level === 1) { this.isShow = true; }*/ }, beforeMount() {}, mounted() {}, destroyed() {}, methods: { getNodeKey(node, index) { return node.id ? node.id : index; }, //打開或者關閉節點 showNode() { if (this.child.children.length <= 0) return false; //操作子元素方式開啓關閉 if (this.isShow) { this.isShow = false; this.rotate = 0; } else { this.isShow = true; this.rotate = 90; } } } }; </script>
四、關於ui組件庫css
我其實一開始寫組件庫的時候css都是寫在每個組件後面的,有些人會直接用scope。
直到我寫遞歸組件我想到一個問題,vue在解析的時候每次都會解析一次css,那麼就會導致你遞歸多少次,css加載多次。如果是scope的情況下,那麼就比較崩潰了。會非常多重複的css出現。特別是寫組件庫的時候,你千萬別用scope,會很容易導致連樣式穿透也無法修改css,導致自定義性很差。
所以我現在把我的組件庫的css全部單獨抽出來了,用一個index.scss文件統一加載
下面是遞歸組件的css部分
關於scss變量部分請自己換成css屬性
.dht-tree-main { position: relative; } .dht-tree-twig-one { position: relative; //transition: height 0.5s ease; overflow: hidden; .dht-tree-node-content { display: flex; flex-flow: row; align-items: center; padding-left: 18px; height: 25px; overflow: hidden; &:hover, &:active { background: rgba(15, 128, 255, 0.1); } .arrow { font-size: 12px; color: $font_info; margin-right: 5px; transition: transform 0.5s ease; //transform: rotate(90deg); } .icon { font-size: $size-main; color: $icon-main; margin-right: 5px; } } .dht-tree-node-enter-active, .dht-tree-node-leave-active { transition: opacity .5s; } .dht-tree-node-enter, .dht-tree-node-leave-to /* .fade-leave-active below version 2.1.8 */ { opacity: 0; } }
五、使用
<dht-tree :data="data"> <span slot-scope="{ node }" class="dhtceshi">{{ node.label }}</span> </dht-tree>
data數據:裏面沒有icon,注意icon是css類
data: [ { id: 2, label: "第2個", children: [] }, { id: 1, label: "第1個", children: [ { id: 1, label: "二級第1個", children: [ { id: 1, label: "三級1第1個", children: [ { id: 1, label: "1", children: [] }, { id: 2, label: "2", children: [] }, { id: 3, label: "3", children: [] }, { id: 4, label: "4", children: [] }, { id: 5, label: "5", children: [] } ] }, { id: 2, label: "三級2第2個", children: [] }, { id: 3, label: "三級3第3個", children: [] }, { id: 4, label: "三級4第4個", children: [] }, { id: 5, label: "三級5第5個", children: [] } ] }, { id: 2, label: "二級第2個", children: [] }, { id: 3, label: "二級第3個", children: [] }, { id: 4, label: "二級第4個", children: [] }, { id: 5, label: "二級第5個", children: [] } ] }, { id: 3, label: "第3個", children: [] }, { id: 4, label: "第4個", children: [] }, { id: 5, label: "第5個", children: [] } ]
六、致謝和說明
本文已經把源碼完全貼出來了,main組件部分在一開始,遞歸子組件在最後部分
使用是單獨貼出來的第五點。
感謝elementUI團隊開源代碼