使用Vue Composition API寫出清晰、可擴展的表單

表單是前端開發中最棘手的部分之一,您可能會在其中發現不少混亂的代碼。javascript

基於組件的框架,如 Vue.js,在提升前端代碼的可擴展性方面作了不少工做,可是表單的問題仍然存在。css

在本教程中,將向您展現新的 Vue Composition API(即將加入 Vue 3 中)如何使表單代碼更清晰、更具可擴展性。前端

爲何表單代碼常常很爛

Vue 這種基於組件的框架的關鍵設計模式是組件組合。vue

這種模式將應用程序的特性抽象爲獨立的、單一用途的組件,這些組件通訊使用 props 和事件的方式。java

然而,在此模式下,不能很好地對錶單進行抽象,由於表單的功能和狀態顯然不屬於任何一個組件,所以將其分離一般會致使與解決的問題同樣多的問題。git

Vue 中表單代碼寫的爛的另外一個重要緣由是,直到 Vue2 以前, 尚未提供強大的手段在組件之間重用代碼。重用代碼對錶單來講很重要,由於表單輸入一般有明顯的不一樣,但在功能上有許多類似之處。github

Vue2 提供的代碼重用的主要方法是 mixin,我認爲這是一個明顯的反模式。npm

Mixins 被認爲「有害」

早在2016年中期,丹·阿布拉莫夫(Dan Abramov)就寫了《mixin被認爲是有害的》(mixin Considered Harmful),他在書中辯稱,將 mixin 用於在 React 組件中重用邏輯是一種反模式,主張遠離它們。後端

不幸的是,他提到的關於 React mixins 的缺點一樣適用於 Vue。在瞭解 Composition API 克服這些缺點以前,讓咱們熟悉這些缺點。設計模式

命名衝突

使用 mixin 模式在運行時合併兩個對象,若是他們兩個都共享同名屬性,會發生什麼?

const mixin = {
  data: () => ({
    myProp: null
  })
}
export default {
  mixins: [mixin],
  data: () => ({
    // 同名!
    myProp: null
  })
}

這就是合併策略發揮做用的地方。這是一組規則,用於肯定當一個組件包含多個具備相同名稱的選項時會發生什麼。

Vue 組件的默認(可選配置)合併策略指示本地選項將覆蓋 mixin 選項。不過也有例外,例如,若是咱們有多個相同類型的生命週期鉤子,這些鉤子將被添加到一個鉤子數組中,而且全部的鉤子都將被依次調用。

儘管咱們不該該遇到任何實際的錯誤,可是在跨多個組件和 mixin 處理命名屬性時,編寫代碼變得愈來愈困難。一旦第三方 mixin 做爲帶有本身命名屬性的 npm 包被添加進來,就會特別困難,由於它們可能會致使衝突。

隱式依賴

mixin 和使用它的組件之間沒有層次關係。

這意味着組件可使用 mixin 中定義的數據屬性(例如mySharedDataProperty),可是 mixin 也可使用組件中定義的數據屬性(例如myLocalDataProperty)。這種狀況一般是在 mixin 被用於共享輸入驗證時出現的,mixin 可能會指望一個組件有一個輸入值,它將在本身的 validate 方法中使用。

不過,這可能會引發一些問題。若是咱們之後想重構一個組件,改變了 mixin 須要的變量名稱,會發生什麼狀況呢?咱們在看這個組件時,不會發現有什麼問題。代碼檢查也不會發現它,只會在運行時看到錯誤。

如今想象一個有不少 mixin 的組件。咱們能夠重構本地數據屬性嗎?或者它會破壞 mixin 嗎?咱們得手動搜索才能知道。

mixins 的缺點是 Composition API 背後的主要推進因素之一,來看看它如何克服 mixin 的問題,寫出清晰、可擴展的表單代碼。

在 Vue2 項目添加 Vue Composition API

經過 Vue CLI 建立一個項目,將 Composition API 做爲插件添加到 Vue 2 項目中。

$ vue create composition-api-form
$ cd composition-api-form
$ npm i -S @vue/composition-api

接下來,在 main.js 中加入這個插件

import Vue from "vue";
import App from "./App.vue";

import VueCompositionApi from "@vue/composition-api";
Vue.use(VueCompositionApi);

new Vue({
  render: h => h(App)
}).$mount('#app');

建立輸入組件

爲了使這個例子簡單,咱們將建立一個僅包含輸入名字和電子郵件的獨立的組件。

