使用Vue Composition API写出清晰、可扩展的表单

表单是前端开发中最棘手的部分之一,您可能会在其中发现很多混乱的代码。

基于组件的框架,如 vue.js,在提高前端代码的可扩展性方面做了很多工作,但是表单的问题仍然存在。

在本教程中,将向您展示新的vue ComposiH : ! @ D z E 2tion| x l API(即将加入 Vue 3 中)如何使表单i Z 2 0代码更清L ; y D P e Y V /晰、更具可( Q U i扩展性。E % i R ] } @ X

为什么表单代码经常很烂

Vue这种基于组件的框架的关键设计模式是组件组合。

这种模式将应用程序的特性抽象为独立的、单一用途的组件,这些组件通信使用pu ~ } ] . m L Krops和事件的方式。

然而,在此模式下,不能很好地对表5 H 9 C b R m单进行抽象,因为表单的功能和状态显然不属于任何一个组件,因此将其分离通常会导致与解决的问题一样多的问题。

Vue中表单代码写的烂的另一个重要原因是,直到Vue2之前, 还没有提供强大的手段在组件之间重用代码。重用代码对表单来说很重要,因为表单输入通常有明显的不同,但在功能上有许多相似之处。

V1 I , u Mue2提供的代7 x J / 8 { M码重用的主要方法是mix6 { ? d Win,我认为这是一个明显的反模式。

Mixins 被认为“有害”

早在2016年中期,丹阿布拉莫夫(Dan Abramov)就写了《mixin被认为是有5 s } V害的》(mixin Considered Harmful),他在书中辩称,将mixin用于在react组件中重用逻. Q F , ] @ A辑是一种反模式,主张远离它们。

不幸的是,他提到的关于react mixins的缺点同样适用于Vue。在了解Composition API克服这些缺点之前,让我们熟悉这些缺点。

命名冲突

使用mixin模式j . J在运行时合并两个对象,如果他们~ J e 8 @ |两个都共享同名属性,会发生什么?

consT F - ` u 3t mixin = {
data: () => ({
myProp: nug L S $ / L + ( /ll
})
}
export default {
mixins: [mixin],
data: () => ({
// 同名!
myProp: null
})
}

这就是合并策略发挥作用的地方。这是一组Z C E O $ 8规则,用于确定当一个组件包含多个具有相同名称的选项时会发生什么。

Vue组件的默认(可选配置)合并策略指. 0 5 i , R示本地选? a d ^项将覆盖mixin选项。不过也有例外,例如,如果我们有多个相同类型的生命周期钩子,这些钩子将被添加到一个钩子数组中,并且所有的钩子都将被依次调用。

尽管我们不应该遇到任何实际的错误,但是在跨多个组件和mixin处理命名属性时,编写代码变得越来越困难。一旦第三方mixin作为带有自己命名属性的npm包被添加进来,就会特别困难,因为a Y P u ) v v它们可能会导致冲突。

隐式依赖

mixin和使用它的组件之间没有层次关系。

这意味着组件可以使用mixin中定义的数据属性(例如mySY J $ # ] , rharedDataProperty),但是mixin也可以使用组件中定义的数据属性(例如myLocalDataProperty)。这种情况通常是在mixin被用于共享输入验证时出现的,mixin可能会期望一个组件有一个输入值,它将在自己的validate方法中使用。

不过,这可m H 7能会引起一些问题。如果我们以后想重构一个组件,改变了mixin需要的变量名称,会发生什么情况呢?我9 A 8 k ?们在看这个组件时,不会发现有什么问题? ? l ` @ 7。代码检查也不会发现它,只会在运行时看到错误。

现在想象一个有很多mixin的组件。 8 K /我们可以重构本地数据属性吗?或者它会破坏mixin吗?我们得手w G ] # O ~ D #动搜索才能知道。

mixins5 : d i的缺点是Composition API背后的主要推动因素之一,来看看它如何克服mixin的问题,写出清晰、可扩展的表单代码。

在 Vue2 项目添加 Vue Composition API

通过Vue CLI创建一个项目,将Composition API作为插件添加到Vue 2项目中。

$ vue creaK % Ite composition-api-fob s r W I vrm
$ cd composition-api-form
$ npm i -S @vue/composition-api

接下来,在main.js中加入这个插件

import Vue from "vue";
import App from "./App.vue"6 ? [ v;
import VueCompositionApi from "@vue/compositiW ! H m C c Yon-api";
Vue.use(VueCompositionApi);
new Vue({
render: h => h(Z 5 o l r D .App)
}).$mount('#app');

创建~ e ! 输入组件

为了O ^ R V % m a 3使这个s m ~ | s Q p例子简单,我们将创建一个仅包含输入名字和电子邮件的独立的组件6 } j a

$ touch src/componentx n ) p X % F ys/InputNameR J i $ F ; s.vue
$ touch sl n 9 s F i - yrc/components/ID T bnputEmail.vue

设置InputName组件模板I P m b = G A !,包括一个html输入元素,并使用v-model指令创建双向绑定。

