用vue寫輪子的一些心得(六)——nav導航欄組件

需求分析

  • 支持橫向導航豎嚮導航;
  • 支持導航click展開下拉列表;
  • 支持下拉列表click展開二級下拉列表,可任意層級;
  • 配有展開動畫;

 

方法實現

1、定義組件:

在html中定義

t-nav組件定義爲最外層導航欄包裹器,nav-item組件包裹每一項導航名字,並且每個nav-item組件都聲明一個name,用以標識每一項導航。sub-nav組件包裹含有下拉列表的導航項。

<t-nav class="box" :selected.sync="selected" vertical style="width: 200px;">
    <nav-item name="home">首頁</nav-item>
    <sub-nav name="about">
        <template slot="title">關於</template>
        <nav-item name="culture">企業文化</nav-item>
        <nav-item name="developers">開發團隊</nav-item>
        <nav-item name="contacts">聯繫電話</nav-item>
        <sub-nav name="phone">
            <template slot="title">聯繫方式</template>
            <nav-item name="cm">移動</nav-item>
            <nav-item name="cu">聯通</nav-item>
            <nav-item name="cn">電信</nav-item>
        </sub-nav>
    </sub-nav>
    <nav-item name="hire">招聘</nav-item>
</t-nav>

2、基礎功能邏輯 :

t-nav組件定義slot插槽,主要負責nav-item和sub-nav組件的傳遞。vertical代表上面html中:selected的初始選中值。

<template>
    <div class="nav-content" :class="{vertical}">
        <slot></slot>
    </div>
</template>

nav-item組件定義導航每一項,可點擊,點擊後選中值爲當前項,並展示相關樣式。

<template>
    <div class="nav-item-content" :class="{selected}" @click="onClick">
        <slot></slot>
    </div>
</template>

sub-item組件定義一組下拉導航,可點擊,根據橫向導航或是豎嚮導航展示不同下拉狀態。

<template>
    <div class="sub-nav-content" :class="{active, vertical}" v-click-outside="close">
        <span class="sub-nav-content-label" @click="onClick">
            <slot name="title"></slot>
            <span class="sub-nav-content-icon" :class="{open, vertical}">
                <t-icon name="right"></t-icon>
            </span>
        </span>
        <template v-if="vertical">
            <transition @enter="enter" @leave="leave" @after-leave="afterLeave" @after-enter="afterEnter">
                <div class="sub-nav-content-popover" v-show="open" :class="{vertical}">
                    <slot></slot>
                </div>
            </transition>
        </template>
        <template v-else>
            <div class="sub-nav-content-popover" v-show="open">
                <slot></slot>
            </div>
        </template>
    </div>
</template>

3、支持橫向導航豎嚮導航

我們在t-nav組件標籤上定義 vertical,有vertical則表示豎嚮導航,無則表示橫向導航。然後將vertical傳給nav組件和sub-nav組件,通過判斷更改組件內部flex排列樣式即可,這裏css反而比較重點也比較難寫。

t-nav組件:

<t-nav class="box" :selected.sync="selected" vertical style="width: 200px;">

nav組件:

vertical是true則CSS展示flex-direction: column。

<template>
    <div class="nav-content" :class="{vertical}">
        <slot></slot>
    </div>
</template>
<style lang="scss" scoped>
    @import "../../styles/var";
    .nav-content {
        display: flex;
        border-bottom: 1px solid $grey;
        color: $color;
        cursor: default;
        &.vertical {
            flex-direction: column;
            border: 1px solid $grey;
        }
    }
</style>

sub-nav組件:

通過vertical判斷展示哪段html,即橫向或是豎向,並展示相應CSS。

<style lang="scss" scoped>
    @import '../../styles/var';
    .sub-nav-content {
        position: relative;
        &:not(.vertical) {
            &.active {
                &::after {
                    content: '';
                    position: absolute;
                    bottom: 0;
                    left: 0;
                    border-bottom: 2px solid $blue;
                    width: 100%;
                }
            }
        }
    }
</style>

 

 4、支持導航click展開下拉列表