$ touch src/components/InputName.vue
$ touch src/components/InputEmail.vue

設置 InputName 組件模板,包括一個 HTML 輸入元素,並使用 v-model 指令建立雙向綁定。

src/components/InputName.vue

<template>
  <div>
    <label>
      Name
      <input type="text" v-model="input" name="name" />
    </label>
  </div>
</template>
<script>
export default {
  name: 'InputName'
}
</script>

設置表單

將添加 novalidate 屬性,讓瀏覽器知道咱們將提供自定義驗證。還將監聽表單的 submit 事件,防止表單自動提交,並使用聲明的 onSubmit 方法處理該事件。

而後,添加 InputNameInputEmail 組件,並分別將本地狀態值 nameemail 進行綁定。

src/App.vue

<template>
  <div id="app">
    <form novalidate @submit.prevent="onSubmit">
      <InputName v-model="name" />
      <InputEmail v-model="email" />
      <button type="submit">Submit</button>
    </form>
  </div>
</template>
<script>
import InputName from "@/components/InputName";
import InputEmail from "@/components/InputEmail";
export default {
  name: 'App',
  components: {
    InputName,
    InputEmail
  }
}
</script>

接下來使用 Composition API 定義表單功能。在組件定義中添加 setup 方法,並使用 Composition API 提供的 ref 方法聲明兩個狀態變量 nameemail

而後聲明一個 onSubmit 函數來處理表單提交。

src/App.vue

// 其他省略
...
import { ref } from "@vue/composition-api";

export default {
  name: "App",
  setup () {
    const name = ref("");
    const email = ref("");
    function onSubmit() {
      // 這裏能夠寫提交後端的邏輯
      console.log(name.value, email.value);
    }
    return {
      name,
      email,
      onSubmit
    }
  },
  ...
}

設置輸入組件

接下來,將定義 InputName 組件的功能。

在組件上使用了 v-model 指令,就和組件建立了雙向綁定,在組件內部的 props 上定義 value 來接收值,這隻作了一半的工做。

建立一個 setup 函數。props 和組件實例被傳遞到這個方法中,使咱們可以訪問組件實例上的方法。

用解構的方式在第二個參數中得到 emit 方法。將須要它來完成 v-model 的雙向綁定的另外一半工做,即觸發 input 事件,修改綁定的值。

在此以前,聲明一個狀態變量 input,將綁定到咱們在模板中聲明的 HTML 元素上。

該變量的值是待定義的合成函數 useInputValidator 執行後返回的值。此函數將處理全部常見的驗證邏輯。

prop.value 傳遞給這個方法做爲第一個參數,第二個參數是一個回調函數,接收通過驗證後的輸入值,在這個回調函數中觸發 input 事件,修改 v-model 綁定的值,實現和父組件雙向綁定的功能。

src/components/InputName.vue

<template>
  <div>
    <label>
      Name
      <input type="text" v-model="input" name="name" />
    </label>
  </div>
</template>
<script>
import useInputValidator from "@/features/useInputValidator";

export default {
  name: "InputName",
  props: {
    value: String
  },
  setup (props, { emit }) {
    const { input } = useInputValidator(
      props.value, 
      value => emit("input", value)
    );
    // 綁定在元素上
    return {
      input
    }
  }
}
</script>

輸入框驗證功能

開始建立 useInputValidator 組合函數,爲此,首先建立一個 features 文件夾,而後爲其建立一個模塊文件。

$ mkdir src/features
$ touch src/features/useInputValidator.js

在模塊文件中,將導出一個函數,它須要兩個參數: 從父表單接收到的值,用 startVal 接收;以及一個回調函數,用 onValidate 接收。

函數須要返回一個 input 狀態變量,所以須要聲明它,經過調用 ref 並提供 startVal 的值進行初始化。

在從函數返回 input 以前,觀察該值的變化,並調用 onValidate回調,傳入最新的 input 的值。

src/features/useInputValidator.js

import { ref, watch } from "@vue/composition-api";

export default function (startVal, onValidate) {
  let input = ref(startVal);
  watch(input, value => { 
    onValidate(value);
  });
  return {
    input
  }
}

添加驗證器

下一步添加驗證器函數。對於 InputName 組件,只有一個驗證規則 minLength,確保輸入是三個字符或更多。還沒有建立的 InputEmail 組件將須要電子郵件驗證規則。

