掌握插槽魔法,助你进阶 Vue 开发,赋予组件无限可能!

大家好,我是前端宝哥。

插槽,就像一个魔法口袋,可以让你轻松地把内容塞到组件里,不用再费劲地用 props 传递。

什么是插槽?

在 React 中,你可以用 children 属性把 JSX 代码塞进组件,但这样操作不太方便。

// 父组件
<blog-list>
  <p>我的文字在这里</p>
</blog-list>
function BlogList({children}) {
  return(
    <div>
      {children}
    </div>
  )
}

HTML 和其他 JavaScript 框架则用 <slot> 元素来创建占位符,你可以直接把内容塞进去,不用再用 props 传递。

而且,你还可以轻松地设置默认内容,不用额外写代码。

// FancyCard 组件
<div class=“fancy” />
  <slot name="text"><p>默认文字</p></slot>
</div>

// 父组件
<FancyCard />

// 默认结果
<div class=“fancy” />
  <p>默认文字</p>
</div>

你也可以直接把整个元素塞进去。

// FancyCard 组件
<div class=“fancy” />
  <slot name="text"><p>默认文字</p></slot>
</div>

// 父组件
<FancyCard>
  <ul slot="text">
    <li><p>我的文字</p></li>
    <li><p>我的文字</p></li>
  </ul>
</FancyCard>

// 结果
<div class=“fancy” />
  <ul>
    <li><p>我的文字</p></li>
    <li><p>我的文字</p></li>
  </ul>
</div>

没有名字的插槽叫做默认插槽。如果你不给内容添加 slot 属性,它就会被塞到默认插槽里。用默认插槽和命名插槽,你就可以把一些内容放到指定位置,把剩下的内容放到另一个位置。

// FancyCard 组件
<div class=“fancy” />
  <slot name="text"><p>默认文字</p></slot>
  <slot></slot>
</div>

// 父组件
<FancyCard>
  <p slot="text">这是其他文字</p>
  <p>这是更多文字</p>
  <button>点击我!</button>
</FancyCard>

// 结果
<div class=“fancy” />
  <p>这是其他文字</p>
  <p>这是更多文字</p>
  <button>点击我!</button>
</div>

如果 <slot> 没有默认内容,你也没塞任何东西进去,它就什么也不会显示。

// FancyCard 组件
<div class=“fancy” />
  <slot name="text"><p>默认文字</p></slot>
  <slot></slot>
</div>

// 父组件 A
<FancyCard />

// 结果 A
<div class=“fancy” />
  <p>默认文字</p>
</div>

Web Components 中的插槽

HTML 有 Web Components 规范,它允许开发者使用 HTML 创建组件。

创建一个使用 <slot> 的自定义元素需要三个部分。第一个是 HTML 模板。

<template id="preview">
  <style>
    @import "../styles.css";
  </style>
  <div class="blog-list-container">
    <h2><slot name="title">默认标题</slot></h2>
    <slot></slot>
  </div>
</template>

第二个部分是使用 自定义元素 创建 JavaScript 类。在自定义元素的构造函数中,我们需要创建一个 影子 DOM 来存放我们的 <slot>。影子 DOM API 在 DOM 中创建了一个作用域、封装的、独立的 DOM,这个 DOM 由你的 HTML 页面中的其他部分创建。为了创建一个影子 DOM 并将我们的 <slot> 放入其中,构造函数执行三个操作:

  • 使用 getElementById 和 .content 属性访问第一部分中 HTML 模板的内容,并将其分配给一个变量。

  • 在开放模式下实例化一个影子根节点,以便浏览器和页面中的其他部分可以访问其中的内容。

  • 克隆 HTML 模板的内容,并将其作为子节点附加到影子 DOM。

class BlogPreview extends HTMLElement {
  constructor() {
    super();
    const templateContent = document.getElementById("preview").content;
    this.attachShadow({mode: "open"});
    this.shadowRoot.appendChild(templateContent.cloneNode(true));
  }
}

