iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 3
2
Modern Web

Vue CLI + Firebase 雲端資料庫 30天打造簡易部落格及後臺管理系列 第 3

Day 03: 組件 Prop + Slot + 仿造 BootstrapVue <b-button>

Prop 是拿來做什麼呢 ? 是我們註冊完一個組件並定義好內容後,可以從父層 ( HTML標籤使用處 ) 透過 Tag Attribute 的方式傳遞資訊給子層定義的 Prop,如果硬要講 Prop 是什麼的話,那大概就是HTML標籤的客製化屬性了吧,這裡我依照 "想讓使用者傳入的屬性來定義子層 Prop " 這個概念來製作我們的組件。

  • 再正式開始說 Prop 之前,先偷渡一下尋找實感,讓我們先偷看下面這段 BootstrapVue 的 Code。
<div>
  <b-button>Button</b-button>
  <b-button variant="danger">Button</b-button>
  <b-button variant="success">Button</b-button>
  <b-button variant="outline-primary">Button</b-button>
</div>
  • 這是這段 Code 的效果

https://ithelp.ithome.com.tw/upload/images/20200910/20129819HOWouQbgql.png

  • 有沒有看見 <b-button> ,這就是 BV 自己做的組件啦。
  • 稍微回顧怎麼註冊自己要用的組件
// 1. 全域註冊
Vue.component('name-generator', {})

// 2. 局部註冊
let nameGeneratorHeHe = {...}

new Vue({
	...
  components: {
	nameGeneratorHeHe 
  }
})
  • 如此
<name-generator></name-generator>
<name-generator-he-he></name-generator-he-he>
  • 本章就要來模仿實作 BV 的 b-button ,在這之前我們要先來學習 Prop 的部份,首先註冊一個組件,我這邊選擇用全域註冊,並為組件添加一個 template 以及 props。
  • props 可以依照需求有不同寫法 ( 三種依狀況取用 )
// 避免混淆, 我取名叫 m-button 表示 my-button
Vue.component('m-button', {
	template: `
		<div class="button">
			<button type="button"></button>
		</div>
	`,

	// 第一種,單純一個陣列表示所有可傳給子組件的prop
	props: ['variant', 'text', 'other']

	// 第二種,以物件形式定義傳入 prop 的型別
	props: {
	  variant: String,
	  text: String,
	  other: ['String', 'Array', 'Boolean'] // 表示多種可傳入的型別
	}
	
	// 第三種, 針對每個 prop 做更詳細的,物件形式的定義
	props: {
	  variant: {
		type: String,
		required: true // 表示一定要提供
	  },

	  text: {
		type: String,
		default: 'button' // 預設值
	  },

	  other: ['String', 'Array', 'Boolean'], // 表示多種可傳入的型別

	  // 官網提供的 自定義驗證方式,個人目前還沒有用過這樣的模式在專案中
	  propF: {
		validator: function (value) {
		  // 这个值必须匹配下列字符串中的一个
		  return ['success', 'warning', 'danger'].indexOf(value) !== -1
		}
	  }
	}
})

// 若是傳入了不符合類型的資訊,使用開發版 Vue 將會看到警告
  • 通常我都用第三種,就算當下功能很小也一樣,如此要擴充才不用重寫形式,接下來讓我們過濾一下資訊以完成組件
BV 的按鈕,看起來,屬性 variant 以及 Button 文本是完成此組件的必要資訊
<b-button variant="danger">Button</b-button>
  • 所以依照 " 想讓使用者傳入的屬性來定義 Prop " 這個概念製作組件,我們必須定義一個 prop variant ,接著就過濾我們組件如下
Vue.component('m-button', {
    template: `
      <div class="button">
        <button type="button"></button>
      </div>
    `,
    props: {
      variant: {
        type: String,
        default: 'normal'
      }
    }
})
  • 接著我們在 HTML 中使用我們自己的 button
<m-button variant="danger">Button</m-button>
  • 然後看看我們的 button

https://ithelp.ithome.com.tw/upload/images/20200910/20129819TqYahanrn5.png

  • 咦 ? 別急我們還缺很多東西啊,我們首先確定HTML Attribute variant 有沒有正確傳值進子層,要怎麼確定呢 ? 組件有繼承 Vue 的生命週期機制和特性,也就是說在一個 Vue 實例中可以使用的一切,比如生命週期的各種掛勾 ( hooks ) created、mounted 等等、data、computed 、methods 等等,都可以使用在組件內,可以想像成組件就是 Vue 實例的縮小版,當然一個組件也是可以設計得很大的 ...。
  • 另外再完全照抄 Vue 官網的一句話 :

