通過UI庫深入瞭解Vue的插槽的使用技巧

金色海洋(jyk)發表於2022-01-17

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 在文字框的前面放置一個插槽,比如 http://
  • 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: 'https://naturefw.gitee.io/nf-rollup-ui-controller'
    },
    {
      name: '小李',
      age: 18,
      mobile: '1399999992',
      url: 'https://naturefw.gitee.io/nf-rollup-ui-controller/meta-base'
    },
    {
      name: '路飛',
      age: 18,
      mobile: '1399999993',
      url: 'https://naturefw.gitee.io/nf-rollup-ui-controller/meta-base'
    }
  ])

這樣就不用手擼 el-table-column 了,交給子元件即可,同時還可以滿足自定義列的需求。

是不是即簡潔又靈活。這個彩蛋還滿意吧。

看看效果:

 json渲染 + 作用域插槽

線上演示

https://naturefw.gitee.io/nf-rollup-ui-controller/test-slot

原始碼

https://gitee.com/naturefw/nf-rollup-ui-controller/tree/master/src/views/test/slot

相關文章