src/components/InputName.vue

&l_ & ~ V [ t o F yt;template>
<div>
<label>D } 3 j G M q @ n
Name
<input type="textG 5 d =  y b O" v-model="input" name="name" />
</label>
</div>8 E 2 C m 8 E
</template>
<script>
export default {
name: 'InputName'
}
</script>

设置表单

将添加novalidate属性,让浏览器知道我们将提供自定义验证。还将监听表单的submit事件,防止表单自动提交,并使用声明的onSubmit方法处理b , q e V 0 j #该事件* D 8 Q G 2 $

然后,添加Ind o = R w ~ x 5 6putNameInputEmail@ { @ % o % * c件,并分别将本地状态值nameemail进行绑定。

src/App.vue

<template>
<div id="app">
<form novalidate @submit.prevent="onSubmit">
<InputName v-model="name" />
<InputEmail v-m5 E . n y r vodel="email" />l p F G;
<button type; + x="submit"&3 E k m a wgt;Submit</button>
</form>
</div>
</template>
<scri` . e $ =pt>
import InputName from "@/components/InputName";
import InputEmail from "@/components/InputEmail";
export default {
name: 'App',
components: {
InputName,
InputEmail
}
}
</script>

接下来使用CompositF M _ 0 3ion API定义表单功能。在组件定义中添加setup方法,并使用Composition API提供的ref方法声明两个状态变量nameemail

然后声明一个onSubmit函数来处理表单提交。

src/App.vue

// 其余省略
...
ims 3 y J zport { ref } from "@vue/composition-api";
export default {
name: ^ p x 5"App",z ` F d } b D K e
setup () {
const name = ref("");
const email = ref("");
function onSubmit() {
// 这里可以写提交后端的逻辑
console.log(name.value, email.value);
}
return {
name,
emQ z Eail,
onSubmit
}
},
...
}

设置输入组件

接下来,将定义InputName组件的功能。

在组件上使用了v-model指令,就和组件创建了双向绑定,在组件内部的props上定义value来接收值,这只做了一半的工作。

创建一个setup函数。props和组件实例被传递到这个方法中,使我们能够访问组件实例上的方k E ^ | l i u d &法。

用解构的方式在第二个参数中获得emit方法。将需要它来完成v-model的双向绑定的另一半工F | ) ~ s作,即e N v g触发input事件,* @ T , 9 * D t f修改绑定的值。

在此之前,声明一个状态变量input,将绑定到我们在模板中声明的html元素上。

该变量的值是待定义的合成函U V | * ) ;useInputValidator执行后返回的值。此函数将处理所有常见的验证逻辑。

prop.value传递给这个Z X . v 方法作为第一个参数,第二个参数是一个回调函数,接收经过验证后的输入值,在这个回调函数+ P Z n 9 2 k中触发input事件,修改v-model绑定的值,实现和父组件双向绑定的功能。

src/components/InputName.vue

