Vue 插槽 & 高复用组件

2019-3-4更新】Vue 2.6+修改了部分语法,对插槽的使用有了较多的更新。在本文中笔者在相应位置给出了更新提示和新的推荐用法


# 前言

  组件是Vue插槽中最为关键的一个特性之一,而插槽是组件的一大亮点。本文主要描述

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

# 为什么用插槽

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

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

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

  而 插槽 “SLOT”就可以完美解决这个问题

# 什么情况下使用插槽

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

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

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

# 准备工作

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

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

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

// 父组件
<template>
  <p>{{ greet }}</p>
  <child-component :data="myData">
    {{ messages }}      // Vue 2.6+ 将该行定义为“后备内容”
  </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) 【子组件<child-component>中的data特性

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

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

# 单个插槽 (匿名插槽)

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

  单个插槽一般都是匿名的,当然也可以给他命名,默认未命名情况下,Vue2.6+版本默认为v-slot:default或简写#default

// 父组件中定义卡片
<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>

# 具名插槽 (Vue2.6+有更新)

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

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

Vue2.6+版本中,要求对所有的具名插槽的v-slot都添加在一个<template>上,除非当被提供的内容只有默认插槽时才能直接用在组件上。另外只有默认插槽时可以省略v-slot:default中的default。注意这两种情况都只适用于只有默认插槽的情况下,一般都不建议使用

  在父组件中,通过使用【Vue2.5用法:即将废弃slot = "slotName"Vue2.6+用法v-slot:name或简写#name来给卡片内容命名,如下案例中,我们将内容分成了两个卡片,一个卡片名为header, 另一个为footer。需要注意的是,包含slot的标签元素也会被插入到卡槽中。如案例中的div标签

<div>
  <child-component>
    <template v-slot:header>   // 这里使用插槽语法全称方式
      <div>
        <h2>插槽标题</h2>
      </div>

    <div>没被命名的“剩余”内容一</div>

    <template #footer>  // 这里使用插槽语法简写方式
      <div #footer>
        <p>版权所有,翻版我也没办法</p>
      </div>
    </template>

    <div>没有被命名的“剩余”内容二</div>
  </child-component>
</div>

  强烈建议将“剩余”内容写在一起,并使用<template>包裹起来,规范的话再加入#default。卡片我们设定好了,接下来设定接收的插槽

// child-component 中的内容
<div>
  <slot name="header"></slot>

  <div>
    <p>这里是组件实现页面相似的功能模块的地方</p>
  </div>

  // 定义默认的卡槽用来存放“剩余”内容
  <slot></slot>

  <slot name="footer"></slot>
</div>

# 作用域插槽

  让插槽内容能够访问子组件中才有的数据是很有用的,作用域插槽(Scope slot)就是这么一个特性,它可以使组件更加的通用,复用性更高。但因为它存在父子作用域的交织关系,使得组件难以理解。

v2.1.0 版本使用(且必须用) <template> 对卡片内容进行统一包装,并使用slot-scope(以前使用scope)属性来接收子组件传出的数据。v2.5.0做了修改,可以将slot-scope用在任意标签上,v2.6+之后,又做了一次更新,使用v-slot:slotName="slotProps"形式

为了更好的体现作用于插槽的强大,回顾一下常规的<todo-list>如下

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

如果在子组件中我们如此设计,将直接限制todo-list就这一种显示形式,假如需要一个ICON,就无法实现了。我们可以将每个 todo 作为父级组件的插槽,以此通过父级组件对其进行控制,然后将 todo 作为一个插槽 prop 进行绑定

<!-- 子组件中  -->
<ul>
  <li v-for="todo in filteredTodos" :key="todo.id">
    <!--
      这种属性外传的形式和 父组件给子组件传递数据的思路非常相似,
      因此父组件接收处也常被命名为slotProps,当然命名可以随意取。
    -->
    <slot :todo="todo">
      这里是后备内容
    <slot>
  </li>
</ul>
<!--父组件中  -->
<div>
  <todo-list :todos="todos">
    <!--
    我们为每个 todo 准备了一个插槽,
    将 `todo` 对象作为一个插槽的 prop 传入。
    -->
    <!-- Vue2.5及之前用法: <template slot-scope="slotProps" > -->
    <template v-slot:default="slotProps" >   // Vue 2.6+ 指定了来源于哪个插槽内容
      {{ slotProps.todo.text }}
    </template>
  </todo-list>
</div>

在 2.5.0,slot-scope 不限制在 <template> 上使用而可以在任意元素上使用;而Vue 2.6+引入了插槽名,不再建议用在元素上。如果只有默认插槽,可以简写v-slot:default成为v-slot,或者成为#default。但如果存在任意别的具名插槽,则不再用第一种简写。

在Vue 2.6+中,如果只有默认插槽,可以这么写

<!-- 父组件 -->
<div>
  <todo-list v-slot="slotProp">  
    <span>{{ slotProp.todo.text }}</span>
  </todo-list>
</div>

  如果存在别的插槽,则不能混用,注意,这里也可以使用v-slot的简写

<div>
  <todo-list> 
    <template #default="slotProps">
      <span>{{ slotProp.todo.text }}</span>
    </template>
    <template #other="otherSlotProps">
      <span>{{ otherSlotProps.todo.text }}</span>
    </template>
  </todo-list>
</div>

# 解构插槽prop

  作用域插槽利用v-bind将想属性绑定到插槽的特性中,这些特性会被处在父组件中绑定给slotProp(假设为该名字)收集,其内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里function(slotProp){ },这意味着v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式,也就是说我们可以使用ES6的解构赋值。

<todo-list :todo="todo">
  <template v-slot="{ todo }">  // 如果简写掉了:default,则不能简写#,即v-slot 和 #default 二选一
    <span v-if="todo.isComplete">✓</span>
    {{ todo.text }}
  </template>
</todo-list>

# 插槽变量

  动态指令参数也可以用在v-slot上,来定义动态的插槽名

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 组件(Component)是Vue.js最核心的功能,也是整个架构设计最精彩的地方,当然也是最难掌握的。...
    六个周阅读 5,564评论 0 32
  • 什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装...
    youins阅读 9,434评论 0 13
  • 本文章是我最近在公司的一场内部分享的内容。我有个习惯就是每次分享都会先将要分享的内容写成文章。所以这个文集也是用来...
    Awey阅读 9,406评论 4 67
  • 此文基于官方文档,里面部分例子有改动,加上了一些自己的理解 什么是组件? 组件(Component)是 Vue.j...
    陆志均阅读 3,794评论 5 14
  • Vue是一款高度封装的、开箱即用的、一栈式的前端框架,既可以结合webpack进行编译式前端开发,也适用基于gul...
    Hebborn_hb阅读 1,082评论 0 31