在Vue中如何用数据来驱动用户界面

发布于 大漠

特别声明,本文来源于@Evan Schultz的《Do it with Elegance: How to Create Data-Driven User Interfaces in Vue》。

虽然我们通常知道在应用程序中构建大多数视图(View)时需要哪些组件(Component),但很多时候我们并不知道这些组件在运行时是什么。这意味着我们需要基于应用程序状态、用户首选项或API的响应来构建一个页面。一个常见的情况是构建动态表单,其中需要整清楚的是:组件是由JSON对象配置的,还是基于用户的答案而更改的字段来构建

所有现代JavaScript框架(比如我们熟悉的Vue、React等)都有处理动态组件的方法。这篇文章将向您展示如何在Vue中实现它。JavaScript为上述场景提供了一个非常优雅和简单的解决方案。

一旦你看到使用Vue是这么的容易,你可能会受到启发,看到你从未考虑过的动态组件的应用程序!

在运行之前,我们需要先走一步,先介绍动态组件的基础知识,然后深入了解如何使用这些概念来构建你自己的动态表单构造器。

基础知识

Vue有一个内置组件,称为<component>。你可以在Vue指南中看到有关于动态组件的完整细节。

Vue指南是这样描述的:

你可以使用相同的挂载点,并使用保留元素动态地在多个组件之间切换,并动态绑定到它的属性。

这意味着在组件之间进行交换变得很简单:

<component :is="componentType">  

让我们把它具体化一些,看看会发生什么。首先创建两个组件,分别名为DynamicOneDynamicTwo。这两个组件中的代码是相同的,只是name的值不一样:

<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'
        }
    }
}

注意,data中的showWhich属性值是DynamicOne的字符串值,这是components对象中创建的name的值。

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

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

<component :is="showWhich"></component>  

点击按钮将会用DynamicTwo替换DynamicOne

这个时候你可能会说,这又怎么样?是很方便,但也可以使用v-if来实现同样的效果。

当你意识到<component>和其他组件一样工作时,这个示例就开始发挥作用了,它可以与v-for这样的Vue指令结合起来使用,用于对迭代集合,或者使用:is可以绑定到一个inputdataprops或者是computed属性。

props和events

组件不是孤立存在的 —— 它们需要一种与周围世界交流的方式。如果使用Vue的话,那么这种交流是通过propsevents来完成的。

你可以在动态组件上指定与其他组件相同的属性和事件绑定,如果加载的组件不需要该属性,Vue将不会抱怨未知的"Attributes”或“properties”。

让我们修改组件显示的内容。DynamicOne组件只接受firstNamelastName,而另一个组件DynamicTwo接受firstNamelastNametitle

对于事件,我们将在DynamicOne中添加一个按钮,它将发出(emit)一个名为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将对象的所有键绑定到组件的props

这个时候模板可以简化为:

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

表单形式

现在我们已经有了一个动态组件区块,接下来可以开始构建一个表单生成器。

让我们从一个基本的表单模式开始 —— 一个JSON对象,它描述了表单的字段、标签和选项等:

  • 文本和数字输入域
  • 一个下拉列表

开始的schema看起来像这样:

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
    }
]

非常简单。在本例中,将保持这些简单的组件实现。

TextInput.vue:

<template>
    <div>
        <label>{{ label }}</label>
        <input type="text" :name="name" :placeholder="placeholder" />
    </div>
</template>

<script>
    export default {
        name: 'TextInput',
        props: ['placeholder', 'lable', 'name']
    }
</script>

SelectList.vue

<template>
    <div>
        <label>{{ label }}</label>
        <select :multiple="nulti">
            <option v-for="option in options" :key="option">{{ option }}</option>
        </select>
    </div>
</template>

<script>
    export default {
        name:'SelectList',
        props: ['multi', 'options', 'name', 'label']
    }
</script>

基于这个schema要生成表单,还要加加下面的内容:

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

结果如下:

数据绑定

如果表单生成但不绑定数据,估计也没啥有用。上面生成了一个表单,但没有绑定数据。你可以第一反应就是在组件中使用v-model来绑定schema中的value属性。

<input 
    type="text"  
    :name="name"
    v-model="value"
    :placeholder="placeholder">

这种方法有一些潜在的缺陷,但是Vue会给我们反馈相应的错误或警告信息:

虽然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>

由于父类负责提供值,所以它也负责处理绑定到它自己的组件状态。为此,我们可以在<component>签上使用v-model

FormGenerator.vue:

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

注意,我们如何使用v-model="formDtata[field.name]"。我们需要在数据属性上提供一个对象:

export default {  
    data() {
        return {
            formData: {
                firstName: 'Evan'
            },
        }
    }
}

我们可以让对象为空,或者如果我们有一些我们想要设置的初始字段,就可以在这里指定它们。

现在我们已经完成了生成表单的过程,这个组件承担了相当多的责任。虽然代码不是很复杂,但是表单生成器自身要是可复用的组件,那就更好了。

让生成器可重用

对于这个表单生成器,我们希望将schema传递给prop,并能够在组件之间设置数据绑定。

当使用生成器时,模板GeneratorDemo.vue变为:

<form-generator :schema="schema" v-model="formData"></form-generator>  

这对父组件进行了相当大的清理。它只关心FormGenerator,并不关心可以使用的每个输入类型、连接事件等。

接下来,创建一个名为FormGenerator的组件。这将最初可复制的代码变得并不重要,重要的是做了一些关键的调整:

  • v-model改变:value@input事件处理
  • props中添加valueschema
  • 实现updateForm

FormGenerator组件变成这样:

<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>

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:

<form-generator :schema="schema" v-model="formData"></form-generator>  

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

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

现在,你已经了解了表单生成器。通过这个简单的示例学习了如何利用Vue的动态组件的基础来创建一些高度动态,数据驱动的UI。文章中整个示例的代码可以在GitHub上获取到,或者在CodeSandbox上进行修改。如果你有任何问题或想要聊的地方,可以通过TwitterGitHub电子邮件联系我。你也可以在下面的评论中与我们一起探讨相关的话题。

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/vue/how-to-create-data-driven-user-interfaces-in-vue.htmlnike air max 1 custom