1. 程式人生 > 實用技巧 >使用Vue Composition API寫出清晰、可擴充套件的表單

使用Vue Composition API寫出清晰、可擴充套件的表單

表單是前端開發中最棘手的部分之一,您可能會在其中發現很多混亂的程式碼。

基於元件的框架,如vue.js,在提高前端程式碼的可擴充套件性方面做了很多工作,但是表單的問題仍然存在。

在本教程中,將向您展示新的vueComposition API(即將加入 Vue 3 中)如何使表單程式碼更清晰、更具可擴充套件性。

為什麼表單程式碼經常很爛

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

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

然而,在此模式下,不能很好地對錶單進行抽象,因為表單的功能和狀態顯然不屬於任何一個元件,因此將其分離通常會導致與解決的問題一樣多的問題。

Vue中表單程式碼寫的爛的另一個重要原因是,直到Vue2之前, 還沒有提供強大的手段在元件之間重用程式碼。重用程式碼對錶單來說很重要,因為表單輸入通常有明顯的不同,但在功能上有許多相似之處。

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

Mixins 被認為“有害”

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

不幸的是,他提到的關於reactmixins的缺點同樣適用於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元件根據業務需要,可以自己定製。

vi設計http://www.maiqicn.com 辦公資源網站大全https://www.wode007.com

重用程式碼

這就是我們基於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>