通過UI庫深入了解Vue的插槽的使用技巧
Vue官網對於插槽的介紹比較簡略,插槽本身也比較「燒腦」,很容易看暈,我就一直沒看懂,直到 使用了element-plus的組件的插槽。
其實我們可以換一個角度來理解插槽,就會豁然開朗了。
技術棧
- vite
- vue3
- element-plus
從父子組件的傳值開始
父子組件傳值可以通過 prosp + emit 來實現,雖然 props 可以傳遞各種類型,但是卻不能傳遞組件(包括HTML),這樣靈活度就差了一些。
那麼怎麼辦呢?為了提高靈活性,Vue 提供了插槽功能。
插槽可以分為:插槽、具名插槽、作用域插槽
如果不明所以的話,可以換一種名稱:匿名插槽、命名插槽、可傳參插槽。
匿名插槽
如何理解插槽呢?可以先看看div,div是一個容器,裡面可以放各種HTML標籤,同時也可以放各種組件。
那麼我們可以把div內部的標籤、組件視為插槽內容,同理,我們也可以把 select 內部的 option 也視為插槽內容。
我們可以用匿名插槽的方式,寫一個my-div的組件。
- 子組件 ./comp/my-div.vue
<div style="margin: 10px;padding: 10px; border:1px solid orange;">
匿名插槽:
插槽前<br><br>
<slot>沒有設置插槽</slot>
<br><br>
插槽後
</div>
子組件設置一個 slot 標籤,slot 可以理解為是一種「插值」,表示父組件的插槽在這個位置被渲染,然後在其前後可以加入子組件自己的內容。
slot 裡面是「備用內容」,如果父組件沒有設置插槽的話,「備用內容」會被渲染,否則會被忽略。
- 父組件
我們看一看在父組件裡面的使用情況:
import myDiv from './comp/my-div.vue'
匿名插槽<br>
設置文本框作為插槽內容:
<my-div>
<input type="text" placeholder="父組件的插槽">
</my-div>
<br>
沒有設置插槽內容:
<br>
<my-div></my-div>
- 看看效果
這樣就實現了一個簡單的具有插槽功能的組件,當然這個組件是為了插槽而插槽,並沒有沒有實際意義。
那麼插槽在實際項目里可以有哪些作用呢?我們可以參考一下UI庫的組件,他們有很多插槽的實際應用,比如 el-input、el-table等。
具名插槽。
「具名」是個啥意思?感覺用「命名插槽」更好理解一些。
- 如果一個組件只有一個插槽,那麼不用寫名稱,Vue會使用默認名稱:default 。
- 如果一個組件有多個插槽的話,那麼就需要起名來區分不同的插槽。
el-input 提供了prefix、suffix、prepend、append四個插槽,就是採用了命名插槽的方式。
我們來看一下官網的例子:
<el-input v-model="input1" placeholder="Please input">
<template #prepend>Http://</template>
</el-input>
<el-input v-model="input2" placeholder="Please input">
<template #append>.com</template>
</el-input>
- # 是 v-lot: 的簡寫形式,類似於 「v-bind:」 簡寫為 「:」,「v-on:」 簡寫為 「@」
- prepend 在文本框的前面放置一個插槽,比如 //
- append 在文本框的後面方式一個插槽,比如 .com
這樣可以方便輸入URL地址。其實如果 append 放置一個 el-autocomplete 的話,可以更靈活的設置域名後綴。
手寫一個命名插槽
還是手寫一個命名插槽,看一下子組件的實現方式。
- 子組件 ./comp/my-div-name.vue
<div style="margin: 10px;padding: 10px; border:1px solid rgba(61, 67, 155, 0.692);">
<slot name="header">我來組成頭部</slot>
插槽中間內容
<slot name="footer">我來組成結尾</slot>
</div>
實現具名插槽的方式很簡單,用 name 屬性設置插槽的名稱即可。
- 父組件的調用
import myDivName from './comp/my-div-name.vue'
<my-div-name>
<template v-slot:header>
<h1>這是頭部</h1>
</template>
<template #footer>
<p>這是結尾</p>
</template>
</my-div-name>
父組件需要用 template 限定具名插槽內容的範圍,我們來看看效果:
作用域插槽
插槽是父組件的,不是子組件的,父組件可以完全操作插槽里的組件。
但是子組件只能規定插槽的渲染位置,其他的就不能操作了,這樣的話還是有些不夠靈活,於是出現了作用域插槽。
作用域插槽的目的是解決父組件、子組件、插槽之間的數據通訊的問題。
還是看看UI庫組件 el-table 的插槽 。
父組件設置列表數據,傳遞給子組件,子組件渲染 table 表格。
為了更靈活,組件提供了自定義列的功能,採用的就是作用域的插槽。
看一下官網示例:
<el-table :data="tableData" style="width: 100%">
<el-table-column label="Date" width="180">
<template #default="scope">
<span style="margin-left: 10px">{{ scope.row.date }}</span>
</template>
</el-table-column>
</table>
- scope 就是子組件傳遞出來的數據集合,包含row、column、$index等屬性。
const tableData = reactive([
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
}
])
- tableData:父組件定義數據列表,通過 data 屬性傳遞給子組件。
這裡的 scope 的數據流程是這樣的:父組件 =》子組件 =》插槽。
為啥要繞一圈呢?雖然父組件可以直接給插槽設置值,但是由於 tr 是循環出來的,父組件無法獲知循環到哪一行了,所以需要子組件告知循環行數,這個資訊就是通過作用域插槽來實現的,我們可以做一個簡單的示例。
手擼一個簡單的作用域插槽
- 子組件 ./comp/my-table.vue
<div>
<table>
<tr>
<th>標題一</th>
<th>標題二</th>
<th>自定義</th>
</tr>
<tr v-for="(item, index) in data"
:key="index"
>
<td>{{item.t1}}</td>
<td>{{item.t2}}</td>
<td>
<slot name="td"
:row="item"
:$index="index"
></slot>
</td>
</tr>
</table>
</div>
第三列設置一個具名插槽,通過row、$index 傳遞數據。
const props = defineProps({
data: Array
})
設置一個屬性,接收列表數據。
- 父組件調用
import myTable from './comp/my-table.vue'
const data = reactive([
{ t1: '11', t2: '12', t3: '13' },
{ t1: '21', t2: '22', t3: '23' },
{ t1: '31', t2: '32', t3: '33' }
])
<my-table :data="data">
<template #td="scope">
自定義列:{{scope}}
</template>
</my-table>
可以看到數據的傳遞。
子組件的插槽,先起個名字,就叫做「td」好了,不要糾結名稱,俺有起名困難症。
然後用 row 屬性傳遞行的數據,用 $index 傳遞遍歷到第幾行的數據。
這樣一個簡單的作用域插槽就搞定了。當然只是一個示例,還是沒有啥實際意義。
那麼有實際意義的是什麼樣子的呢?還記得標題嗎?我可不是標題黨,彩蛋馬上就來。
片尾彩蛋
現在流行用 json 來渲染組件,還是用 el-table 舉例,我們可以定義一個 json,來描述表格列的情況,比如:
{
"itemMeta": [
{
"prop": "name",
"label": "姓名",
"width": 140,
"align": "center",
"header-align": "center"
},
{
"prop": "age",
"label": "年齡",
"width": 140,
"align": "center",
"header-align": "center"
},
{
"prop": "mobile",
"label": "電話",
"width": 140,
"align": "center",
"header-align": "center"
},
{
"prop": "url",
"label": "URL",
"width": 140,
"align": "center",
"header-align": "center"
}
]
}
然後遍歷 el-table-colmun 設置屬性,這樣就可以實現動態渲染 table 的功能。
這樣雖然很方便,但是自定義列呢?如果不支援插槽的話,那麼靈活性就差了一些。
魚和熊掌能不能兼得呢?既然都寫到這裡了,那麼肯定可以兼得。
做一個默認規則
自定義列的插槽名稱格式:td_{欄位名稱}。
也就是說 td_開頭的視為自定義列的插槽,加上前綴可以避免和 el-table 自帶的具名插槽衝突。
然後封裝一下 el-table
建立一個組件 ./comp/my-table-json.vue
import { useSlots } from 'vue'
const props = defineProps({
colInfo: Object,
data: Array
})
// 獲取插槽資訊
const slots = useSlots()
// 獲取列的描述資訊
const colInfo = props.colInfo
// 檢查插槽,設置名稱
colInfo.forEach(col => {
const _slotName = 'td_' + col.prop
if (typeof slots[_slotName] === 'function') {
// 有插槽
col.slotName = _slotName
} else {
// 沒有插槽
col.slotName = ''
}
})
定義屬性,接收數據和列的描述。
然後獲取插槽的資訊,設置列是否需要載入插槽。
<el-table :data="data" style="width: 100%">
<template
v-for="(item, index) in colInfo"
:key="index"
>
<!--不帶插槽的列-->
<el-table-column
v-if="item.slotName == ''"
v-bind="item"
>
</el-table-column>
<!--帶插槽的列-->
<el-table-column
v-else
v-bind="item"
>
<template #default="scope">
<slot :name="item.slotName" v-bind="scope"></slot>
</template>
</el-table-column>
</template>
</el-table>
遍歷列的描述資訊,判斷是否需要載入插槽,如果需要插槽的話,設置插槽並且傳遞 scope 數據。
父組件的調用
父組件就簡單多了。
UI庫的 table 的二次封裝
不用自定義列:
<my-table-json :data="data" :colInfo="colInfo">
</my-table-json>
使用自定義列:
<my-table-json :data="data" :colInfo="colInfo">
<template #td_url="{ row }">
<a :href="row.url" target="blank">{{row.name}}</a>
</template>
<template #td_mobile="scope">
手機:{{scope.row.mobile}}
</template>
</my-table-json>
不需要自定義列的話,程式碼可以更簡潔;
需要自定義列的話,也支援用插槽的方式實現。
import myTableJson from './comp/my-table-json.vue'
import meta from './grid.json'
const colInfo = reactive(meta.itemMeta)
const data = reactive([
{
name: '阿蒙',
age: 18,
mobile: '1399999991',
url: '//naturefw.gitee.io/nf-rollup-ui-controller'
},
{
name: '小李',
age: 18,
mobile: '1399999992',
url: '//naturefw.gitee.io/nf-rollup-ui-controller/meta-base'
},
{
name: '路飛',
age: 18,
mobile: '1399999993',
url: '//naturefw.gitee.io/nf-rollup-ui-controller/meta-base'
}
])
這樣就不用手擼 el-table-column 了,交給子組件即可,同時還可以滿足自定義列的需求。
是不是即簡潔又靈活。這個彩蛋還滿意吧。
看看效果:
在線演示
//naturefw.gitee.io/nf-rollup-ui-controller/test-slot
源碼
//gitee.com/naturefw/nf-rollup-ui-controller/tree/master/src/views/test/slot