Lara

构建无渲染的 Vue 组件(函数式组件)

原文链接: css-tricks.com

关于 Vue 有一种特别流行的说法:当 React 和 Angular 在一起并生了个孩子,那就是 Vue。我一直都有这种感觉。由于 Vue 的学习曲线很小,所以有那么多人喜欢它并不奇怪。Vue 一直试图让开发人员尽可能多地控制组件及其实现,这就引出了今天的话题。

术语 renderless components 是指不渲染任何内容的组件。本文中,我们将介绍 Vue 如何处理一个组件的渲染。

我们还将了解如何使用 render() 函数来构建无渲染组件。

为了更好地理解这篇文章,你可能想了解一点有关 Vue 的知识。如果你是新手可以访问网站 Sarah Drasner's got your back,另外,官方文档 也是一个很好的资源。

揭开 Vue 如何渲染组件的神秘面纱

Vue 有很多定义组件标签的方法。有:

  • 单文件组件 让我们像写普通的 HTML 文件一样定义组件及其标签。

  • 组件的 template 属性允许我们使用 JavaScript 的模板字面量来定义组件的标签。

  • 组件的 el 属性让 Vue 查询 DOM 以获取要用作模板的标签。

你可能听说过(可能很烦人):归根结底,Vue 及其所有的组件都只是 JavaScript。从我们写的 HTML 和 CSS 的数量来看,我可以理解为什么你会认为这种说法是错误的。案例与要点:单文件组件。

使用单文件组件,我们可以这样定义一个 Vue 组件:

