[重构倒数第02天] - Slots 与 Render Functions 的进阶心法

前言

该系列是为了让看过Vue官方文件或学过Vue但是却不知道怎麽下手去重构现在有的网站而去规画的系列文章,在这边整理了许多我自己使用Vue重构很多网站的经验分享给读者们。

使用过Vue的朋友都一定都有听过 Slots 这个功能,但是有通过不一定有使用过,就让我来稍微介绍一下我常在专案里面使用Slots 的时机与案例。

我们先来看一下以下的例子

https://ithelp.ithome.com.tw/upload/images/20210929/20125854Vya4pVTxiR.jpg

我们在制作网站的时候很常会有像是这样的 title,我们会看到除了内容不一样以外,旁边的 icon 跟 style 都是一样的,所以理所当然地会把这个 title给拆出组件来重复使用,这边我来示范一个我最常看到的做法。

首先我会新增一个 TitleBar.vue 的组件

<script>
export default {
  props: {
    content: {
      type: String,
      default: "",
    },
  },
  setup(props) {
    return {
      props,
    };
  },
};
</script>
<template>
  <h1>
    <img class="icon" src="../assets/icon.png" alt="" />
    {{ props.content }}
  </h1>
</template>

然後这个组件会透过 props 来传递 title 的文字内容,然後我只要在上层去传入 props,title 的内容就会不一样,像下面这样子。

<TitleBar :content="'最新消息'" />
<TitleBar :content="'关於我们'" />
<TitleBar :content="'热门商品'" />
<TitleBar :content="'你也感兴趣的'" />

codesandbox 范例: https://codesandbox.io/s/vue-slot-title-1-7roh7

这样的做法虽然可行,但是却不直觉,我们可以搭配 Slots 把这个组件当作是一个 html tag 一样使用,变成是下面这样

<TitleBar>最新消息</TitleBar>
<TitleBar>关於我们</TitleBar>
<TitleBar>热门商品</TitleBar>
<TitleBar>你也感兴趣的</TitleBar>

要怎麽做才能这样? Slots到底是什麽?

Slots 顾名思义就是「插槽」

我们来看一下官网的这张图

https://ithelp.ithome.com.tw/upload/images/20210929/20125854ZGZOOP7gMW.jpg

这张图其实就已经说明完了 Slots 的整个概念「渲染作用域」。

我们可以在我们的 component 里面去定义一个 <slot></slot> 的作用域,只要上层有放入内容,就会把这个内容给 Render 到你 <slot></slot> 的区域内,我们来看一下改过後的范例,我们删除掉 TitleBar.vue 里面所有的 props,然後在原本的内容位置塞入 <slot></slot>

TitleBar.vue

<template>
  <h1>
    <img class="icon" src="../assets/logo.png" alt="" />
    <slot></slot>
  </h1>
</template>

App.vue

<TitleBar>最新消息</TitleBar>
<TitleBar>关於我们</TitleBar>
<TitleBar>热门商品</TitleBar>
<TitleBar>你也感兴趣的</TitleBar>

我使用的时候就可以像是 html tag 一样使用,这样在看code的时候会比较明确,也减少不必要的 props,你也可以对 slot 插入预设的内容,当今天如果你没有在上层插入你的内文,就会直接Render你的预设的内容。

<template>
  <h1>
    <img class="icon" src="../assets/logo.png" alt="" />
    <slot>这是预设的内容喔</slot>
  </h1>
</template>
<!-- 不带内容进去 -->
<TitleBar></TitleBar>

https://ithelp.ithome.com.tw/upload/images/20210929/20125854SJLJgSdZPF.jpg

codesandbox 范例:https://codesandbox.io/s/vue-slot-title-2-jywyl

关於 Slots 官方还有很多的使用方式,例如下面我列出来的几个,但是这些给你们自己慢慢看就好了,我不想把文件内容整个复制贴上来一次。

  1. 具名插槽
  2. 作用域插槽
  3. 独占默认插槽的缩写语法
  4. 解构插槽 Prop
  5. 动态插槽名
  6. 具名插槽的缩写

