(译)函数式组件在Vue.js中的运用

你是否曾经遇到过这样一个场景,你有个需求须要引入一个第三方库,然而你只须要使用这个库里面某一个功能,若是这个库不支持分模块导出的话,就会由于引入整个库而致使项目体积变大,进而影响项目加载性能。

再好比,下拉列表、时间选择器或者自动填充属性等自定义控件都是很是复杂的,须要考虑不少边缘的复杂状况。虽然有不少库很好的解决了这种复杂性,可是他们也带来了很差的缺点,就是这类组件没法自定义样式。css

就拿下面的标签输入控件举例:html

这个组件拥有一些有趣的功能:vue

  • 不容许你添加剧复的标签
  • 不容许添加空标签
  • 自动去除标签内容两边的空格
  • 点击Enter键保存标签
  • 点击x字符删除标签

若是你的项目中须要使用这样一个组件,把这个做为一个库引入,而且剥离这些逻辑确定可以节省不少时间和精力。react

可是假如这个时候你须要一个不一样样式展示呢?git

下面这个组件拥有和上面组件同样的行为功能,可是布局明显不同:github

https://codepen.io/adamwathan/pen/KomKNK

经过组合css和配置选项,你能够尝试在一个组件里面都支持这些布局,但显然这不是很好的方法,万一有一天你又须要另一个布局,你又得去改这个组件,破坏了组件的封闭性,容易引发其它问题。ajax

针对以上状况,咱们来介绍本文最重要的一个知识点。json

做用域插槽(Scoped Slots)

在Vue.js中,slots是组件中的一个占位符元素,会被从父组件/消费者中传过来的内容替换。 数组

Scoped slots就像常规插槽同样,可是它可以将参数从子组件传递到父组件/消费者。

常规slots就像是给组件传递了一段html文本,scoped slots就像是给组件传递了一个可以接收数据并返回Html的回调函数。less

经过向子组件里面slot元素增长props,将参数传递给父组件。父组件经过解构destructuring slot-scope里面接收的属性数据来得到这些参数。

这里有一个为每个list元素暴露scoped slot属性的LinksList组件,而且经过:link prop将每一项的数据传递回给父元素。

经过将 :link prop 添加到LinksList组件中的slot元素,父元素组件如今可以经过 slot-scope访问的到这些数据而且在本身的slot模块里面使用它。

插槽属性的类型

你能够传递任何类型给slot,可是我发现使用如下3个类型的数据之一是最有用的。

数据(Data)

最简单的slot prop类型就是数据类型:strings,numbers,boolean values,arrays,objects等。

在咱们的links-list组件例子中,link就是一个data prop类型的例子,它是一个拥有一些属性的对象。

父组件可以渲染这些数据或者本身决定该如何去渲染它

动做(Actions)

动做属性是由子组件提供的一个函数,父组件能够经过调用这个函数来触发子组件里面某些行为。

举个例子,咱们能够给父组件传递一个bookmark方法,这个方法用来为给定连接添加书签。

当用户点击一个未添加至书签的连接旁边的按钮时,父组件可以调用这个操做。

绑定(Bindings)

Bindings是一系列属性或者监听事件的集合,经过使用v-bind或者v-on,绑定到特定的元素中。

当你想要封装有关如何与给定的元素进行交互的细节时,这些很是有用。

举个例子,咱们提供了bookmarkButtonAttrs绑定和bookmarkButtonEvents绑定用来把这些细节移动到组件自身,而不是让消费组件本身经过v-show指令和@click处理添加至书签的按钮逻辑。

代码如上,如今若是消费组件喜欢,它们可用运用这些绑定到bookmark按钮上而且不用关心它们内部的实现。

Renderless Components

名称解释:Renderless Components,直译为非渲染组件,我更喜欢叫函数式组件(借鉴于react中的叫法,如下统称函数式组件)。

函数式组件是一个不渲染任何html文本的组件。

相反,它只管理状态和行为,给父组件或者消费组件暴露一个做用域插槽,以便它们能本身控制该渲染的内容。

函数式组件可以准确的渲染你给它传入的内容,无需任何其它元素。

那为何这样有用呢?

分离层现和行为

由于函数式组件只处理状态和行为,它们不会作出任何有关设计和布局的决定。

那就意味着若是你能找出一种方式将像咱们的标签输入功能这样有趣的行为从ui组件里面剥离出来,你就可以复用这个函数式组件去实现任何标签输入组件的布局。

下面都是标签输入组件,但此次是由一个函数式组件支持。

那它是怎么支持的呢?

函数式组件的基本结构

函数式组件仅仅暴露一个scoped slot,消费者能够在其中提供整个他们想要渲染的模块。

一个基本的函数式组件的骨架像下面这样:

它没有template标签或者不渲染任何html文本,相反,它经过使用一个 render函数去调用可以访问全部的slot props的默认的做用域插槽,而后返回结果。

任何父组件/消费者都可以在本身的模板中,经过解构slot-scope中的exampleProp去使用。

一个实际的使用案列

让咱们从头开始构建一个标签输入控件的函数式版本。

咱们首先要创建一个无插槽的空白的无渲染组件,

