Vue 組件封裝

Vue 組件封裝

項目中沒有從零開始封裝一個組件,本文記錄一下 Vue 組件封裝的基本實踐和一些組件的相關知識。主要涉及如下知識點:javascript

  • 封裝一個組件的代碼組織形式;
  • vue 組件的三大核心:
    • 屬性(props、data);
    • 事件
    • 插槽
  • 樣式
  • 其餘一些雜項
    • $nextTick 函數的使用
    • 獲取 DOM 元素及在父級組件中執行子組件方法

使用第三方計數庫 countup.js 建立一個 count-to 組件對以上知識進行總結。css

文件組織形式

在組件文件夾 component 下建立一個與組件名相同的文件,文件夾內必須有 index.js,並將組件導入到該文件中,這樣方便咱們引用組件。html

count-to 文件夾內:vue

//index.js
import CountTo from './count-to.vue'
export default CountTo
複製代碼

使用組件時,只需這樣引入:java

import CountTo from  "_c/count-to";// _c 是組件存放路徑
複製代碼

Vue 組件的三大核心

屬性(props、data 和樣式)

props 定義了組件可配置的數據,肯定的組件的核心功能。封裝組件時,props 推薦寫成對象形式,方便對數據進行驗證,提升了代碼健壯性也能明確如何使用。react

常見的檢查類型:NumberStringBooleanArrayObjectDateFunctionSymbol構造函數null|undefined 會經過全部類型。git

還能夠自定義驗證函數,指定是否必須和默認值。github

props:{
	// 多個可能的類型
  propB: [String, Number],
	// 必填的字符串
  propC: {
    type: String,
    required: true
  },
  // 帶有默認值的數字
  propD: {
    type: Number,
    default: 100
  },
  // 帶有默認值的對象
  propE: {
    type: Object,
    // 對象或數組默認值必須從一個工廠函數獲取
    default: function () {
      return { message: 'hello' }
    }
  },
  // 自定義驗證函數
  propF: {
    validator: function (value) {
      // 這個值必須匹配下列字符串中的一個
      return ['success', 'warning', 'danger'].indexOf(value) !== -1
    }
  }
}
複製代碼

經過閱讀 countUP文檔,瞭解到構造函數CountUp 的參數web

CountUp(eleDOM,startValue,endValue,decimals,duration,options);// eleDOM 是數值顯示的元素;endValue 是數值的最終值,這兩個參數必須的。
複製代碼

組件代碼以下:segmentfault

<template>
  <div>
    <span :id="eleId"></span>
  </div>
