[译]如何优雅地用 Vue 建立数据驱动的用户界面

翻译: 珈蓝 from 迅雷前端javascript

翻译自 Evan Schultz 的文章 Do it with Elegance: How to Create Data-Driven User Interfaces in Vuehtml

本文演示了如何利用 Vue 的动态组件根据 schema 来生成一个动态的表单生成器,在管理后台、设置中心等相似的场景中,你彻底能够利用这种思路来更效率地开发界面。前端

虽然咱们一般在构建大部分的视图时知道须要用到哪些组件,但有时咱们直到运行时才知道它们是什么组件(译者注:动态组件)。这意味着咱们须要基于应用程序状态、用户设置或来自 API 请求的响应结果来构建视图。一个常见的状况是构建动态表单,其中所需的问题和组件由 JSON 对象配置,或者字段根据用户的答案进行更改。vue

全部现代的 JavaScript 框架都有处理动态组件的方法。这篇文章将向你展现如何在 Vue.JS 中实现它,它为上面的场景提供了一个很是优雅简单的解决方案。java

一旦你看到用 Vue.JS 实现它是多么的简单,你可能会受到启发而且开始思考你之前从未考虑过的动态组件应用。git

咱们要先学会走才能学会跑,因此首先我将介绍动态组件的基础知识,而后深刻讨论如何使用这些概念构建你本身的动态表单构造器。github

基础

Vue 有一个叫作 <component>的内置组件,你能够在VueJS 指南的动态组件中了解完整的详细信息。并发

指南上写道:app

“你可使用相同的挂载点并使用保留的元素在多个组件之间动态切换,并动态绑定到其 is 属性。”框架

这意味着切换组件能够向像下面这样简单:

<component :is="componentType">  
复制代码

让咱们再多补充一点,看看发生了什么。咱们将建立两个组件叫作 DynamicOne 和 DynamicTwo - One 和 Two 都是同样的,因此我不会重复展现这两个的代码。

<template>  
  <div>Dynamic Component One</div>
</template>  
<script> export default { name: 'DynamicOne', } </script>
复制代码

下面是一个可以在它们之间切换的快速示例,咱们在 App.vue 中设置咱们的组件。

import DynamicOne from './components/DynamicOne.vue';
import DynamicTwo from './components/DynamicTwo.vue';

export default {
  name: 'app',
  components: {
    DynamicOne,
    DynamicTwo,
  },
  data() {
    return {
      showWhich: 'DynamicOne',
    };
  },
};
复制代码

注意:showWhich data 属性的值是字符串DynamicOne-这是在组件的components对象中建立的属性名。

在咱们的模板中,咱们将设置两个按钮来切换这两个动态组件。

<button @click="showWhich = 'DynamicOne'">Show Component One</button>  
<button @click="showWhich = 'DynamicTwo'">Show Component Two</button>

<component :is="showWhich"></component>  
复制代码

点击这两个按钮将会交换显示 DynamicOne 和 DynamicTwo

看到这你也许会想,“那又怎样呢?这很方便——但我用v-if同样很简单”。

当你意识到<component>能够像其余任何组件同样工做时,这个例子就开始发挥做用了,而且它能够与诸如v-for之类的东西结合用于迭代集合,或者将is绑定到 input 的属性、data 属性或计算属性上。

关于 props 和事件

组件不是孤立地存在,它们须要一种方式与周围的世界交流。在 Vue 中,这种方式是经过 props 和事件实现的。

你能够用和其余组件同样的方式在动态组件上设置 props 和绑定事件,而且若是加载的组件不须要该属性,Vue 也不会报未知属性的错误。

让咱们来修改咱们的组件来展现一个问候组件。一个组件会接受firstNamelastName,另外一个会接受firstNamelastNametitle

关于事件,咱们将在DynamicOne中添加一个按钮,它将发射一个叫作"upperCase"的事件,在DynamicTwo中,这个按钮将发射一个叫作"lowerCase"的事件。

