Vue3.3 的新功能的體驗(下):泛型元件(Generic Component) 與 defineSlots

2023-05-26 15:00:31

上一篇說了 DefineOptions、defineModel、Props 的響應式解構和從外部匯入型別 這幾個新功能,但是沒有說Generic、defineSlots等,這是因為還沒有完全搞清楚可以用在什麼地方。折騰了幾天終於弄清楚了。

這還要從 TS 的泛型說起。

泛型的目的和意義

泛型僅僅只是表達傳啥都行嗎?當然不是,因為js原生就支援「泛型」,本來就啥都可以傳的。
泛型的目的是——約束!泛型相當於制定了一個白名單,名單裡面的型別可以傳,不在名單裡面的不可以傳。

TS 的泛型可以幫助我們更準確的推斷型別,從而在編寫程式碼的時候,可以有更準確的提示和提供驗證依據。

泛型元件(Generic Component)

元件的props可以設定各種型別,那麼如果想用泛型的話,要如何設定呢?這就需要使用 Generic:

<script setup lang="ts" generic="T extends {name: string} ">
   
  const props = defineProps<{
    list: T[], // 泛型的方式
    list2: number[], // 只能是 number 型別的陣列
    list3: Array<any>, // 任意型別的陣列
    name: string,
    person: {
      name: string
    }
  }>()

  console.log('props-ts:\n', props)

這裡定義了幾個屬性,第一個使用了泛型,第二個是 number[],第三個是任意型別的陣列。

我們來看看不同型別的提示資訊:

  • Array<any> 提示的時候,無法獲知具體的型別。

  • number[] 必須和設定的型別完全一致。

  • T[] 可以根據傳入的型別做出對應的提示

    • 傳入 {name: string}

    • 傳入 {name: string, age: number}

    • 型別不匹配的提示

對比一下,我們可以發現,使用泛型可以準確的推斷型別,在模板裡面可以有更準確的提示,如果型別不合格,可以有提示資訊。
這樣在編寫程式碼的時候可以避免低階錯誤。

defineSlots

defineSlots 是做什麼的呢,是定義插槽還是獲取插槽?準確的說,是定義作用域插槽props的型別(支援泛型),然後返回父元件傳入的插槽。

在 setup 裡面定義插槽的型別

在元件裡面定義兩個插槽,一個是匿名插槽,一個是作用域插槽(col),

定義一個 list 的屬性,傳入一個陣列,然後遍歷這個陣列,建立一組列表,列表內使用作用域插槽。

通過作用域插槽的props把陣列元素傳遞給父元件:(好像有點繞)

<script setup lang="ts" generic="T extends Object ">
  
  const props = defineProps<{
    list: T[], // 泛型的方式
  }>()

  const slot = defineSlots<{
    default(props: any): any,
    col(props:
      {
        row: T,
        index: number
      }): any
  }>()
  console.log('slot:\n', slot)
<template>
  <!--匿名插槽-->
  <slot></slot>
  <div v-for="(item, index) in list" :key="index">
    <!--作用域插槽-->
    <slot name="col" :row="item" :index="index" ></slot>
  </div>
</template>

父元件裡使用的方法

<script setup lang="ts">
  import { reactive } from 'vue'
  // 載入子元件
  import ts from './20-ts.vue'
  // 定義陣列
  const list2 = reactive([
    {
      name: '11',
      age: 10
    },
    {
      name: '66',
      age: 10
    }
  ])
</script>
<template>
  <ts :list="list2" > <!--傳入資料列表-->
    <h1>測試插槽</h1>
    <template #col="{ item, index }"> <!--用解構的方式獲取-->
      序號:{{ index }}<br> <!--其實這裡是迴圈-->
      內容:{{ item }}
    </template>
  </ts>
</template>

UI庫裡的 table 元件一般都會支援這樣的插槽,以便於靈活設定列表,比如 el-table 的 el-table-column:

(來自官網範例程式碼)

<el-table :data="tableData" style="width: 100%">
    <el-table-column label="日期" width="180">
      <template #default="scope">
        <div style="display: flex; align-items: center">
          <el-icon><timer /></el-icon>
          <span style="margin-left: 10px">{{ scope.row.date }}</span>
        </div>
      </template>
    </el-table-column>
    ...
  </el-table>

這裡的 default 就是一個匿名作用域插槽,可以通過scope.row獲得每一行的資料。

defineEmits

defineEmits 是定義事件的一種快捷表達方式,也是一種語法糖,這個和 defineModel 有重合的地方,那就是 v-model 的 update:modelValue 的部分。

話說,元件需要事件嗎?以前是事件驅動,現在是資料驅動,或者說是狀態驅動。以前監聽事件,現在只需要監聽狀態的變化即可,從dom脫離出來。

好吧,其實我基本已經不使用 emit 了,感覺似乎並不需要了。

參考資料

Generic component enhancements - Discussion #436:

unplugin-vue-define-options - npm: https://www.npmjs.com/package/unplugin-vue-define-options

Announcing Vue 3.3 | The Vue Point: https://blog.vuejs.org/posts/vue-3-3

Vue 3.3 主要新特性詳解 - 三咲智子 Kevin Deng: https://xlog.sxzz.moe/vue-3-3

5 Vue3.3 釋出:十分鐘速遞

6 官方幫助檔案

7 elementPlus