以及一个静态的,没有任何交互的父组件,而后将其传递到子组件的插槽中,

一步一步的,咱们将会为函数式组件增长状态和行为,同时经过 slot-scope暴露给咱们布局的地方来完善这个组件。

标签列表

首先,咱们将静态列表替换为动态列表。

这个标签输入组件是自定义表单控件,和这个原始例子同样,这个tags应该在父组件中,而且经过v-model绑定到组件中。

咱们首先给函数式组件增长一个value属性,并将其传递给一个名为tags的插槽。

接下来,在父组件中咱们将会增长 v-model指令,从 slot-scope中获取到tags,而后使 用v-for指令来遍历它们。

这个tags slot属性就是一个很好的数据属性的例子

删除标签

下一步,当点击X按钮,删除一个标签。

在函数式组件中,咱们将会增长一个removeTag的方法,而且将其做为一个slot属性传递给父元素。

而后咱们在父组件的按钮元素中增长一个 @click事件,这个事件可以在当前的标签中调用 removeTag方法。

这个removeTag slot属性就是一个动做属性的例子

点击回车键添加标签

添加新标签比前面两个例子都要复杂些。

为了理解为何,咱们先来看一下传统的组件都是怎么实现的。

咱们在newTag属性中保持跟踪这个新标签(在被添加以前),而后咱们经过 v-model将这个属性绑定到input中。

一旦用户点击enter键,只要这个标签是合法的,咱们就把它添加到list数组中,而后清除input输入的值。

这儿的问题就是咱们怎样经过scoped-slot传递v-model绑定。

好吧,若是你深刻了解过Vue,你应该知道v-model其实就是一个语法糖,它负责将value特性绑定到一个名叫value的prop上,同时在其input事件被触发时,将新的值经过自定义的input事件抛出。

这就意味着咱们能够在函数式组件中作一些更改来处理这个添加的行为:

  • 组件中添加一个newTag数据属性
  • 回传一个绑定到:valuenewTag的绑定属性
  • 回传一个绑定@keydown.enter用来添加标签和绑定@input用来更新标签的事件绑定属性

如今咱们只须要在父组件的input元素中绑定这些属性便可;

明确添加新标签

在咱们的当前布局中,用户经过在输入元素中输入以及敲击enter键来完成添加一个新标签的操做。但这也很容易想到,有些用户但愿可以提供点击添加按钮来添加标签。

要实现这个很简单,咱们只须要给slot scope传递一个addTag的方法的引用。

当设计像这样的函数式组件的时候,最好是多提供一些slot props,总比少要好。

消费者只须要解构出它们实际须要的属性便可,因此若是你提供了它们可能用不到的属性,它们也没有什么成本。

运行Demo

这就是到目前为止咱们创建的函数式组件:

这个实际组件不包含任何html文本,而且咱们定义模板的父组件不包含任何行为,是否是接近完美?

换个布局

如今咱们已经有了一个标签输入控件的函数式组件,咱们能够很容易的编写咱们想要的任何Html并将提供的插槽属性应用到正确的位置来轻松实现替代布局。

如下就是咱们利用咱们新的函数式组件从头开始实现的堆叠式布局。

建立本身的包裹式组件

看到这么多的例子,你可能会想:“哇,当我须要另外一种形式的标签组件时,每次我都须要写这么多html”,是的,你说的对。

不管何时你须要一个标签输入组件,你确实须要写不少。

而不是这个,咱们一开始常规的写法那样:

这有一个容易的解决方法:建立一个本身的包裹式组件!

这就是根据函数式组件编写原始 <tags-input>组件的样子

如今你可以只须要一行代码就可以在任何你想要的地方使用这个组件。

更加疯狂的是

一旦你意识到一个组件能够不用渲染任何内容而只负责提供数据,那么经过组件建模的行为就没了限制。

举个列子,这里有一个使用URL做为属性,从这个URL获取json数据并给父组件传递响应数据的fetch-data组件:

这是发送ajax请求最好的方法么?可能不是,但它真的颇有趣!

结论

将一个组件拆分红一个视图组件和一个函数式组件是很是有用的一种模式,可使代码复用更容易,但并非每次都值得这样作。

若是有如下这类状况,能够考虑使用这种模式:

  • 你打算构建一个库,而且但愿用户能够自定义组件的外观
  • 在你的项目中有不少功能类似但布局不同的组件

若是你正在研究一个在任何状况看来都类似的组件,那就不要走这条路了。这种情形下将全部你须要的写在一个组件里面可能会更好更简单。


此外,视图代码和业务逻辑分离只是一种下降代码耦合,进而增长代码健壮性的一种手段,其深层次的就是组件应该符合高内聚、低耦合的思想,其它符合这种思想的手段还有像控制反转(IOC)、发布订阅模式等等,我以为代码越日后写越应该培养这种意识,不然简简单单的写写业务代码,以完成需求而写代码,提高进步会比较慢。

文章翻译没有彻底按照原文来翻译,为了可以更好理解我加了一些本身的理解,若是你喜欢个人文章,请点个赞表示鼓励或者分享给你的朋友。

相关连接

Demo源代码(Vue单文件组件形式)

原文连接

IOC控制反转

相关文章
相关标签/搜索