Slots 官方文件 : https://v3.vuejs.org/guide/component-slots.html

之前也有针对 slots 开一场直播,有兴趣的朋友可以看一下,虽然是Vue2的

如果 Slots 组件的 html tag 不符合 html 语意规范怎麽办 ?

以刚刚我们举的例子,我的 TitleBar.vue 是用h1来放我的文字内容,然後旁边有一个 icon,这是现在的设计,但是如果我今天有一个 ulli 的结构,然後这个 li 也要跟我现在这个 TitleBar.vue 的设计一样,会改变内文,但是 icon 不变,或是甚至是可以选择这个 icon 图片能不能客制,那这样的需求我能不能直接拿 TitleBar.vue 来用呢?

<ul>
    <TitleBar>最新消息</TitleBar>
    <TitleBar>关於我们</TitleBar>
    <TitleBar>热门商品</TitleBar>
    <TitleBar>你也感兴趣的</TitleBar>
    <TitleBar></TitleBar>
</ul>

你这样使用画面的呈现当然可以,但是你的 HTML 规范就完全的不行了。

https://ithelp.ithome.com.tw/upload/images/20210929/20125854HfSEZsvEBR.jpg

天啊! 简直一团糟 !

於是我灵光一闪,如果我可以跟 vue-router 中的 router-link一样可以自己决定我这个组件的 html tag的话,也就是支援动态改变html tag,那该有多好~ 想用在那个地方就用在哪个地方,把这个组件能客制化的地方最大化 !!!

使用 Render Functions 吧 !

那我们现在来实际来改一下,首先如果要动态的改变我们的 html tag的话,不能用原本的 template 的方式来写 html,我们需要透过 Render Functions的方式来 Render 我们的DOM元素,我先砍掉原本的 template ,然後加上了 props,然後要用一个我们平常比较少用到的函式 render()来渲染我们的 DOM。

<script>
import { h } from "vue";
import icon from "../assets/icon.png";
export default {
  props: {
    tag: {
      type: String,
      default: "h1",
    },
  },
  render() {
    return h(this.tag, {}. [
      h("img", {
        class: "icon",
        src: icon,
      }),
      this.$slots.default(),
    ]);
  },
};
</script>

https://ithelp.ithome.com.tw/upload/images/20210929/20125854f8fWajCWXE.jpg

这个时候你只要在使用 <TitleBar>的时候带入一个名叫 tag 的 props,它就会去替换它里面的 html tag,进而达到动态改变的效果。

<ul>
    <TitleBar :tag="'li'">最新消息</TitleBar>
    <TitleBar :tag="'li'">关於我们</TitleBar>
    <TitleBar :tag="'li'">热门商品</TitleBar>
    <TitleBar :tag="'li'">你也感兴趣的</TitleBar>
</ul>

https://ithelp.ithome.com.tw/upload/images/20210929/20125854RMwq4XptZV.jpg

你可能突然头会很痛,看到又是 h 又是 render,想说 WTF...

什麽是 h() ???

h() 函式是一个用於创造 VNode( virtual node 虚拟节点 ) 的方法,但由於太频繁的使用且基於语法简洁的考量,它被称为 h()

h()可以带入三个参数:

  1. tag name - { String } (必填) : html 的标签名称。
  2. attribute - { Object } (非必填) : html 身上的属性,像是 src、class、alt 等等。
  3. VNodes - { String | Array | Object } (非必填) : 所以要放入这个 DOM 内部的 vNode 及 Slots 的内容。

我们要可以在 render 函式内取得 slots 的内容,可以用下面的方式来取得。

this.$slots.default()

所以刚刚范例的那段 code 我们再回来看一次

render() {
    return h(this.tag, {}. [
        h("img", {
            class: "icon",
            src: icon,
        }),
        this.$slots.default(),
    ]);
},
  1. 首先我的 h 函式放入了我从 props 传入的 tag 名称 this.tag,如果没有传入,那就是预设的 h1 tag。
  2. 因为这个 tag 并没有要放入其他 attribute,所以给一个空物件。
  3. 因为我的这个 DOM 里面还有一个 img 以及 slots 的内容,所以我这边用 Array 来带入。
  4. 然後在 Array[0]h()来创建一个 img 的 virtual DOM,这次因为要塞入 srcclass 这两个 attribute,所以我第二个参数的 Object 里面就有带入这两个 attribute 要塞入的内容。
  5. Array[1] 塞入接下来的 slots 的内容。

