Vue 插槽 & 高复用组件

作者 : 开心源码 本文共6858个字,预计阅读时间需要18分钟 发布时间: 2022-05-12 共103人阅读

# 前言

? 组件是Vue插槽中最为关键的一个特性之一,而组件的API分三大板块

  1. Props: 允许外环境给组件传递数据,
  2. Events:允许组件触发外部环境的反作用 和
  3. 插槽:允许内外环境作用于交叉,将额外的内容组合到组件中去。

正确了解了这三个,相信你能写出很柔美的组件

? 本文主要形容

  • 对插槽的了解
  • 匿名插槽
  • 具名插槽
  • 作用域插槽
  • 编写高复用组件的几点思路
    ?

# 为什么用插槽

? 组件的最大特性就是复用性,而用好插槽能大大提高组件的可复用能力。

? 组件的复用性常见情形如“在有类似功能的板块中,他们具备相似的UI界面,通过使用组件间的通信机制传递数据,从而达到一套代码渲染不同数据的效果”。

? 然而这种利用组件间通信的机制只能满足在结构上相同,渲染数据不同的情形;假设两个类似的页面,他们只在某一板块有不同的UI效果,以上办法就做不到了。可能你会想,使用 v-ifv-else来特殊解决这两个功能板块,不就处理了?很优秀,处理了,但不完美。极端一点,假设我们有一百个这种页面,就需要写一百个v-ifv-else-ifv-else来解决?那组件看起来将不再简小精致,维护起来也不容易。

? 而 插槽 “SLOT”即可以完美处理这个问题
?

# 什么情况下使用插槽

? 顾名思义,插槽即往卡槽中插入一段功能块。还是举刚才的例子。假如有一百个基本类似,只有一个板块功能不同的页面,而我们只想写一个组件。可以将不同的那个板块单独解决成一个卡片,在需要使用的时候将对应的卡片插入到组件中就可实现对应的完整的功能页。而不是在组件中把所有的情形用if-else罗列出来

? 可能你会想,那我把一个组件分割成一片片的插槽,需要什么拼接什么,岂不是只需一个组件就能完成所有的功能?思路上没错,但是需要明白的是,卡片是在父组件上代替子组件实现的功能,使用插槽无疑是在给父组件页面添加规模,假如全都使用拼装的方式,和不用组件又有什么区别。因而,插槽并不是用的越多越好。

? 插槽是组件最大化利用的一种手段,而不是替代组件的策略,当然也不能替代组件。假如能在组件中实现的板块,或者者只要要使用一次v-else, 或者一次v-else-if,v-else就能处理的问题,都建议直接在组件中实现。
?

# 准备工作

? 使用插槽前,需要先理解什么是编译作用域, 即

父组件模板的内容在父组件的作用域内编译,子组件模板的内容在子组件的作用域内编译

? 什么意思?假设有如下案例

// 父组件<template>  <p>{{ greet }}</p>  <child-component :data="myData">    {{ messages }}  </child-component></template>
// 组件child-component<template>  <div>    <p>{{ myName }}</p>    <slot></slot>  </div></template>

? 在父组件作用域中参加编译的内容有:(1) 父组件P标签的greet。(2)【重点上变量 message; (3) 变量myData
? 在子组件中参加编译的内容有:(1)子组件 p 标签中的myName。(2) 【重点下子组件标签中的data属性

? 需要强调的是,【重点上】中的存在于父组件编译作用于上的message部分也就是插槽内容,是不能访问存在于子组件【重点下】中的data属性的,假如需要访问这部分内容,需要使用到作用域插槽功能

? 上面提到过一个观点:“卡片是在父组件上代替子组件实现的功能,使用插槽无疑是在给父组件页面添加规模”。从上面案例中也可以看出,子组件只提供了插槽<slot>,而具体什么内容它并不论,都交给了父组件作用于中存在于<child-component>包含的那部分内容去分发。这部分内容,就是我们所说的卡片
?

# 单个插槽 (匿名插槽)