customElements.define('preview-component', Preview);

使用 <slot> 的第三部分是在 HTML 文件中调用自定义元素。

<preview-component><span slot="title">我的标题</span><p>我的描述</p></preview-component>

结果将显示我的 <span>,其中包含我的标题文本,放在 <h2> 中。

<div class="blog-list-container">
  <h2><span>我的标题</span></h2>
  <p>我的描述</p>
</div>

当你检查这个自定义元素时,DevTools 将显示带有 <slot> 和你传递给组件的内容的影子根节点。

<preview-component>
  #shadow-root (open)
    <div class="blog-list-container">
      <h2><slot name="title">default</slot></h2>
      <slot></slot>
    </div>
  <span slot="title">我的标题</span>
  <p>我的描述</p>
</preview-component>

Chrome DevTools 和 FireFox DevTools 都有内置工具,可以帮助你通过点击来查看  内容的来源。

Vue 中的插槽

Vue 插槽功能更加强大,但你也能看到它们是如何从 Web Components 规范中汲取灵感的。

默认插槽

如果你把内容放到默认的 <slot> 中,你只需要把它包裹在你的组件中。

// blog-list 组件
<template>
  <div class="blog-list-container">
    <h2><slot>默认</slot></h2>
  </div>
</template>

// 父组件
<blog-list>
  <span>我的标题</span>
</blog-list>

// 结果
<div class="blog-list-container">
  <h2><span>我的标题</span></h2>
</div>

命名插槽

如果你使用命名插槽,你需要使用一个带有插槽名称的 <template>,以 # 开头。

// blog-list 组件
<template>
  <div class="blog-list-container">
    <h2><slot name="title">默认</slot></h2>
  </div>
</template>

// 父组件
<blog-list>
  <template #title>
    <span>我的标题</span>
  </template>
</blog-list>

// 结果
<div class="blog-list-container">
  <h2><span>我的标题</span></h2>
</div>

你也可以在默认插槽中使用这种语法。

// blog-list 组件
<template>
  <div class="blog-list-container">
    <h2><slot name="title">默认</slot></h2>
    <slot></slot>
  </div>
</template>

// 父组件
<blog-list>
  <template #title>
    <span>我的标题</span>
  </template>
  <template #default>
    <p>把我放进去,教练!</p>
  </template>
</blog-list>

// 结果
<div class="blog-list-container">
  <h2><span>我的标题</span></h2>
  <p>把我放进去,教练!</p>
</div>

作用域插槽

这里就变得有趣了。你可以从子组件传递数据到插槽。当你在父组件中使用子组件时,你就可以访问这些数据。

// counter 组件
<script setup>
  const count = ref(0);
  const counterMessage = count < 10 ? "继续计数!" : "干得好!"
</script>
<template>
  <div class="counter-container">
    <slot :number="count" :message="counterMessage"></slot>
  </div>
</template>

// 父组件
<counter v-slot="slotProps">
  <h2>我的计数器</h2>
  <p>{{slotProps.number}}</p>
  <p>{{slotProps.message}}</p>
</counter>

// 结果
<div class="counter-container">
  <h2>我的计数器</h2>
  <p>0</p>
  <p>继续计数!</p>
</div>

你可以使用解构,这样你就不必使用 slotProps.property,只需使用 property

// 父组件
<counter v-slot="{number, message}">
  <span>我的计数器</span>
  <p>{{number}}</p>
  <p>{{message}}</p>
</counter>

这个功能在命名插槽中使用了略微不同的语法。此外,如果你在传递插槽属性时混合使用默认插槽和命名插槽,则必须使用 <template #default>

// counter 组件
<template>
  <div class="counter-container">
    <h2><slot>默认</slot></h2>
    <slot name="display" :number="count" :message="counterMessage"></slot>
  </div>
</template>

