Vue學習筆記(1)——在頁面右上角實現可懸浮/隱藏的系統選單
轉自 ofollow,noindex">榕樹島
本文不能算一個教程,而是記錄一隻Vue菜鳥踩坑心路歷程的筆記,對於正在踩坑的新手或許會有一點小幫助。作為第一篇正經八百的技術部落格,歡迎大家發表看法,關愛菜鳥,請輕拍
很久以後回來更一下,這個功能可以用Vuex輕鬆搞定,此文就當練習父子元件通訊了吧,emmm……
原文: 這是個大多數網站很常見的功能,點選頁面右上角頭像顯示一個懸浮選單,點選頁面其他位置或再次點選頭像則選單隱藏。

前端攻城獅實現這個功能可以說是很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-header
、 tHeader
和 THeader
,在這種情況下注冊的元件會自動識別並渲染到 <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
;而寫入了getter
、setter
,計算屬性的值依賴於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> 複製程式碼