[譯]如何優雅地用 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, 或郵件聯繫我。

掃一掃關注迅雷前端公衆號

相關文章
相關標籤/搜索