把它们组合在一块儿,修改后的动态组件看起来像这样:

<component :is="showWhich" :firstName="person.firstName" :lastName="person.lastName" :title="person.title" @upperCase="switchCase('upperCase')" @lowerCase="switchCase('lowerCase')">
</component>  
复制代码

不是全部的属性或事件都须要在咱们正在切换的动态组件上定义。

你须要预先知道 props 吗?

在这一点上,你可能会想知道,“若是组件是动态的,而且不是全部的组件都须要知道每一个可能的 props,那我须要预先知道 props 并在模板中声明它们吗?”

谢天谢地,答案是否认的。Vue 提供了一个快捷方式,你能够用v-bind将一个对象的全部 key 都绑定到组件的 props 上。

这简化了模板:

<component :is="showWhich" v-bind="person" @upperCase="switchCase('upperCase')" @lowerCase="switchCase('lowerCase')">
</component>  
复制代码

关于表单

如今咱们拥有这些动态组件积木,咱们就能够开始在 Vue 基础上构建表单生成器了。

咱们从一个基本的表单模式开始 - 一个描述表单的字段,标签,选项等的 JSON 对象。首先,咱们从下列类型的输入表单开始:

  • 文本和数字输入域
  • 一个选项列表

初始模式是这样的:

schema: [
  {
    fieldType: 'SelectList',
    name: 'title',
    multi: false,
    label: 'Title',
    options: ['Ms', 'Mr', 'Mx', 'Dr', 'Madam', 'Lord'],
  },
  {
    fieldType: 'TextInput',
    placeholder: 'First Name',
    label: 'First Name',
    name: 'firstName',
  },
  {
    fieldType: 'TextInput',
    placeholder: 'Last Name',
    label: 'Last Name',
    name: 'lastName',
  },
  {
    fieldType: 'NumberInput',
    placeholder: 'Age',
    name: 'age',
    label: 'Age',
    minValue: 0,
  },
];
复制代码

看起来很是简单:能够配置标签,占位符等,选择列表还列出了可能的选项options。在这个例子中,咱们将保持组件的实现一直如此简单。

TextInput.vue - template

<div>  
  <label>{{label}}</label>
  <input type="text" :name="name" placeholder="placeholder">
</div>  
复制代码

TextInput.vue - script

export default {
  name: 'TextInput',
  props: ['placeholder', 'label', 'name'],
};
复制代码

SelectList.vue - template

<div>
    <label>{{label}}</label>
    <select :multiple="multi">
      <option v-for="option in options" :key="option">
        {{option}}
      </option>
    </select>
  </div>
复制代码

SelectList.vue - script

export default {
  name: 'SelectList',
  props: ['multi', 'options', 'name', 'label'],
};
复制代码

要根据上面定义的模式生成表单,须要添加如下内容:

<component v-for="(field, index) in schema" :key="index" :is="field.fieldType" v-bind="field">
</component>  
复制代码

表单效果以下:

数据绑定

若是生成表单但不绑定数据,它会有用吗?可能不会。咱们目前正在生成一个表单,但没有办法将数据绑定到它。你的第一反应多是在模式中添加一个value属性,而且在组件中使用v-model,以下所示:

<input type="text" :name="name" v-model="value" :placeholder="placeholder">
复制代码

这种方法存在一些潜在的缺陷,但咱们最关心的是 Vue 会给咱们一个错误/警告:

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"

found in

---> <TextInput> at src/components/v4/TextInput.vue
       <FormsDemo> at src/components/DemoFour.vue
         <App> at src/App.vue
           <Root>
复制代码

尽管 Vue 确实提供了语法糖,使组件状态的双向绑定更容易,但框架仍然偏向于单向数据流。咱们试图直接在组件内修改父组件的数据,因此 Vue 会向咱们发出警告。

仔细看看v-model,它没有太多的魔力,因此让咱们按照Vue 的表单输入组件指南中的描述来分解它。

<input v-model="something">
复制代码

和下面相同的