? 在没有使用插槽前,我们在组件内部写入的内容都会被抛弃,起因很简单,在父组件渲染的时候,会使用子组件里的内容来替换它在父组件的占位。假如不想被丢弃,就需要在子组件中使用单个插槽来接收内容

? 单个插槽一般都是匿名的,当然也可以给他命名,命名的方式使用slot="slotName"

// 父组件中定义卡片<div>    <h1>父组件</h1>    <child-component>        <p>卡片内容1</p>        <p>卡片内容2</p>    </child-component></div>
// child-component组件中使用slot接收<div>    <h2>子组件</h2>    <slot>        插槽默认内容    </slot></div>

在案例中除了有卡片内容与插槽内容,我们还看到了在<slot>中定义的一段话,它是插槽标签的默认内容,会在子组件编译作用域内编译,只有当宿主元素为空,且没有相应的插入内容时才显示。上面的案例我们可以得到如下结果:

// 渲染结果:<div>    <h1>父组件</h1>    <div>        <h2>子组件</h2>        <p>卡片内容1</p>        <p>卡片内容2</p>    </div></div>

?

# 具名插槽

? 我们可以给插槽定义名字,使其成为具名插槽。在单个插槽中,会将父组件中所有的卡片(假设都没有命名)按其在父组件中定义的顺序都接收过来;

? 而具名插槽则是接收指定的卡片。这样,我们即可以在不同位置定义多个插槽,分别用来接收不同的卡片内容。也可以添加一个匿名插槽,用来接收父组件编译作用域中未被指定名称的卡片内容(剩余内容),从而达到卡片的最大化利用。

? 在父组件中,通过使用slot = "slotName"来给卡片内容命名,如下案例中,我们将内容分成了两个卡片,一个卡片名为header, 另一个为footer。需要注意的是,包含slot的标签元素也会被插入到卡槽中。如案例中的div

<div>  <child-component>    <div slot="header">      <h2>插槽标题</h2>    </div>    <div>没被命名的“剩余”内容一</div>    <div slot="footer">      <p>版权所有,翻版我也没办法</p>    </div>    <div>没有被命名的“剩余”内容二</div>  </child-component></div>

? 卡片我们设定好了,接下来设定接收的卡槽

// child-component 中的内容<div>  <slot name="header"></slot>  <div>    <p>这里是组件实现页面类似的功能板块的地方</p>  </div>  // 定义默认的卡槽用来存放“剩余”内容  <slot></slot>  <slot name="footer"></slot></div>

?

# 作用域插槽

? 作用域插槽(Scope slot)是Vue中很重要的一个特性,可以使组件更加的通用,复用性更高。但由于它存在父子作用域的交织关系,使得组件难以了解。

? 再次回到编译作作用域的内容,父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。存在于父组件编译作用域内的内容不能访问子组件作用域中的变量,反之亦然。但是我们知道,插槽的内容其实是为了实现子组件的定制化设计,因而往往有部分信息需要子组件中的数据来控制或者渲染,这时就需要知道子组件会传递少量什么信息过来。

? 既然需要根据子组件的信息来决定卡片的内容,我们需要将子组件中整个待定制板块的内容寄托给一个slot,让它在父组件卡片上实现,而后将这个slot 所需要的数据也传递出去。此时出现了一个待处理问题就是:需要将子组件编译作用域中的数据让其在父组件作用域中生效。Vue的作用域插槽就是来处理这个问题的。

v2.1.0 版本使用(且必需用) template 对卡片内容进行统一包装,并使用slot-scope(以前使用scope)属性来接收子组件传出的数据

为了更好的比照,回顾一下常规的<todo-list>如下

// 子组件中...<ul>  <li v-for="todo in todos" :key="todo.id">    {{ todo.text }}  </li></ul>

假如在子组件中我们如此设计,那么只能实现某一种列表形式的列表结构页面,假设我们页面每行需要多一个图标,就无法复用这个组件了,怎样办?我们可以抽出行元素到父组件中进行定制化设计,谁需要单独设计就抽出来,不需要的就是用默认结构