<template>
  <div class="mood">
    {{ todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me' }}
  </div>
</template>

<script>
  export default {
    data: () => ({ todayIsSunny: true })
  }
</script>

<style>
  .mood:after {
    content: '&#x1f389;&#x1f389;';
  }
</style>

从上面的官方语言来看,我们怎么能说 Vue “只是 JavaScript”?但是,归根结底,Vue 确实是。Vue 确实试图让我们的视图更容易管理其样式和其他资源文件,但 Vue 没有直接这么做 — 它把它交给了构建流程,比如 webpack

当 Webpack 遇到 .vue 文件时,它将通过一个转换流程运行它。在这个过程中,css 会从组件中提取出来并放在它自己的文件中,文件的其余内容被转换成 Javascript。像这样:

export default {
  template: 
    <div class="mood">
      {{ todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me' }}
    </div>,
  data: () => ({ todayIsSunny: true })
}

好吧……不完全是我们上面说的那样。为了理解接下来会发生什么,我们需要说一下模板编译器。

模板编译器及 Render 函数

Vue 组件的构建过程这一部分对于编译和运行 Vue 当前实现的每个优化技术都是必要的。

当模板编译器遇到以下情况时:

{
  template: <div class="mood">...</div>,
  data: () => ({ todayIsSunny: true })
}

……它提取 template 属性并将其内容编译为 JavaScript。然后将一个 render 函数添加到组件对象中。此 render 函数将依次返回已转换为 JavaScript 的所提取的模板属性的内容。

这就是上面的模板在 render 函数中的样子:

...
render(h) {
  return h(
    'div',
    { class: 'mood' },
    this.todayIsSunny ? 'Makes me happy' : 'Eh! Doesn't bother me'
  )
}
...

请参阅官方文档以了解关于 render 函数 的更多信息。

现在,当组件对象传递给 Vue,组件的 render 函数经过一些优化转化成 VNode(虚拟节点)。VNode 是传递到 snabbdom 的内容(Vue 内部用来管理虚拟 DOM 的库)。Sarah Drasner 对上面所说的 render 函数中的 “h” 做了很好的解释。

VNnode 是 Vue 渲染组件的方式。顺便说一下,render 函数还允许我们在 Vue 中使用 JSX

我们也不必等 Vue 为我们添加 render 函数 — 我们可以定义 render 函数而且它应该优先于 el 或 template 属性。阅读此处了解 render 函数及其选项

通过 Vue CLI 或某些自定义构建过程生成 Vue 组件,你就不需要引入会增大生成文件大小的模板编译器。你的组件也会进行预优化,从而获得卓越的性能及真正轻量级的 JavaScript 文件。

所以……无渲染的 Vue 组件

正如我提到的, 术语 renderless components 是指不渲染任何内容的组件。那么我们为什么想要不渲染任何内容的组件呢?

我们可以将其归结为创建一个普通组件功能的抽象来作为自己的组件,并扩展所述组件,以创建更好、甚至更健壮的组件。也符合 S.O.L.I.D 原则。

根据 S.O.L.I.D. 的单一责任原则:

一个类应该只有一个目的。

通过使每个组件只有一个职责,我们可以将这个概念一直到 Vue 开发中。

你可能会像 Nicky 一样说 “切,是的,我知道”。好吧,当然!你可能写了一个名为 “password-input” 的组件,并且确实能渲染一个密码输入框。但是这种方法的问题是,当你想在另一个项目中复用此组件时,你可能必须在该组件的源代码中修改样式或标签,以便与新项目的样式指南保持一致。

这打破了 S.O.L.I.D. 的一条规则,即 开闭原则 ,该原则规定:

在开闭原则下,一个类或组件,应该可以扩展,但不能修改。

这就是说,您应该能够扩展组件,而不是编辑组件的源代码。

由于 Vue 了解 S.O.L.I.D. 原则,因此它允许组件具有 propseventsslots。和 scoped slots ,从而使组件的通信和扩展变得轻而易举。然后,我们可以构建具有所有功能的组件,而无需任何样式或标记。这对于代码的可重用性和高效性来说是非常好的。

构建 “Toggle“ 无渲染组件

这很简单,不需要建立 Vue CLI 项目。

Toggle 组件允许你在 on 和 off 状态之间切换。它也会提供辅助函数来进行状态的切换。它对于构建类似 on/off 组件(如自定义复选框)和任何需要 on/off 状态的组件都很有用。

让我们快速存根我们的组件:转到 CodePen pen 的 JavaScript 区域,然后继续。

// toggle.js
const toggle = {
  props: {
    on: { type: Boolean, default: false }
  },
  render() {
    return []
  },
  data() {
    return { currentState: this.on }
  },
  methods: {
    setOn() {
      this.currentState = true
    },
    setOff() {
      this.currentState = false
    },
    toggle() {
      this.currentState = !this.currentState
    }
  }
}

这个组件是非常小的而且还没有完成。它需要一个模板,而且因为我们不希望该组件渲染任何东西,所以我们必须确保它能与任何能渲染的组件一起工作。

可以使用 slots !

无渲染组件中的 Slots

Slots 允许我们在 Vue 组件的起始标签和闭合标签之间放置内容。像这样:

<toggle>
  This entire area is a slot.
</toggle>

在 Vue 的单文件组件中,我们可以像这样来定义一个 slot :

<template>
  <div>
    <slot/>
  </div>
</template>

好吧,如果使用 render 函数来实现,我们可以:

// toggle.js
render() {
  return this.$slots.default
}

我们可以自动把东西放到我们的 toggle 组件中,没有标签,什么都没有。

使用作用域插槽(Scoped Slots)向上发送数据

在 toggle.js 中,我们有一些 on 的状态和在 methods 对象中的辅助函数。如果我们能让开发人员获取到它们,那就太好了。我们现在正在使用的 slots,不允许我们暴露子组件中的任何内容。

我们想要的是 scoped slots (作用域插槽),作用域插槽(scoped slots)与插槽(slots)的工作方式完全相同,其额外优点是具有作用域插槽的组件能够在不触发事件的情况下暴露数据。

我们可以这样做:

<toggle>
  <div slot-scope="{ on }">
    {{ on ? 'On' : 'Off' }}
  </div>
</toggle>

你能看见的 div 上的 slot-scope 属性正在解构从 toggle 组件中暴露的对象。

回到 render() 函数,我们可以做:

render() {
  return this.$scopedSlots.default({})
}

这一次,我们将 $scopedSlots 对象的默认属性当作方法来调用,因为作用域插槽是接受一个参数的方法。本例中,方法名是默认的,因为没有为作用域插槽指定名称并且它是唯一存在的作用域插槽。我们传给作用域插槽的参数可以从组件中暴露出去。在我们的例子中,让我们暴露 on 的当前状态以及辅助函数来帮助控制该状态。

render() {
  return this.$scopedSlots.default({
    on: this.currentState,
    setOn: this.setOn,
    setOff: this.setOff,
    toggle: this.toggle,
  })
}

使用 Toggle 组件

我们可以在 CodePen 中进行操作。这里是我们正在构建的东西:

CodePen 上查看由 Samuel Oloruntoba(@kayandrae07) 编写的代码模板。

下面是运行中的标签:

<div id="app">
  <toggle>
    <div slot-scope="{ on, setOn, setOff }" class="container">
      <button @click="click(setOn)" class="button">Blue pill</button>
      <button @click="click(setOff)" class="button isRed">Red pill</button>
      <div v-if="buttonPressed" class="message">
        <span v-if="on">It's all a dream, go back to sleep.</span>
        <span v-else>I don't know how far the rabbit hole goes, I'm not a rabbit, neither do I measure holes.</span>
      </div>
    </div>
  </toggle>
</div>

  1. 首先,我们正在解构作用域插槽中的状态及辅助函数。

  2. 然后,在该作用域插槽中,我们创建了两个按钮,一个用来将状态切换为 on,另一个用来将状态切换为 off 。

  3. 单击方法仅用于确保按钮在显示结果之前确实被按下。您可以查看下面的点击方法。

new Vue({
  el: '#app',
  components: { toggle },
  data: {
    buttonPressed: false,
  },
  methods: {
    click(fn) {
      this.buttonPressed = true
      fn()
    },
  },
})

我们仍然可以通过 Toggle 组件传递属性和调用事件。使用作用域插槽不会改变任何内容。

new Vue({
  el: '#app',
  components: { toggle },
  data: {
    buttonPressed: false,
  },
  methods: {
    click(fn) {
      this.buttonPressed = true
      fn()
    },
  },
})

这是一个简单的例子,但是当我们开始构建像 date picker 或autocomplete 这样的组件时,我们就可以看到它有多么的强大。我们可以在多个项目中复用这些组件,而不必担心那些讨厌的样式表妨碍了这些组件的使用。

我们可以做的另一件事是从 scoped slot 中暴露可访问所需的属性,而且不需要担心让扩展该组件的组件变得容易访问。

综上所述

  • 组件的 render 函数极其强大。

  • 构建更快速运行的 Vue 组件

  • 组件的 el,template 甚至单文件组件都被编译成 render 函数。

  • 尝试构建更小的组件以获得更多可复用的代码。

  • 你的代码不需要遵守 S.O.L.I.D.,但这是一个特别好的编码规范。

引用