<template&gp $ I o Rt;
<div>
<label>
Name
<in| @ U #put type="text" v-modef  :l="input" name="name" />
</labH ? B 3 .el>
</div>
</template>
<script>
import useInputValidator from "@/features/useInput5 K ^ D o E + 1 gValidator";
export default {
naz O Q H $me: "InputName",
props: {
value: String
},
setup (props, { emit }) {
const { inpuV _ wt } =m * / |  + 9 useInputValidatm | -or(
props.vx a Xalue,
value => emit("input", value)
);
/@ y b/ 绑定在元素上
rS p & ` ?eturp a Vn {
inP T , $ v yput
}
}
}
</script>

输入框验证功能

开始创建useInputValidator组合函数,为此,首先创建一个features文件夹,然后为其创建一个模块文件。

$ mkdir srs H f V  C $c/features
$ touch src/features/useInputValidator.js

在模块文件中,将导出一个函数,它需要两个参数: 从父表单接收到的值,用startVQ l m } b Kal接收;以及一个回调函数,用onValidate接收。

函数需要返回a ] & n j )一个input状态J W ^变量,因此需要声明它,通过调用ref并提供startVal的值进行初始化。

在从函l Y D w数返回input之前,观察该值的变化,并调用onValidate回调,传入最新的input的值。

src/features/useInputValidator.js

ii # Nmport { ref, watch } fM ^  + - ^rom "@v: y b - S G 1ue/composition-api";
export defO _ . ) j 1 # I paultg W & function (startVal, onValidate) {
let input = ref(startVal);
watch(input, value => {
onValidate(value);
});
return {
input
}
}

添加验证器

下一步添加验证器函数。对于InputName组件,只有一个验证规则minLength,确保W _ | 4 i H !输入是三个字符或更多。尚未创建的IC B ^ p z ; ZnputEmail组件将需要电子邮件验证规则。

将在src文件夹中创建模块validators.js,并写这些验证器。在实际的项目中,您可8 R I * n A K能会使用第三方库。

不会详细介绍validator= j & $ D C } M8 ? = T数,但是有两件重要的事情需要注意:

  • 这些是返回函数的函数。这样的结构允许我& Z , * & I | ^ N们通过传递成为闭包一部分的参数来定制验证规则。
  • 每个验证器返回的函数总是返回一个字符串(错误消息),如果没有错误,则返回null

src/vaj j Q 8 d U m +lidators.js

const minLength = min => {
return input => input.length < min
? `Value must be at least ${min} characters`
: null} J ) ] H;
};
const isEmail = () => {
const re = /\S+@\S+\7 % 2 t 1 _.\S+/;
return input => re.test(input)
? null
: "Musy b 6 X , qt ba 6 : 9 $ Ie a? g + # % F v valid email address";
}
export { min[ @ ! Z d p - XLength, isEmail };

回到上面的组合函数所在文件useInputValidator.js中,我们希望使用需要的验证,给函数添加另一个参数,它是一组验证函数。

input监视器内部,使用数组的map方法调用验证函数,将input的当前值传递给每个验证器方法。

返回值将在一个新的状态变量ers U ) erorsq ` r - g C j E中捕获,也将返回给所在组件使用。

src/features/O * : [ yuseInputValidator.js

eS y x k O [xport default function (starz D z H ItVal, validators, onValidate) {
const input = ref(startVal);
const errors = ref([]);
watch(ia J {nput, value => {
errors.value = validators.map(validator => validator(vG N = calue));
onValA B Qidate(value);
});
return {
input,
errors
}
}

最后回到InputName组件,现在将为useInputValidator方法提供必需的三) W M j E =个参数。
第二个参数现在是一个验证器数组,因此让我们在适当的地方声明一个数组,并传入minLength方法。

minLength是一个工厂函数,调用o o x K q并传递指定的最小长度。

现在我们还从合成函数返回的对象获取inputerrorV d t Os,它们U e _ V = / ( ?A / k _ ? s P U将从setup方法返回,以便在组件. V L 7 ~ E - _ s中可用。

src/components/InputName.vue

// 省略其他0 t |代码
...
import { minLength } frog % * . C S zm "@/validators";
import useInputValidator from "@/featurew . U H ) t ) $ ws/useInputValidator";
expor[  s L a Dt default {
.7 K ] u h ( g * 6..
seK ] r l U ^ 7tup (props, { emit }) {
const { input, b t O o, err{ + ^ p 9 $ N Sors } = useInputValidator(
props.value,
[ minJ P 9 o % X pLength(3) ],
value => emit("input", value)
);
return {
input,
errors
}
}
}

这是我们将添加到该组件的最后一个功能。在我们继续之前,花点时间对比一下这段代码比使用mixin可读A + M _ u ~ Y性强得多。

首先,可以清楚地看到状态变j 2 C Q | : A量在哪里声明和修改,而不必切换到单独的 mixin 模块文件。另外,不需要担心局部变量和复合函数之间的名称冲突。


显示错误g t / T k t J

进入InputName组件的模板,有潜在的错误数组 c p n要显示,将其委托给一个称为ErrorDisplay的组件来显示错误。

src/componentr v y q } 9 8 1s/InputName.vue

<template>
<div>
<| O c;label>
Name
<input type="text" v-model="input+ C d" name="name" />
</label>
<ErrorDisplay :errord , 3 4 Z os="errors" />
</div>
</templatj z $ i +e>
<script>
...
import ErrorDisplay from "@/components/ErrorDisplay";
export default: {
...
components: {
ErrorDisplay
}
}
</script>

EC L ; VrrorDisplay组件根据业t D x u @ P N w务需要,可以自己定制。

重用代码

这就是我们基于Composition API写的表单的基本功能。本教程的目标是创建清晰且可扩展的表单代码,通过定义InputEmais | 3 : t y ~l组件,来证明我们已经做到了这一点。

src/components/InputEmail

<template>
<div>
<label>
Email
<input type="email" v-model="input" name="email" />
</label>
<ErrorDisplay v-if="input" :errors="errors" />
&l& / I O O tt;/div>
<k p i $  L / { I;/template>
<script>
import useInput/ K VValidator from "@/features/useInputValidator";
import { isEmail } f? N - / Z $ q 8rom "@/? s x Ovalidators";
import ErrorDisplay from "./ErrorDisplay";
export defaul@  7 / / Ft {
name: "InputEmailt ( y - [ k n } ?",
prQ d o ^ W s qops: {
value: String
},
setup (props, { emit }) {
const { input, errors } = useInputVal_ T | 7 { z M eidator(
props.value,
[ isEmail() ],
value => emit("input", value)
);
return$ R i f ; ? ~ = & {
input,
errors
}
},o / S n s A r |
components: {
ErrorDisplay
}
}
</script>

原文:https://vuejsdevelopers.cz N J f + v |om/
参考:https://csV s Rs-tricks.com/
github博客地址:https://github.com/WYseven/

站长推荐

1.云服务推U T A 9 b H F G 5荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平N 8 | / h M台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http] S 4 8 B k 3 J Z://www.fly63.co% 9 ~ ` 5m/article/detial/9281