// 子组件中...<ul>  <li v-for="item in todos" :key="item.id">    // 将这部分需要定制化的内容外传,并把所需的数据以属性形式外传    // 这种属性外传的形式和 父组件给子组件传递数据的思路非常类似,因而父组件接收处也常被命名为props,当然命名可以随便取。    <slot :todo="item">这里是默认内容<slot>  </li></ul>
// 父组件<div>  <todo-list :todos="todos">    // 定义一个template来包裹卡片内容,并利用slot-scope属性来接收子组件的数据    <template slot-scope="props" >      <span v-if="props.todo.isComplete">?</span>      {{ props.todo.text }}    </template>  </todo-list></div>

在父组件中的slot-scope中定义的变量是父组件编译作用域中的临时变量,用来存放从子组件中传递过来的props对象。该对象中,定义在子组件上的属性作为props的键,如<slot :todo="item"><slot>中的todo; 子组件中属性对应的值作为props的值,如<slot :todo="item"><slot>中的item。当然这里的item是一个对象。

在 2.5.0+,slot-scope 不再限制在 <template> 元素上使用,而可以用在插槽内的任何元素或者组件上。

在 2.5.0+ 版本里,上面的例子可以这么写

// 父组件<div>  <todo-list :todos="todos">    // 不需要再使用template来包裹卡片内容,直接将slot-scope定义在span上(这里稍作改动假设文本在span中)    <span slot-scope="props">{{ props.todo.text }}</span>  </todo-list></div>

?

# 利用结构赋值简化代码

? 利用es6的解构赋值特性,可以使得代码结构更加清晰易懂,作用域插槽也变得更干净少量

<todo-list v-bind:todos="todos">  <template slot-scope="{ todo }">    <span v-if="todo.isComplete">?</span>    {{ todo.text }}  </template></todo-list>

?

# 如何编写一个高复用的组件

? Vue 作为一套构建客户的渐进式框架,倡导使用简单的API来实现响应式的数据来绑定和组合视图组件。然而由于vue的语法自由,方案众多,不同人处理问题的思路不一样,写出来的代码自然有差别,假如是多人开发,就容易造成规范不统一,自成一套的问题。

? 对于业务量较小的系统,组件的可复用性和规模编写影响并不大,但随着业务代码日益庞大,组件必将会越来越多,组件逻辑的耦合性也更加严重,容易出现维护困难,牵一发而动全身的困恼。笔者查阅了相关资料书籍结合自身的了解,得出如下几个要点。

0. 说明 – 组件职责

? 组件根据其用处可粗略分为两类:一类是通用组件(可复用组件)即本章重点,一类是业务组件(几乎为一次性组件)。Vue提倡将页面划分成不同的板块,将每一个板块封装成一个组件。这种思路决定了不可能所有的组件都是通用组件,必然存在少量一次性的业务组件,封装它们的目的是为了提高代码的可读性和易维护性。

? 虽说有这两类,但并没有一条特别清晰的分界线,起因是Vue组件的编写极具艺术性,通过Vue语法的巧妙利用,典型代表就是「作用域插槽」,理想情况下能将业务组件拆分成一个插槽的卡片内容,但这也存在难度。这也是为什么称Vue是渐进式框架的起因

可复用组件实现通用的功能(无关使用位置,使用场景的变化)

  • UI 效果展现
  • 与客户的交互 (如点击事件)
  • CSS特效如动画效果

业务组件则实现偏向业务话的功能

  • 获取数据
  • 和vuex相关的操作(不应该在通用组件中出现)
  • 埋点功能
  • 引用可复用组件
1. 业务无关

? 组件的命名应该和业务无关,而是根据功能命名。

? 假设有一个团队列表,需要把每一项作为一个组件,你可能会想使用Team。这时,有另一个需求要求展现为每一个人员赠送的节日礼物列表,再使用这个Team组件显然感觉不合适。

? 关于如何智慧的命名,给一个建议: 可以借用ElementUI等这类UI框架的规范,他们实质上也是对Vue组件的少量封装,可以学习他们的做法。 举个例子如 ItemListItemCell等命名