But...

现在这样看起来没啥问题,但是如果你上层不带入任何内容的话就会报错,所以我们现在要来处理预设内容的部分

<!-- 现在不带内容进去会出错 -->
<TitleBar :tag="'li'"></TitleBar>

我们可以先在 render 函式内去做判断

render() {
    const slotsContext = Object.keys(this.$slots).length === 0 ? "这是预设内容" : this.$slots.default();
    return h(this.tag, {}, [
        h("img", {
            class: "icon",
            src: icon,
        }),
        slotsContext,
    ]);
},

如果今天没有带入内容的话 this.$slots就会是一个空的 Object,所以我们去检查它是不是空的,如果是空的就给它一个预设的内容。

以下就完成了可以动态去改变 html tag 的 component ,阿如果要可以改变 icon 的话自己在写一个 props 去带入 src 的部分即可,我就不特别示范了。

<script>
import { h } from "vue";
import icon from "../assets/logo.png";
export default {
  props: {
    tag: {
      type: String,
      default: "h1",
    },
  },
  render() {
    const slotsContext =
      Object.keys(this.$slots).length === 0
        ? "这是预设内容"
        : this.$slots.default();

    return h(this.tag, {}, [
      h("img", {
        class: "icon",
        src: icon,
      }),
      slotsContext,
    ]);
  },
};
</script>

关於Vue DOM的文件 : https://v3.cn.vuejs.org/api/options-dom.html#template
Render Functions 的文件 : https://v3.vuejs.org/guide/render-function.html#render-functions

codesandbox 范例 : https://codesandbox.io/s/vue-slot-title-3-e3fl1

最後

Slots 在很多地方其实都非常的好用,除了帮我减少不必要的 props 以外,还可以帮我在需多重复性功能太多的 component 上面做一个整合,今天示范的不管是Slots 或是 Render Functions 的东西都只有其中一部分,还不是全部,它其实还有很多东西我没有讲到,不过我们先把这些基本的使用方式熟悉的之後,再去慢慢往後延伸了解也不迟,好啦~那我们明天见。

QRcode

那如果对於Vue3不够熟的话呢?

Ps. 购买的时候请登入或注册该平台的会员,然後再使用下面连结进入网站点击「立即购课」,这样才可以让我获得更多的课程分润,还可以帮助我完成更多丰富的内容给各位。

我有开设了一堂专门针对Vue3从零开始教学的课程,如果你觉得不错的话,可以购买我课程来学习
https://hiskio.com/bundles/9WwPNYRpz?s=tc

那如果对於JS基础不熟的朋友,我也有开设JS的入门课程,可以参考这个课程
https://hiskio.com/bundles/b9Rovqy7z?s=tc

订阅Mike的频道享受精彩的教学与分享

Mike 的 Youtube 频道
Mike的medium
MIke 的官方 line 帐号,好友搜寻 @mike_cheng\


<<:  Day14 - 解析看板文章及显示

>>:  [Day29]What is the Probability?

[Day27]Flutter Netflix UI 使用json_serializable转换Model

大家好,今天要来做Model的转换,使用到json_serializable、build_runne...

(ISC)² 道德准则

(ISC)² 道德准则仅适用於 (ISC)² 会员。垃圾邮件发送者的身份不明或匿名,这些垃圾邮件发送...

Day 19. UI 设计软件- Figma 简介与优势

前二篇解释了 GUI Design 阶段的重点,也提到此时花费设计师的工时相当可观。功欲善其事,必先...

[D25] placeholder

写在前面 test for placeholder test for placeholder tes...

Day4:梯度下降法(Gradient descent)

  梯度下降法经常被使用为优化学习的一种方式,寻找局部最佳解(至於为何是局部,之後会提到),想像有个...