Vue學習筆記(1)——在頁面右上角實現可懸浮/隱藏的系統選單

榕樹島發表於2019-03-01

轉自榕樹島

本文不能算一個教程,而是記錄一隻Vue菜鳥踩坑心路歷程的筆記,對於正在踩坑的新手或許會有一點小幫助。作為第一篇正經八百的技術部落格,歡迎大家發表看法,關愛菜鳥,請輕拍

很久以後回來更一下,這個功能可以用Vuex輕鬆搞定,此文就當練習父子元件通訊了吧,emmm……


原文:
這是個大多數網站很常見的功能,點選頁面右上角頭像顯示一個懸浮選單,點選頁面其他位置或再次點選頭像則選單隱藏。

Vue學習筆記(1)——在頁面右上角實現可懸浮/隱藏的系統選單

作為一個jQuery前端攻城獅實現這個功能可以說是很easy了,但是對只剛粗看了一遍vue文件的菜鳥來說,坑還是要親自踩過才算圓滿。

知識點


  • 元件及元件間通訊
  • 計算屬性

正文


1. 父元件

這裡暫時只涉及系統選單這一個功能,因此路由暫未涉及。
基本思路是:通過props將showCancel這個Boolean值傳遞到子元件,對父子元件分別繫結事件,來控制這個系統選單的顯示狀態。其中在父元件的繫結click事件中,將傳入子元件的showCancel值重置。
這裡就涉及第一個小知識點——子元件呼叫:
首先寫好等待被子元件渲染的自定義元素:

<t-header :showCancel=showCancel></t-header>
複製程式碼

接著import寫好的子元件:

import THeader from "./components/t-header/t-header";
複製程式碼

然後在元件中註冊子元件:

components: {
  THeader
}
複製程式碼

到這裡,新入坑的同學可能會比較疑惑這幾行程式碼是怎樣把子元件對應到<t-header>標籤的,官方文件是這樣說的:

當註冊元件 (或者 prop) 時,可以使用 kebab-case (短橫線分隔命名)、camelCase (駝峰式命名) 或 PascalCase (單詞首字母大寫命名);
HTML 模板中,請使用 kebab-case;

我的理解是(舉例),當自定義元素為<t-header>時,註冊元件名可以有三種寫法:t-headertHeaderTHeader,在這種情況下注冊的元件會自動識別並渲染到<t-header>
需要注意的是,以上使用的是HTML 模板,是在單檔案元件裡用<template><template/>指定的模板;另外存在一種字串模板,是用在元件選項裡用template: "" 指定的模板。當使用字串模板時,自定義標籤可以用三種寫法,具體情況請移步官方文件 元件命名約定
這樣父元件的雛形就誕生了:

<template>
  <div id="app" @click="hideCancel">
    <t-header :showCancel=showCancel></t-header>
    <!-- <router-view/> -->
  </div>
</template>

<script>
  import THeader from "./components/t-header/t-header";
  export default {
    name: "app",
    components: {
      THeader
    },
    data() {
      return {
        showCancel: false
      };
    },
    methods: {
      hideCancel() {
        this.showCancel = false;
      }
    }
  };
</script>
複製程式碼

2. 子元件

子元件中.cancel為開啟系統選單的按鈕,.cancel-div為系統選單,最開始是這個樣子:

<template>
  <div class="header-wrapper">
    /*這裡是logo和title*/
    ...
    /*這裡是使用者名稱和按鈕*/
    <div class="info-wrapper">
      <span class="username">你好,管理員!</span>
      <span class="cancel" @click.stop="switchCancelBoard">
        <div class="cancel-div" v-show="showCancel">
          <ul>
            <li @click.stop="doSomething" title="使用者設定">設定 </li>
            <li @click.stop="doSomething" title="退出登入">退出 </li>
          </ul>
        </div>
      </span>
    </div>
  </div>
</template>
複製程式碼

按照踩坑之前的思路,在子元件接到showCancel值後用v-show控制顯示隱藏,那麼在父子元件的繫結click事件中只需要根據情況更改showCancel值就可以了,只要注意對系統選單內幾個選項的繫結事件不要觸發父子元件上的繫結事件——總不能一點選單它就沒了,所以在繫結事件中用到了.stop,即
@click.stop="doSomething"
於是萬事大吉,也就是像這樣:

<script>
  export default {
    props: {
      showCancel: {
        type: Boolean
      }
    },
    methods: {
      doSomething() {},
      switchCancelBoard() {
        this.showCancel = !this.showCancel;
      }
    },
    computed: {
      ifShowCancel() {
        return this.showCancel;
      }
    }
  };
</script>
複製程式碼