2. 数据无关

? 编写的组件应该尽可能的无状态,除非真实具备某些适普功能的特殊组件。应尽量不要在组件内部去获取业务数据,以及任何与服务器端打交道的操作,这将严重缩小组件的可用范围。

3.命名空间

?可复用组件除了定义一个清晰的公开接口,还需要有命名空间,避免与浏览器保留的标签和其余组件发生冲突。特别是当项目引用外部UI或者迁移到其余项目时,也能处理很多命名冲突问题。命名空间建议使用项目名称的缩写。

? 当然,业务组件也建议有命名空间

上下文无关

? 所谓上下文无关并不是说全无关,而是尽可能减少对外部环境的依赖。虽说Vue是拆分组件,拆分板块的思想,但并不是无意义拆分。并不希望把一个具备独立功能的组件按照他的板块拆散,这样不进添加了无意义的数据传输,还不利于上下文无关特性。

数据扁平化

? 传递数据时,不要将整个对象作为一个prop传递进来。很常见的一个现象就是

<child :data="resData"></child>

? 而后resData的结构为一个JS对象。这么做不是不行,而是有少量弊端。
(1)组件的接口不清晰,甚至需要写注释才能看明白这组数据如何解决。
(2)props 校验无法校验对象内部的属性类型
(3)当服务器端返回的对象中带有的key与组件接口不一致时,需要手动转换或者构建。
当然,这是一把双刃剑,当需要渲染的数据字段不多时,提倡使用扁平数据分格。如下

<child :title="resData.title" :describe="resData.describe" :author="resData.author"></child>
项目骨架

? 单组件不异过重,组件在功能独立的前提下应该尽量简单,越简单的组件可复用性越强。当你实现组件的代码,不包括CSS,有好几百行了(这个大小视业务而定),那么就要考虑拆分成更小的组件。

? 当组件足够简单时,即可以在一个更大的业务组件中去自由组合这些组件,实现我们的业务功能。因而,理想情况下,组件的引用层级,只有两级。业务组件引用通用组件。

? 而对于一个庞大的项目,必然会有更深层的组件嵌套,此时建议将业务层组件和通用组件分离

使用插槽将[业务组件]剥离成[通用组件]

? 插槽绝对是Vue中的利器。通过插槽我们不难将一个业务组件剥离出公用部分成为通用组件,通过slot再将所需要的业务内容插入对应插槽中。如下案例

// 组件two-col-layout<template>    <ul slot="content" v-if="Lists.length">      <li v-for="item in Lists" :key="item.id">        <div class="l">          <slot name="left" :item="item">图片区域</slot>        </div>        <div class="r">          <slot name="right" :item="item">介绍区域</slot>        </div>      </li>      <slot name="after"></slot>    </ul></template>

? 案例中展现的是一个两列布局的通用组件。其设置了左边栏为图片展现区域,右边栏为介绍展现区域。但是关于这两栏具体信息如何展现,那是业务组件需要干的事情。

  1. 案例中的组件与业务无关:他不关心页面需要些什么,介绍区域会放些什么东西,有几栏,而是将这些交给父组件实现。
  2. 与数据无关:他同样不关心数据是什么样的,有些什么字段,字段名是什么,他只关心数据类型能通过Props验证就可。毕竟这里需要做v-for循环。
  3. 与上下文无关:告诉该组件一个数据名称就可,它只做数据转交工作
  4. 结构扁平:他将业务信息交回给父组件完成,因而自己不需要做太多的子组件封装,也就避免了多层组件嵌套
  5. 命名规范:名称根据组件的功能命名,两列布局two-col-layout,很容易看懂。
    ?

# 结束语

? Vue为渐进式框架,上手简单并不代表这门技术就简单。经常复习官网和查阅相关书籍,会发现不同的东西。太多时候埋头于写业务代码,而忽略了对这门极具艺术的语言有较多的研究。多思考,虚心学,或者许你会觉得,越学,不会的越多~那就对了

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Vue 插槽 & 高复用组件

发表回复