注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的。

  • 也就是說註冊組件並使用時 Props 會先跑且驗證後才進入創建的生命週期,理解了這樣的機制,所以我們可以在生命週期中檢查 prop。
Vue.component('m-button', {
  template: `
    <div class="mbutton">
        <button type="button"></button>
    </div>
  `,
  props: {
    variant: {
        type: String,
        default: 'normal'
    }
  },
  beforeCreate () {
    console.log('in beforeCreate')
    console.log(this.variant)
  },
  created () {
    console.log('in created')
    console.log(this.variant)
  }
})
  • 輸出

https://ithelp.ithome.com.tw/upload/images/20200910/20129819V44J0HYd3A.png

  • 所以發現 Props 的特性和 data 是一樣的,都要等到 created 之後才能夠進行存取,並且目前我們的字串 danger 是有傳進子層的。
<m-button variant="danger">Button</m-button>
  • 接下來,為何 Button文本 沒有顯示呢 ? 是否記得 Day 02 中有提到,生命週期若有提供 template,會完全把 template 的內容編譯替換過去,也就是說你原本若是有寫東西,他是會不見的,舉個例子 review 下 ( 只是舉例,通常比較少在外面的 Vue 實例寫 template,因為所有東西都會被 template 覆蓋,如果真的這麼做,那你所有東西都必須塞進 template了 )。
<div id="app">
  aka allen
</div>

... in script tag

let vm = new Vue({
  template: `
    <div class="NewTemplate">
      Elvis
    </div>
  `
})

// 先不要掛載,註解起來
// vm.$mount("#app")
  • 拿掉註解前

https://ithelp.ithome.com.tw/upload/images/20200910/20129819sHL2brukxa.png

  • 拿掉註解,現在我變 Elvis 了啦,今天逛爆廢公社了沒,真是廢爆了。

https://ithelp.ithome.com.tw/upload/images/20200910/20129819uqUuD6bQUC.png

  • 也就是說 template 就是大局,而接下來要說的 Slot ( 插槽 ),就是讓我們可以在外部插入內容的關鍵利器,你可以把 template 想成主機板,而我們的主機板不是有很多的插槽嗎 ?
  • 有記憶體的

https://ithelp.ithome.com.tw/upload/images/20200910/20129819SED7Py36YQ.png

圖片來源: https://ofeyhong.pixnet.net/blog/post/68205660

  • CPU 的

https://ithelp.ithome.com.tw/upload/images/20200910/20129819qb3Qm0DQPh.png

圖片來源: https://zh.wikipedia.org/zh-mo/Socket_370

  • 搞什麼東西隨便 Google 人家圖片也太廢了吧,會不會被吉阿。想像一下,如果今天整塊主機板都已經封裝焊死,板子上沒有其他插槽留給你,零件也拔不下來的話,那我今天要怎麼更換顯示卡,記憶體這些零件呢 ? 所以我們的 slot 就好像我們幫使用者預留的插槽一樣,讓使用者可以在外部更換自己想使用的零件啦,so :
<div id="app">
  aka allen
</div>

... in script tag

// 加入slot
let vm = new Vue({
  template: `
    <div class="NewTemplate">
	  <slot></slot>
      Elvis 
    </div>
  `
})

vm.$mount("#app")
  • 看看結果 ? 咦 ? 怎麼還是只有 Elvis ? 原來我們的 slot 不能用在最外面的 Vue 實例,只能用在組件裡,如果有人知道怎麼在 Vue 實例 template 使用 slot 的話請留言教學一下,讓我也學習學習。
  • 回到 m-button 加入 slot ,console什麼的可以刪了啦。
<m-button variant="danger">Button</m-button>

// ...

Vue.component('m-button', {
  template: `
    <div class="mbutton">
      <button type="button">
		<slot></slot>
	  </button>
    </div>
  `,
  props: {
	variant: {
	  type: String,
		default: 'normal'
	}
  }
})
  • Button 出現了

https://ithelp.ithome.com.tw/upload/images/20200910/20129819vFEkfOLjtA.png

  • 注意我們的插槽位置,事實上他可以放在任何位置,比如說