將在 src 文件夾中建立模塊 validators.js,並寫這些驗證器。在實際的項目中,您可能會使用第三方庫。

不會詳細介紹 validator 函數,可是有兩件重要的事情須要注意:

  • 這些是返回函數的函數。這樣的結構容許咱們經過傳遞成爲閉包一部分的參數來定製驗證規則。
  • 每一個驗證器返回的函數老是返回一個字符串(錯誤消息),若是沒有錯誤,則返回 null

src/validators.js

const minLength = min => {
  return input => input.length < min 
  ? `Value must be at least ${min} characters` 
  : null;
};

const isEmail = () => {
  const re = /\S+@\S+\.\S+/;
  return input => re.test(input)
  ? null
  : "Must be a valid email address";
}

export { minLength, isEmail };

回到上面的組合函數所在文件 useInputValidator.js 中,咱們但願使用須要的驗證,給函數添加另外一個參數,它是一組驗證函數。

input 監視器內部,使用數組的 map 方法調用驗證函數,將 input 的當前值傳遞給每一個驗證器方法。

返回值將在一個新的狀態變量 errors 中捕獲,也將返回給所在組件使用。

src/features/useInputValidator.js

export default function (startVal, validators, onValidate) {
  const input = ref(startVal);
  const errors = ref([]);
  watch(input, value => {
    errors.value = validators.map(validator => validator(value));
    onValidate(value);
  });
  return {
    input,
    errors
  }
}

最後回到 InputName 組件,如今將爲 useInputValidator 方法提供必需的三個參數。
第二個參數如今是一個驗證器數組,所以讓咱們在適當的地方聲明一個數組,並傳入 minLength 方法。

minLength 是一個工廠函數,調用並傳遞指定的最小長度。

如今咱們還從合成函數返回的對象獲取 inputerrors,它們都將從 setup 方法返回,以便在組件中可用。

src/components/InputName.vue

// 省略其餘代碼
...
import { minLength } from "@/validators";
import useInputValidator from "@/features/useInputValidator";

export default {
  ...
  setup (props, { emit }) {
    const { input, errors } = useInputValidator(
      props.value, 
      [ minLength(3) ],
      value => emit("input", value)
    );
    return {
      input,
      errors
    }
  }
}

這是咱們將添加到該組件的最後一個功能。在咱們繼續以前,花點時間對比一下這段代碼比使用mixin可讀性強得多。

首先,能夠清楚地看到狀態變量在哪裏聲明和修改,而沒必要切換到單獨的 mixin 模塊文件。另外,不須要擔憂局部變量和複合函數之間的名稱衝突。

顯示錯誤

進入 InputName 組件的模板,有潛在的錯誤數組要顯示,將其委託給一個稱爲 ErrorDisplay 的組件來顯示錯誤。

src/components/InputName.vue

<template>
  <div>
    <label>
      Name
      <input type="text" v-model="input" name="name" />
    </label>
    <ErrorDisplay :errors="errors" />
  </div>
</template>
<script>
...
import ErrorDisplay from "@/components/ErrorDisplay";

export default: {
  ...
  components: {
    ErrorDisplay
  }
}
</script>

ErrorDisplay 組件根據業務須要,能夠本身定製。

重用代碼

這就是咱們基於Composition API 寫的表單的基本功能。本教程的目標是建立清晰且可擴展的表單代碼,經過定義 InputEmail 組件,來證實咱們已經作到了這一點。

src/components/InputEmail

<template>
  <div>
    <label>
      Email
      <input type="email" v-model="input" name="email" />
    </label>
    <ErrorDisplay v-if="input" :errors="errors" />
  </div>
</template>
<script>
import useInputValidator from "@/features/useInputValidator";
import { isEmail } from "@/validators";
import ErrorDisplay from "./ErrorDisplay";

export default {
  name: "InputEmail",
  props: {
    value: String
  },
  setup (props, { emit }) {
    const { input, errors } = useInputValidator(
      props.value, 
      [ isEmail() ], 
      value => emit("input", value)
    );
    return {
      input,
      errors
    }
  },
  components: {
    ErrorDisplay
  }
}
</script>

原文:https://vuejsdevelopers.com/2...
參考:https://css-tricks.com/how-th...

github博客地址:https://github.com/WYseven/bl...
若是對你有幫助,請關注【前端技能解鎖】:
前端技能解鎖

相關文章
相關標籤/搜索