通過sub-nav下拉導航組件來展示,v-show="open" 是否顯示下拉列表。爲什麼用v-show,而不是v-if。因爲v-if從false到true的狀態會觸發生命週期鉤子:created、updated。v-if並不是能一開始跟隨父組件一起經歷所有的鉤子事件,而是單獨從false狀態到true狀態時觸發的。因此在數據傳遞的過程中並不是同步的,會出現BUG。

<template v-else>
    <div class="sub-nav-content-popover" v-show="open">
        <slot></slot>
    </div>
</template>

5、支持下拉列表click展開二級下拉列表,可任意層級

原理是通過sub-nav組件與nav-item組件嵌套即可實現,但sub-nav下面的nav-item與t-nav下面的sub-nav不屬於同級,層級比較深,相當於sub-nav下面的nav-item可能是層級很深的子孫組件,存在跨組件通訊的問題。

怎麼解決這個問題?使用依賴注入provide/inject,將父級的數據注入到其底下所有的子子孫孫當中,這樣無論子孫組件層級多深,我們都可以調用到父級的數據並操作。

<t-nav class="box" :selected.sync="selected" vertical style="width: 200px;">
    <sub-nav name="about">
        <sub-nav name="phone">
            <template slot="title">聯繫方式</template>
            <nav-item name="cm">移動</nav-item>
            <nav-item name="cu">聯通</nav-item>
            <nav-item name="cn">電信</nav-item>
            <sub-nav name="phone">
                <template slot="title">聯繫方式</template>
                <nav-item name="cm">移動</nav-item>
                <nav-item name="cu">聯通</nav-item>
                <nav-item name="cn">電信</nav-item>
            </sub-nav>
        </sub-nav>
    </sub-nav>
</t-nav>

父組件t-nav:

export default {
    name: 't-nav',
    provide() { //依賴
        return {
            root: this,
            vertical: this.vertical
        }
    },
}

子孫組件nav-item:

export default {
    name: 't-nav-item',
    inject: ['root'], //注入
    mounted() {
        this.root.addItem(this)
    },
}

子孫組件sub-nav:

export default {
    inject: ['root', 'vertical'], //注入
    computed: {
        active() {
            return this.root.namePath.indexOf(this.name) > -1
        },
    },
}

6、配有展開動畫

下拉的展開動畫,通過vue中的transiton動畫組件即可完成。但這裏我們特意用了transition的動畫鉤子實現的,就是定義動畫開始、動畫結束等等相關時調用鉤子來做一些操作,這在寫複雜的動畫樣式時非常有用。

<transition @enter="enter" @leave="leave" @after-leave="afterLeave" @after-enter="afterEnter">
    <div class="sub-nav-content-popover" v-show="open" :class="{vertical}">
        <slot></slot>
    </div>
</transition>
methods: {
    enter(el, done) {
        let {height} = el.getBoundingClientRect()
        el.style.height = 0
        el.getBoundingClientRect()
        el.style.height = `${height}px`
        el.addEventListener('transitionend', ()=>{
            done()
        })
    },
    afterEnter(el) {
        el.style.height = 'auto'
    },
    leave(el, done) {
        let {height} = el.getBoundingClientRect()
        el.style.height = `${height}px`
        el.getBoundingClientRect()
        el.style.height = 0
        el.addEventListener('transitionend', ()=>{
            done()
        })
    },
    afterLeave(el) {
        el.style.height = 'auto'
    },
}
&-popover {
    position: absolute;
    top: 100%;
    left: 0;
    border: 1px solid black;
    white-space: nowrap;
    background: white;
    margin-top: 4px;
    box-shadow: 0 0 3px fade_out(black, 0.8);
    border-radius: $border-radius;
    font-size: $font-size;
    color: $light-color;
    min-width: 8em;
    &.vertical {
        position: static;
        border-radius: 0;
        border: none;
        box-shadow: none;
        transition: height .3s;
        overflow: hidden;
    }
}

這裏鉤子的使用時,要注意瀏覽器的對於樣式的多次修改會執行合併操作,瀏覽器只會執行最後一個樣式。所以當我們在短時間內要執行多個樣式的時候,需要在樣式中間做點動作,讓瀏覽器無法合併樣式操作。

比如:

el.style.height = `${height}px`
el.getBoundingClientRect()
el.style.height = 0

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章