template: `
  <div class="mbutton">
	<slot></slot>
    <button type="button"></button>
  </div>
`
  • 如此這樣顯示

https://ithelp.ithome.com.tw/upload/images/20200910/20129819Gs04jp4RKx.png

  • 換到這邊
template: `
  <div class="mbutton">
    <button type="button"></button>
	<slot></slot>
  </div>
`
  • 變這樣

https://ithelp.ithome.com.tw/upload/images/20200910/20129819FaT37WHCjs.png

  • 太調皮了ㄅㄏㄏ,針對 slot ,還可以做更進一步的使用,不過目前為止的需求使用到這邊就足夠了,我們先這樣就好,現在先把 slot 安裝回正確的位置,這樣我們主機板上的記憶體才不會插在莫名其妙的版面配置中 ...。

  • 接下來為我們的按鈕準備 style ,經過去 BV 官網偷看,以下是狀態按鈕的色票們:

    預設 = #6c757d,danger = #dc3545 , success = #28a745,primary = #007bff,依照這些色票,開始包裝我們的組件,我們將使用 computed 屬性來替換 class 的方法來達成目標。

1. 刪掉 div.mbutton 那層,以button作為根元素就好
2. class="btn" 與 :class=" 'btn-' + dynamicClass" 是可以共存的,最終兩個會一起渲染,利用這個特性,準備一個共用的.btn
3. 而 dynamicClass 將會被 computed 的 dynamicClass getter 返回一個新值
   隨使用者傳入的不同字串,動態的被改變。
4. v-bind 除了可以綁定 data member, 也可綁定 props member ,相對的 computed 也可以監測 props 變化,可以把 props 當成另一種形式的 data。

Vue.component('m-button', {
  template: `
    <button type="button" class="btn" :class=" 'btn-' + dynamicClass" >
      <slot></slot>
    </button>
  `,
  props: {
    variant: {
      type: String,
      default: 'normal'
    }
  },
  computed: {
    dynamicClass () {
      return this.variant
    }
  }
})
<m-button>Button</m-button>
<m-button variant="success">Button</m-button>
<m-button variant="primary">Button</m-button>
<m-button variant="danger">Button</m-button>

隨著傳入字串的不同,HTML確實被動態的改變了,並且預設沒傳入 variant 也確實地藉由設定 default 來進行預設的渲染。

https://ithelp.ithome.com.tw/upload/images/20200910/20129819SVZ910VoUK.png

  • 我們的按鈕目前還長這樣

https://ithelp.ithome.com.tw/upload/images/20200910/20129819g9Cx2uLgiw.png

  • 準備 CSS
// 先準備 .btn 作為按鈕基本型
.btn {
  border-radius: 5px;
  color: white;
  padding: .375rem .75rem;
  border: 1px solid transparent;
  box-shadow: 0 1px 1px 0px black
}

// 定義各色塊
// 預設 = #6c757d , success = #28a745 , primary = #007bff , danger = #dc3545

.btn-normal {
  background-color: #6c757d;
}

.btn-success {
  background-color: #28a745;
}

.btn-primary {
  background-color: #007bff;
}

.btn-danger {
  background-color: #dc3545;
}

最後這樣:

https://ithelp.ithome.com.tw/upload/images/20200910/201298190LxMTngloh.png

做到這邊,我們模仿 BV 的 算是成功了,你可以自己擴充關鍵字色票建立自己的基本按鈕,hover 效果也可以自己處理,不過目前按鈕還沒有實質效果,下一篇將解說 Slot 更進一步的應用以及組件 Event 的建立及監聽。

https://codepen.io/fiftybillionHuang/pen/BaKwvjN

沒事也可以逛逛我們其他團隊成員的文章啦 ~~

eien_zheng: 前端小嘍嘍的Golang學習旅程_The journey of learning Golang 系列

PollyPO技術: 前端設計轉前端工程師-JS踩坑雜記 30 天 系列

阿電: 忍住不打牌位,只要30天VueJS帶你上A牌 系列

喬依司: 實作經典 JavaScript 30 系列


上一篇
Day 02 : 比較少用的生命週期
下一篇
Day 04: 組件自定義發射事件,父子資料傳遞
系列文
Vue CLI + Firebase 雲端資料庫 30天打造簡易部落格及後臺管理30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言