</template>
<script> import CountUp from "countup"; export default { name: "CountTo", props: { /** * @description 起始值 */ startValue: { type: Number, default: 0 }, /** * @description 終止值 */ endValue: { type: Number, required: true }, /** * @description 小數點後保留幾位小數(精度) */ decimals: { type: Number, default: 0 }, /** * @description 漸變時長(秒) */ duration: { type: Number, default: 1 }, /** *@description 變速效果 */ useEasing: { type: Boolean, default: false }, /** *@description 分組 */ useGrouping: { type: Boolean, default: true }, /** *@description 分組符號 2,2234 */ separator: { type: String, default: "," }, /** *@description 整數小數分隔符 34.56 */ decimal: { type: String, default: "." }, /** * @description 動畫延遲(秒) */ delay: { type: Number, default: 0 }, }, data() { return {}; }, computed: { eleId() { //使用 this.uid 生成全局惟一id return `count_up_uid${this._uid}`; }, }, mounted() { //TODO: this.$nextTick this.$nextTick(() => { let options = { useEasing: this.useEasing, useGrouping: this.useGrouping, separator: this.separator, decimal: this.decimal }; this.counter = new CountUp( this.eleId, this.startValue, this.endValue, this.decimals, this.duration, options ); }); } }; </script>
複製代碼

代碼說明: this._uid 用於生成組件內惟一的id值,可用做元素的id,值是遞增的。 this.$nextTick 函數接收一個回調函數做爲參數,回調函數會在 DOM更新 以後執行,若是某些操做必須在DOM更新以後,可將這些操做做爲其參數。

計數組件的基本功能就知足了。

這樣使用組件:

<template>
	<div>
		<count-to :end-value="endValue" :decimals="decimals" :duration="5" title="這個會掛載到組件根元素上">
		</count-to>
	</div>
</template>
<script> import CountTo from '_c/count-to' export default { name: 'count_to', components: { CountTo }, data() { return { endValue: 4000, decimals: 2, className: '', } }, } </script>
複製代碼
<count-to :end-value="endValue" :decimals="decimals" :duration="5"></count-to>
複製代碼

prop 的命名:

組件中使用小駝峯命名,傳遞值是使用-

關於 props 傳遞靜態值:

不使用 v-bind 指令:傳遞的是靜態值,是一個字符串字常量,而不是變量,而使用:指令傳遞的值,是有類型的。:duration="5" 傳遞是 數值 5,duration="5" 傳遞字符串5duration="true" 傳遞的是字符串true 而不是 Boolean 值真值。

默認值:

傳遞是引用類型的值(對象和數組),默認值須要使用一個工廠函數返回一個引用類型的值。

inheritAttrs:

若是傳遞一個組件中沒有聲明的屬性,該屬性會掛載都組件元素上,可在組件中將inheritAttrs 設置爲 false 取消這一行爲。上面的 title 屬性會掛載到組件的 div 上。該屬性不該 style 和 calss 的傳遞。

<count-to title="會掛載到組件的根元素上" test="test" :end-value="endValue" :decimals="decimals" :duration="5">	</count-to>
複製代碼

title 會成爲count-to 組件的根元素的屬性:

<div title="這是標題" test="測試">
	<span id="count_up_uid14" >10,000.00</span>
</div>
複製代碼

$attrs 接收沒有聲明的屬性

title 和 test 屬性沒有在組件中聲明,依然能夠在組件中使用 attrs 接收到些屬性: <span>沒有props接收的父組件數據:{{$attrs}}</span><br/>

最後的結果:

<div title="這是標題" test="測試">
	<span>沒有props接收的父組件數據:{
		"title": "這是標題",
		"test": "測試"
	}</span><br>
	<span id="count_up_uid14">10,000.00</span>
</div>
複製代碼

inheritAttrs: false 和 $attrs 結合使用:

有了 inheritAttrs: false 和 $attrs,你就能夠手動決定這些特性會被賦予哪一個元素,而不須要聲明變量接收

{% raw %}

See the Pen $attrs使用 by JackZhouMine (@JackZhouMine) on CodePen.

{% endraw %}

data vs props

props 從父級組件入,傳入的值由父級組件維護,不容許在子組件中直接操做,是否必需和數據類型都是肯定的,咱們不能改變。

data 是組件內部維護的狀態,組件可直接操做,可隨時改變值、類型等。

相同點:都是組件的屬性,改變二者都會響應到模板上。

打破 props 單向數據流

Vue 不容許在子組件中直接操做 props ,不然會報錯,由於父組件和子組件均可直接操做 props,會使得 props 的管理變得混亂。可經過一些間接的方式操做 props:

  1. 將 props 賦值給 data ,而後操做 data;
  2. 在計算屬性中返回 props;

以上兩種方式,修改後的值,是不能會響應到父組件的,想要在父級組件中也看到修改,須要用到下面的方式:

  1. .sync 和 $emit 結合

傳遞props 時加上 .sync 修飾符,在子組件內部使用 $emit 更新 props。

使用 .sync 須要注意:

  • 不能和表達式一塊兒使用:v-bind:title.sync="doc.title + '!'";
  • 不能傳遞對象字面量:v-bind.sync="{ title: doc.title }"
  1. 傳遞引用類型的 props

傳遞數組和對象,在子組件中修改他們,會直接反應到父組件上。

事件

傳統的web開發使用事件驅動:

  • 查詢節點→綁定事件監聽;
  • 用在頁面上觸發事件→執行監聽器,修改DOM,反饋到頁面上; 這種模式開發效率低成本高。

Vue 的核心思想是數據驅動,視圖由數據決定。MVVM 架構的頁面變化流程:

View(用戶操做) → 執行 DOMlistenrs (ViewModel) → Data 改變 (Model)→ View 改變。

組件和綁定原生事件和自定義事件,綁定原生事件時,須要添加native修飾符。

能夠在組件的原生事件處理器中觸發一個自定義事件,就能在父級組件中監聽該事件,執行相關操做。

count-to 聲明一個 changeValue 事件:

增長一個按鈕:

<button @click="add">+</button>
複製代碼

在事件處理器add中觸發一個自定義事件:

add() {
	this.$emit("changeValue", Math.random() * 100);
}
複製代碼

$emit 的第一個參數是事件名稱,第二個參數是傳遞到該事件監聽器的參數。

在組件上監聽 changValue:

<template>
	<div>
		<count-to :end-value="endValue" :decimals="decimals" :duration="5" @changeValue="changeValue">
		</count-to>
	</div>
</template>
<script> import CountTo from '_c/count-to' export default { name: 'count_to', components: { CountTo }, data() { return { endValue: 4000, decimals: 2, } }, methods: { changeValue(value) { this.endValue += value } }, } </script>
複製代碼

自定義一個更新結束事件:

<script> import CountUp from "countup"; export default { name: "CountTo", methods: { getCount() { //使用 id 獲取 DOM let span = document.getElementById(this.eleId); let currentValue = Number.parseFloat(span.innerText.split(",").join("")); return currentValue.toFixed(this.decimals); }, emitEnd() { this.$emit("on-end", this.getCount()); // this.$emit('on-end', this.endValue) 使用 endValue 不是 庫處理後的值,全部使用 DOM 元素獲取更新後的值 }, }, // 監聽 props 屬性的變化 watch: { endValue(newValue) { //update 是庫的方法 this.counter.update(newValue); setTimeout(() => { this.emitEnd(); }, this.duration * 1000 + 2); } } }; </script>
複製代碼

在組件上使用監聽on-end:

<template>
	<div>
		<count-to :end-value="endValue" :decimals="decimals" :duration="5" @on-end="endUp">
		</count-to>
	</div>
</template>
<script> import CountTo from '_c/count-to' export default { name: 'count_to', components: { CountTo }, data() { return { endValue: 4000, decimals: 2, } }, methods: { // 更新接收後,會觸發自定義事件,而後執行該函數 endUp(value) { console.log('endValue => ', value); }, }, } </script>
複製代碼

表單修飾符

  • lazy : 在change事件同步數據;
  • trim : 刪除首尾空格;
  • number :只能輸入數字;

事件修飾符

  • stop:阻止冒泡;
  • prevent :阻止默認行爲;
<!-- 阻止單擊事件繼續傳播 -->
<a v-on :click.stop="doThis"></a>
<!-- 提交事件再也不重載頁面 -->
<form v-on :submit.prevent="onSubmit"></form>
<!-- 修飾符能夠串聯 -->
<a v-on:click.stop.prevent="doThat"></a>
複製代碼

插槽

props 傳遞普通的數據類型,插槽提供了傳遞 HTML 代碼的方式,父組件中給的插槽內容,會被放置到子組件的指定爲位置。

父組件決定是否顯示插槽和怎樣顯示,子組件決定插槽顯示的位置。

三種插槽:

  • 匿名插槽;
  • 命名插槽;
  • 做用域插槽。

咱們如今想要在 數值左邊顯示一個從父級組件傳遞到組件中的文字提示,數值右邊顯示人民幣符號。

可以使用插槽接收文字提示和人民幣符號:

<template>
  <div>
	<!-- 匿名插槽 找不到放置的位置,就放在這裏-->
    <slot></slot>
    <span :id="eleId"></span>
    <slot name="right"></slot>
		<!-- 命名插槽-->
  </div>
</template>
複製代碼

在父級組件傳遞插槽內容:

<template>
	<div>
		<count-to :end-value="endValue" :decimals="decimals" :duration="5">
			<span>金額:</span>
			<span slot="right"></span>
		</count-to>
	</div>
</template>
複製代碼

最後的html是這樣的:

<div>
	<span>金額:</span>
	<span id="count_up_uid13" >4,000.00</span>
	<span></span>
</div>
複製代碼

不傳遞插槽內容時,能夠在組件中設置一個默認的插槽內容:

<template>
  <div>
    <slot>獎金額度:</slot>
    <span :id="eleId"></span>
    <slot name="right"></slot>
  </div>
</template>
複製代碼

父級組件的做用域和子組件的做用是獨立的,在父級組件的插槽內容中,獲取不到子組件的數據。

<template>
	<div>
		<count-to :end-value="endValue" :decimals="parentDecimals" :duration="5">
			<span>精確到幾位小數:{{parentDecimals}}</span>
			<span slot="right">{{decimals}}</span>
		</count-to>
	</div>
</template>
複製代碼

parentDecimals 是父級組件中的屬性,插槽內容屬於父級做用域,可獲取父級的數據; decimals 是子級組件中的屬性,插槽內容屬於父級做用域,獲取不到值;

想要在父級插槽內容中獲取子組件的數據,就須要用到做用域插槽。

如今想要把數值前面的文字從父級組件傳遞到子組件,而且還要傳遞文字的顏色:

text: {
	name: "本月工資",
	color: "#F4D03F"
},
複製代碼

子組件這樣定義:

<template>
  <div>
		<!--向父級組件傳遞text 並起了名字-->
    <slot v-bind="text" name="left">獎金額度:</slot>
    <span :id="eleId" ref="number"></span>
    <slot name="right"></slot>
  </div>
</template>
<script> import CountUp from "countup"; export default { name: "CountTo", props: { //增長 prop  text:{ type:Object, default:()=>{} }, } }; </script>
複製代碼

這樣使用組件:

<template>
  <div>
    <count-to :end-value="endValue" :decimals="decimals" :duration="5" :text="text" >
      <template slot-scope="data" slot="left">
        <span :style="{color:data.color}">{{data.name}}:</span>
      </template>
      <span slot="right"></span>
    </count-to>
  </div>
</template>
<script> import CountTo from "_c/count-to"; export default { name: "count_to", components: { CountTo }, data() { return { text: { name: "本月工資", color: "#F4D03F" }, endValue: 4000, decimals: 2, }; } }; </script>
複製代碼

<slot v-bind="text">獎金額度:</slot>,向父級組件傳遞數據; slot-scope="data" 用來接收插槽傳遞到父組件的數據;

新指令 v-slot

在 2.6.0 中,咱們爲具名插槽和做用域插槽引入了一個新的統一的語法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 。

子組件:

<template>
  <div>
		<!-- 向父級組件傳遞 textFromChild -->
    <slot :textFromChild="text" name="left">獎金額度:</slot>
    <span :id="eleId" ref="number"></span>
    <slot name="right"></slot>
  </div>
</template>
<script> import CountUp from "countup"; export default { name: "CountTo", props: { //增長 prop  text:{ type:Object, default:()=>{} }, } }; </script>
複製代碼

這樣使用組件:

<template>
  <div>
    <count-to :end-value="endValue" :decimals="decimals" :duration="5" :text="text" >
      <template v-slot:left="{textFromChild}">
        <span :style="{color:textFromChild.color}">{{textFromChild.name}}:</span>
      </template>
      <span slot="right"></span>
    </count-to>
  </div>
</template>
複製代碼

子組件傳遞過來的變量被放置在一個對象中,使用解構賦值的方式提取出來。

<template v-slot:left="{textFromChild}">
  <span :style="{color:textFromChild.color}">{{textFromChild.name}}:</span>
</template>
複製代碼

v-slot 指令後跟一個 slot 的名字,插槽具備名字時,可簡寫爲#

<template #left="{textFromChild}">
  <span :style="{color:textFromChild.color}">{{textFromChild.name}}:</span>
</template>
複製代碼

無論有幾個插槽,都把插槽內容放置在 template 中是很好的作法。

其餘雜項

組件生成 id

使用this_uid其餘字母,可成組件內惟一的id。 count-to組件中,咱們使用計算屬性,設置 span 的 id。

eleId() {
      //使用 this.uid 生成全局惟一id
      return `count_up_uid${this._uid}`;
    },
複製代碼

在組件內部,能夠經過 id 或者 class 等獲取到 dom,可是不推薦這麼作。可經過ref 屬性,獲取到DOM,更加簡潔,而且能夠直接經過ref 獲取組件或者DOM元素。

在下面的函數中獲取DOM:

getCount() {
      // TODO: 獲取 DOM
      //使用 ref 屬性獲取 DOM 元素
      // console.log(this.$refs.number.innerText)
      // return this.$refs.number.innerText

      //使用 id 獲取 DOM
      let span = document.getElementById(this.eleId);
      let currentValue = Number.parseFloat(span.innerText.split(",").join(""));
      return currentValue.toFixed(this.decimals);
    },
複製代碼

$nextTick 函數的使用

this.$nextTick 接收一個回調函數做爲參數,參數會在 Vue 完成DOM 更新後當即調用。若是某些操做是依賴DOM更新後的,能夠把這些操做放在回調函數裏執行。

  • 在 created 和 mounted 階段,若是須要操做渲染後的試圖,也要使用 nextTick 方法。
  • mounted 不會承諾全部的子組件也都一塊兒被掛載。若是你但願等到整個視圖都渲染完畢,能夠用 vm.$nextTick 替換掉 mounted。

Vue.$nexttick 全局的,this.$nexttick 是局部的。

var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改數據
vm.$el.textContent === 'new message' // false 此時DOM還沒渲染
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})
複製代碼

Vue DOM 的更新是異步的,數據變化後,組件不會當即渲染,而是在事件隊列刷新時,在下一個事件循環 tick 中渲染。

$nexttick 返回一個 Promise,可以使用 await 關鍵詞調用。

methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}
複製代碼

在父級組件中調用子組件的方法

有時候須要再父級組件中調用子組件的方法。能夠在使用組件時指定 ref ,而後使用 ref 調用。 好比調用組件的暫停方法,使得數據變化暫停。

在組件中定義暫停方法:

<template>
  <div>
    <slot :textFromChild="text" name="left">獎金額度:</slot>
    <span :id="eleId" ref="number" :class="countClass"></span>
    <slot name="right"></slot>
  </div>
</template>
<script> import CountUp from "countup"; export default { name: "CountTo", data() { return {}; }, methods: { //TODO: 在父級組件中使用封裝組件內部的方法 // 在父級組件中調用該方法,實現暫停 pause() { this.counter.pauseResume(); } } }; </script>
複製代碼

在父組件中使用調用組件暫停方法。

<template>
  <div>
    <count-to :end-value="endValue" :decimals="decimals" :duration="5" ref="countTo" >
		<!-- 指定 ref -->
      <template #left="{textFromChild}">
        <span :style="{color:textFromChild.color}">{{textFromChild.name}}:</span>
      </template>
      <span slot="right"></span>
    </count-to>
		<button @click="pasue">暫停</button>
  </div>
</template>
<script> import CountTo from "_c/count-to"; export default { name: "count_to", components: { CountTo }, data() { return { endValue: 4000, decimals: 2, }; }, methods: { pasue() { // 使用 refs 訪問組件,而後調用器方法 this.$refs.countTo.pause(); } } }; </script>
複製代碼

樣式

組件使用樣式,用三種方式:

  • 外部樣式;
  • 內部樣式;
  • 經過 props 傳入 類名,以指定使用內部樣式中的哪一個類名。

外部樣式兩種方法引入: 在 script 標籤中引入和在 style 標籤中引入。

<template>
  <div>
    <slot :textFromChild="text" name="left">獎金額度:</slot>
	<!-- 將 props 中的類綁定到 class 上 -->
    <span :id="eleId" ref="number" :class="countClass"></span>
    <slot name="right"></slot>
  </div>
</template>
<script> //引入樣式方法一: // import './count-to.css' import CountUp from "countup"; export default { name: "CountTo", inheritAttrs: true, //不讓父做用域的屬性掛載到組件的根元素上 props: { /** * @description 自定義樣式類名 */ className: { type: String, default: "" } } }; </script>
<style lang="css"> /* 引入樣式方法二 */ /* @import './count-to.css' */ /* 內部樣式 */ .count-to-number { color: red; font-size: 30px; } </style>
複製代碼

經過 props 傳遞類名,實際是在父級組件中指定使用內部樣式中的哪一個類。

經過 style 也能夠應用樣式到組件上。

總結

封裝一個組件 props 和 data 決定了組件的核心功能,插槽用於向組件傳遞 html 標籤,使得組件更加具備擴展性。經過事件咱們能夠對組件進行某些操做。改天分析一個第三方組件,好好體會一下這些概念。

參考

相關文章
相關標籤/搜索