<input v-bind:value="something" v-on:input="something = $event.target.value">
复制代码

随着魔法揭示,咱们想要完成的是:

  • 让父组件将值提供给子组件
  • 让父组件知道值已更新

咱们经过绑定到value并发出@input事件来通知父组件值已经发生变化,从而完成此操做。

来看看咱们的TextInput组件

<div>
  <label>{{label}}</label>
  <input type="text" :name="name" :value="value" @input="$emit('input',$event.target.value)" :placeholder="placeholder">
  </div>
复制代码

因为父组件负责提供该值,所以它也负责处理绑定到它本身的组件状态。为此,咱们能够在组件上使用v-model

FormGenerator.vue - template

<component v-for="(field, index) in schema" :key="index" :is="field.fieldType" v-model="formData[field.name]" v-bind="field">
</component>  
复制代码

注意咱们如何使用v-model ="formData[field.name]"。咱们须要在这个 data 属性上设置一个对象:

export default {  
  data() {
  return {
    formData: {
      firstName: 'Evan'
    },
}
复制代码

咱们能够将对象留空,或者若是咱们有一些咱们想要设置的初始字段值,咱们能够在这里指定它们。

如今咱们已经完成了生成表单的工做,而且发现这个组件承担了至关多的责任。虽然这不是复杂的代码,但若是表单生成器自己是一个可复用组件,那将会很好。

打造可复用的生成器

对于这个表单生成器,咱们但愿将模式做为一个 prop 传递给它,而且可以在组件之间创建数据绑定。

用生成器的模板是这样:

GeneratorDemo.vue - template

<form-generator :schema="schema" v-model="formData">  
</form-generator>  
复制代码

这大大简化了父组件。它只关心FormGenerator,而不关心每一个可用的输入类型、链接的事件等等。

接下来,建立一个名为FormGenerator的组件。这几乎是复制粘贴最初的代码而后进行一些微小但关键的调整:

  • v-modle改成:value,而后用@input处理事件
  • 添加valueschema 到 props 上
  • 实现 updateForm方法

FormGenerator 组件以下:

FormGenerator.vue - template

<component v-for="(field, index) in schema" :key="index" :is="field.fieldType" :value="formData[field.name]" @input="updateForm(field.name, $event)" v-bind="field">
</component>
复制代码

FormGenerator.vue - template

import NumberInput from '@/components/v5/NumberInput';
import SelectList from '@/components/v5/SelectList';
import TextInput from '@/components/v5/TextInput';

export default {
  name: 'FormGenerator',
  components: {NumberInput, SelectList, TextInput},
  props: ['schema', 'value'],
  data() {
    return {
      formData: this.value || {},
    };
  },
  methods: {
    updateForm(fieldName, value) {
      this.$set(this.formData, fieldName, value);
      this.$emit('input', this.formData);
    },
  },
};
复制代码

因为formData属性并不知道咱们传入的每个可能的字段,咱们使用this.$set,这样 Vue 的响应系统就能够跟踪它的任何变化,并容许FormGenerator组件跟踪它本身的内部状态。

如今咱们有了一个基本的、可复用的表单生成器。

在组件内使用它:

GeneratorDemo.vue - template

<form-generator :schema="schema" v-model="formData">  
</form-generator>  
复制代码

GeneratorDemo.vue - script

import FormGenerator from '@/components/v5/FormGenerator'

export default {  
  name: "GeneratorDemo",
  components: { FormGenerator },
  data() {
    return {
      formData: {
        firstName: 'Evan'
      },
      schema: [{ /* .... */ },
}
复制代码

如今你已经看到了表单生成器如何利用 Vue 的基础动态组件建立一些高度动态的、数据驱动的 UI。我鼓励你好好研究下GitHub上的示例代码或者在CodeSanbox上实践。若是你有任何问题或者想聊一聊,能够随时经过 Twitter, Github, 或邮件联系我。

扫一扫关注迅雷前端公众号

相关文章
相关标签/搜索