// 父组件
<counter>
  <template #default>
    <span>我的计数器</span>
  </template>
  <template #display="{number, message}">
    <p>{{number}}</p>
    <p>{{message}}</p>
  </template>
</counter>

// 结果
<div class="counter-container">
  <h2><span>我的计数器</span></h2>
  <p>0</p>
  <p>继续计数!</p>
</div>

如果你曾经使用过 Array.prototype.map() 来创建 JSX 列表,那么你就可以理解 Vue 的设计思路。你可以创建一个可扩展的列表组件,并以你想要的方式显示它。

// 列表组件
<script setup>
  defineProps({
    listItems: Array as PropType<String[]>
  })
</script>

<template>
  <ul>
    <li v-for="listItem in listItems" :key="listItem.id">
      <slot name="item" :item="listItem"></slot>
    </li>
  </ul>
</template>

// 父组件
<script setup>
  const products = [
    {
      name: "袜子",
      description: "你的双脚的奢华享受",
      count: 12,
    },
    {
      name: "帽子",
      description: "戴在头上的",
      count: 8
    },
];
</script>
<template>
  <list-component :listItems="products">
    <template #item="{ item }">
      <div class="product">
        <h3>{{ item.name }}</h3>
        <p>{{ item.description }}</p>
        <span class="count">{{ item.count }}</span>
      </div>
    </template>
  </list-component>
</template>

// 结果
<ul>
  <li>
    <div class="product">
      <h3>袜子</h3>
      <p>你的双脚的奢华享受</p>
      <span class="count">12</span>
    </div>
  </li>
  <li>
    <div class="product">
      <h3>帽子</h3>
      <p>戴在头上的</p>
      <span class="count">8</span>
    </div>
  </li>
</ul>

你甚至可以更进一步,创建 高阶组件 或智能组件,它们不渲染元素,只包含逻辑。Vue 将它们称为 无渲染组件。

Angular 中的插槽

Angular 借鉴了 Web Components 规范,也拥有类似的功能。你可以使用 影子 DOM API 在 Angular 中实现 视图封装模式。

Angular 也使用插槽,但它将功能称为 内容投影。它不使用 <slot> 元素,而是使用 <ng-content> 元素。

创建组件时,可以使用两种方法创建模板。

在 HTML 文件中:

// blog-list.component.html
<ng-content select="[text]"></ng-content>

或者在 @component 装饰器中:

@Component({
  selector: 'blog-list'
  template: `
    <h2>标题</h2>

    默认:
    <ng-content></ng-content>

    文字:
    <ng-content select="[blog-text]"></ng-content>
  `
})

然后,在父组件中,你就可以使用你在想要传递给它的元素上定义的 <ng-content> 选择器。

// 父组件
<blog-list>
  <p blog-text>我的文字在这里</p>
</blog-list>

// 结果
<h2>标题</h2>
<p>我的文字在这里</p>

就像之前的例子一样,未命名的插槽将接收所有不使用定义的选择器传递的内容。

Angular 还提供 条件内容投影,使用 <ng-template>

总结

过去我在 React(以及 2013 年的 AngularJS)中工作过,我花了一些时间才开始习惯在编写 Vue 组件时使用 <slot> 而不是 props。 用 Web Components 规范手动创建一些 <slot> 并使用 Chrome DevTools 检查它们非常有帮助!


往期推荐

38个Vue、Nuxt 和 Vite 技巧、窍门和实践的合集

Vue 如何处理异步组件加载错误

Vue 3 将推出新特性,可以抛弃虚拟DOM了!

Vue 小技巧:何时使用可组合函数

别再用错 localStorage 了!小心踩坑!

怎么才能做出一个牛逼的Vue 组件库?

最后,如果你觉得宝哥的分享还算实在,就给我点个赞,关注一波。分享出去,也许你的转发能给别人带来一点启发。

关注我,明天见!

2596fa7ffa61d3442fc9b9d6421284a1.png

觉得好看,请关注我,点“在看”fd21855538e8e56b65553c74670b3959.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值