然而第一波踩坑之後一起表明顯然我還是太年輕。下面是一些不好的示範

  • prop來的showCancel值的確可以用,點選子元件按鈕的時候,

    this.showCancel=!this.showCancel

    實現了選單的顯示/隱藏,但是一開啟控制檯,每次點選貢獻了一條報錯:

    vue.esm.js?efeb:578 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop`s value.

    意思是:避免修改prop值,因為父元件一旦re-render,這個值就會被覆蓋;

    另外,儘管在這個按鈕上實現了顯示狀態的切換,但是點選其他區域的時候,並不會隱藏它,原因是:子元件prop值的變化並沒有影響到父元件,因此showCancel的值一直保持初始值沒有變化,而只有在這個值被更新時才會觸發子元件中相關值的更新

    ——好吧,那麼老老實實的用一個計算屬性接收showCancel值,這樣實現點選子元件控制系統選單的狀態切換;

  • 獲得了計算屬性ifShowCancel,元件相應的變成了v-show="ifShowCancel",我試圖在繫結事件裡通過this.ifShowCancel=!this.ifShowCancel切換選單狀態,報錯,得到報錯資訊:Computed property "ifShowCancel" was assigned to but it has no setter

    明白了,要以直接賦值的形式改變計算屬性ifShowCancel的值,需要一個setter函式,但是setter函式中無法修改prop值,因此在getter中也就無法通過return this.showCancel來更新這個計算屬性,所以這個方法貌似也行不通;

    到此為止,好像路都成了堵死狀態:prop值不能改->要用計算屬性;計算屬性不能改->需要setter;而寫入了gettersetter,計算屬性的值依賴於prop值->prop值不能改。——一個堪稱完美的閉環誕生了!

    走投無路之際我想起了$emit$on這一對。

3. 父子互相通訊

前邊的prop實現了從父到子的單向通訊,而通過$emit$on,就可以實現從子元件到父元件的通訊:這不能直接修改父元件的屬性,但卻可以觸發父元件的指定繫結事件,並將一個值傳入父元件。
在這一步我摒棄了點選按鈕時的去操作子元件內屬性的想法,既然計算屬性ifShowCancel依賴於prop值,那麼就在點選按鈕時,通過$emit觸發父元件的事件,並將需要修改的屬性值傳入父元件,於是:

<!--父元件自定義元素繫結switch-show事件-->
<t-header :showCancel=showCancel @switch-show="switchShow"></t-header>
複製程式碼
// 父元件js
methods: {
  //會被子元件$emit觸發的方法
  switchShow(val) {
    this.showCancel = val;
  }
}

// 子元件js
methods: {
  //按鈕上的繫結click事件
  switchCancelBoard() {
    this.$emit("switch-show", this.ifShowCancel);
  }
}
複製程式碼

這樣處理流程就變成了:點選按鈕->ifShowCancel值傳入父元件並觸發父元件事件,對showCancel賦值->父元件屬性更新->子元件prop更新->重新compute,更新ifShowCancel值->v-show起作用。
另外在點選其他區域時,通過父元件繫結的click事件,就可以重置showCancel值,進而隱藏掉出現的系統選單。

4. 完整程式碼

/*父元件*/
<template>
  <div id="app" @click="hideCancel">
    <t-header :showCancel=showCancel @switch-show="switchShow"></t-header>
    <!-- <router-view/> -->
  </div>
</template>

<script>
  import THeader from "./components/t-header/t-header";

  export default {
    name: "app",
    components: {
      THeader
    },
    data() {
      return {
        showCancel: false
      };
    },
    methods: {
      hideCancel() {
        this.showCancel = false;
      },
      switchShow(val) {
        this.showCancel = val;
      }
    }
  };
</script>

<style scope lang="stylus">
</style>
複製程式碼
/*子元件*/
<template>
  <div class="header-wrapper">
    <div class="title-wrapper">
      <div class="logo"></div>
      <h2 class="title">Title</h2>
    </div>
    <div class="info-wrapper">
      <span class="username">你好,管理員!</span>
      <span class="cancel" @click.stop="switchCancelBoard">
        <div class="cancel-div" v-show="ifShowCancel">
          <ul>
            <li @click.stop="doSomething" title="使用者設定">設定 </li>
            <li @click.stop="doSomething" title="退出登入">退出 </li>
          </ul>
        </div>
      </span>
    </div>
  </div>
</template>

<script>
  export default {
    props: {
      showCancel: {
        type: Boolean
      }
    },
    methods: {
      doSomething() {},
      switchCancelBoard() {
        // this.ifShowCancel = !this.showCancel;
        this.$emit("switch-show", !this.ifShowCancel);
      }
    },
    computed: {
      ifShowCancel() {
        return this.showCancel;
      }
    }
  };
</script>
<style lang="stylus" rel="stylesheet/stylus" scoped>
  .header-wrapper
    background: #1C60D1
    color: #fff
    width: 100%
    height: 50px
    line-height: 50px
    position: fixed
    top: 0px
    left: 0px
    font-size: 0
    .title-wrapper
      display: block
      position: relative
      float: left
      height: 50px
      .logo
        display: inline-block
        background-image: url(`./logo.png`)
        background-size: 30px 30px
        background-repeat: no-repeat
        width: 30px
        height: 30px
        margin-top: 10px
      .title
        display: inline-block
        font-size: 16px
        height: 50px
        line-height: 50px
        margin: 0px auto 0px 16px
        font-weight: normal
        vertical-align: top
    .info-wrapper
      display: block
      position: relative
      float: right
      height: 50px
      width: 160px
      font-size: 0
      .username
        display: inline-block
        height: 50px
        line-height: 50px
        font-size: 14px
        vertical-align: top
      .cancel
        display: inline-block
        vertical-align: middle
        background-image: url(`./cancel.png`)
        background-size: 32px 32px
        cursor: pointer
        background-repeat: no-repeat
        width: 32px
        height: 32px
        .cancel-div
          position: absolute
          display: block
          width: 60px
          height: 80px
          background: #fff
          z-index: 50
          top: 40px
          right: 16px
          font-size: 14px
          color: #646464
          box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.4)
          ul
            padding-left: 0px
            margin: 0px
            li
              width: 100%
              height: 40px
              line-height: 40px
              text-align: center
              list-style-type: none
              &:hover
                background-color: #eaeaea
</style>
複製程式碼

相關文章