阅读 6106

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

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

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

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

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

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

如果你的项目中需要使用这样一个组件,把这个作为一个库引入,并且剥离这些逻辑肯定能够节省很多时间和精力。

但是假如这个时候你需要一个不同样式展现呢?

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

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

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

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

作用域插槽(Scoped Slots)

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

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

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

通过向子组件里面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控制反转

关注下面的标签,发现更多相似文章
评论