Skyphobia

Vue 组件封装指南(一)——透明封装组件

Front-EndjsVue

什么是透明封装组件

What is Transparent Wrapper Components?

透明组件是一个用于封装其他组件为新组件可以将父组件的参数透传给子组件的组件。

也就是说,透传参数(Transparent transmission)+ 封装组件(Wrapper Component) = 透明封装组件(Transparent Wrapper Component

封装组件这件事情我们再熟悉不过了,那所谓的参数透传是什么意思呢?我们可以先来看看这个简单的 input 组件:

HTML
01<template>
02 <input
03 :value="value"
04 :type="type"
05 @input="handleInput"
06 @keydown="handleKeydown"
07 />
08</template>
09
10<script>
11export default {
12 props: {
13 value: String,
14 type: String
15 },
16
17 methods: {
18 handleInput (e) {
19 this.$emit('input', e.target.value)
20 },
21
22 handleKeydown (e) {
23 this.$emit('keydown', e)
24 }
25 }
26}
27</script>

这看上去似乎没什么问题,这个组件可以很好的满足我们当前的需求。但万一到了周末,你吃着火锅唱着歌,产品姥爷突然和你说:“醒了吗?再加个 placeholder 吧?”——这还算是特别简单的参数扩展需求,要遇上个复杂的参数扩展,每次都要在模板中罗列新的属性和事件,整个过程涉及到注册属性(定义 props)、写事件代码(emit events)你不立马原地爆炸?

那为了让子弹多飞一会儿,我们应该在这里设计一个可以忽略参数个数,直接把所有父组件参数透传给子组件的透明封装组件

为什么 React 不需要透明封装组件

Wrapper Components in React with Props

Google 能找到有关透明封装组件概念的文章最早出现在 2018 年——《Transparent Wrapper Components in Vue》。有趣的是,从那以后从未见过有人提起过“Transparent Wrapper Components in React”的话题。

这里直接给出暴论:“React 不需要设计透明封装组件”。不是 React 不配,而是 React 的设计很好的避免了在封装组件中定义一大堆参数和事件的问题:

React 既可以直接透传参数:<Component {...props} />

也可以通过高阶组件(HOCHigher-Order Component)实现 const EnhancedComponent = higherOrderComponnet(WrappedComponent)

Vue 2.x 如何实现透明封装组件

Transparent Wrapper Component in Vue 2.x

大致作以下几点思考:

  • Vue 2.x 组件传递的参数被挂在在组件实例上,有 $attrs$props 两种。$props 被定义在组件上,classstyle 会被特殊处理,其余均为 $attrs,可以合并后通过 v-bind 传递给子组件;
  • 如果封装的子组件存在 v-model ,众所周知,v-modelvalue + @input 的语法糖,拆分出来处理一下就是了;
  • 事件可以通过 v-on 将组件实例上的 $listeners 直接给子组件。

综上,我们可以得到类似这样的模板文件:

HTML
01<template>
02 <input
03 v-bind="attrs"
04 v-on="listeners"
05 :value="value"
06 />
07</template>
08
09<script>
10export default {
11 props: {
12 value: String
13 },
14
15 computed: {
16 attrs () {
17 return {
18 ...(this.$attrs || {}),
19 ...(this.$props || {})
20 }
21 },
22
23 listeners () {
24 return {
25 ...this.$listeners,
26 input: e => {
27 this.$emit('input', e.target.value)
28 }
29 }
30 }
31 }
32}
33</script>

但这并不完美,如果子组件需要传递 slots 的时候我们需要如何处理?为了处理这个问题,我们尝试使用渲染函数 h 来透传参数和插槽:

JavaScript
01export default {
02 props: {
03 value: String
04 },
05
06 render(h) {
07 return h(
08 'select',
09 {
10 props: {
11 ...this.$props,
12 value: this.value
13 },
14 attrs: this.$attrs,
15 on: {
16 ...this.$listeners,
17 input: (e) => {
18 this.$emit('input', e.target.value)
19 }
20 }
21 },
22 this.$slots.default
23 )
24 }
25}

这显然也不是一个完美的透明封装组件,我们还没有处理过 scoped slot 和 named slot,实际使用 slot 的业务场景也远比这个 demo 复杂得多,因此有关 slot 透传的详细方式我们留到下一次再说。

Vue 3.x 中的透明封装组件

One More Step! Transparent Wrapper Component in Vue 3.x!

既然我们在 Vue 2.x 中通过透明封装组件解决了繁琐的罗列参数和事件的问题,那就更进一步,看看 Vue 3.x 中透明封装组件的手段有什么改变吧:

JavaScript
01import { h } from 'vue'
02
03export default {
04 props: {
05 modelValue: String
06 },
07
08 emits: ['update:modelValue'],
09
10 render() {
11 return h(
12 'select',
13 {
14 props: {
15 value: this.modelValue
16 },
17 attrs: this.$attrs,
18 onInput: (e) => {
19 this.$emit('update:modelValue', e.target.value)
20 }
21 },
22 this.$slots.default()
23 )
24 }
25}

附言

透明封装组件的意义不仅仅是为了少写几个 props 的定义。在前端工程上我们通常会碰到一些可能已经被广泛应用在了项目各个角落的组件,但新的项目迭代中可能会要求对某一个引用了该组件的模块进行升级或改动。为了防止直接对该组件改动导致牵一发动全身,我们可以通过透明封装组件对这样的组件进行有效的隔离封装,保证新组件与旧组件大致行为和样式上的一致性,又能为其添加一些新的特性和改动。这种做法可能更接近封装的本意吧。

杂项