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


Posted in Javascript onJune 10, 2020

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

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

在本教程中,将向您展示新的 Vue Composition API(即将加入 Vue 3 中)如何使表单代码更清晰、更具可扩展性。

为什么表单代码经常很烂

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

这种模式将应用程序的特性抽象为独立的、单一用途的组件,这些组件通信使用 props 和事件的方式。

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

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

Vue2 提供的代码重用的主要方法是 mixin,我认为这是一个明显的反模式。

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 方法处理该事件。

然后,添加 InputName 和 InputEmail 组件,并分别将本地状态值 name 和 email 进行绑定。

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 方法声明两个状态变量 name 和 email。

然后声明一个 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 是一个工厂函数,调用并传递指定的最小长度。

现在我们还从合成函数返回的对象获取 input 和 errors,它们都将从 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/2020/03/31/vue-js-form-composition-api/
参考:https://css-tricks.com/how-the-vue-composition-api-replaces-vue-mixins/

到此这篇关于使用Vue Composition API写出清晰、可扩展的表单实现的文章就介绍到这了,更多相关Vue Composition API清晰、可扩展的表单内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript针对DOM的应用分析(三)
Apr 15 Javascript
深入理解JavaScript系列(46):代码复用模式(推荐篇)详解
Mar 04 Javascript
JavaScript实现多个重叠层点击切换效果的方法
Apr 24 Javascript
浅谈在js传递参数中含加号(+)的处理方式
Oct 11 Javascript
详解angular2封装material2对话框组件
Mar 03 Javascript
Angular 4依赖注入学习教程之Injectable装饰器(六)
Jun 04 Javascript
bootstrap 通过加减按钮实现输入框组功能
Nov 15 Javascript
Bootstrap 中data-[*] 属性的整理
Mar 13 Javascript
angular2/ionic2 实现搜索结果中的搜索关键字高亮的示例
Aug 17 Javascript
微信小程序多音频播放进度条问题
Aug 28 Javascript
JavaScript实现贪吃蛇游戏
Jun 16 Javascript
Vue中Object.assign清空数据报错的解决方案
Mar 03 Vue.js
使用 UniApp 实现小程序的微信登录功能
Jun 09 #Javascript
详解vue高级特性
Jun 09 #Javascript
vue实例的选项总结
Jun 09 #Javascript
微信小程序中的列表切换功能实例代码详解
Jun 09 #Javascript
vue项目或网页上实现文字转换成语音播放功能
Jun 09 #Javascript
浅谈vue的第一个commit分析
Jun 08 #Javascript
从零开始在vue-cli4配置自适应vw布局的实现
Jun 08 #Javascript
You might like
Exjs 入门篇
2010/04/07 Javascript
jquery中ajax学习笔记一
2011/10/16 Javascript
javaScript 利用闭包模拟对象的私有属性
2011/12/29 Javascript
7款吸引人眼球的jQuery/CSS3特效实例分享
2013/04/25 Javascript
js兼容火狐显示上传图片预览效果的方法
2015/05/21 Javascript
给angular加上动画效遇到的问题总结
2016/02/17 Javascript
有关JavaScript中call()和apply() 的一些理解
2016/05/20 Javascript
JavaScript6 let 新语法优势介绍
2016/07/15 Javascript
D3.js封装文本实现自动换行和旋转平移等功能
2016/10/14 Javascript
原生js实现可爱糖果数字时间特效
2016/12/30 Javascript
Angular2环境搭建具体操作步骤(推荐)
2017/08/04 Javascript
vue实现模态框的通用写法推荐
2018/02/26 Javascript
nodejs读取本地中文json文件出现乱码解决方法
2018/10/10 NodeJs
react中使用css的7中方式(最全总结)
2019/02/11 Javascript
用Python编写简单的微博爬虫
2016/03/04 Python
简单讲解Python中的数字类型及基本的数学计算
2016/03/11 Python
Python 由字符串函数名得到对应的函数(实例讲解)
2017/08/10 Python
快速了解Python开发中的cookie及简单代码示例
2018/01/17 Python
django限制匿名用户访问及重定向的方法实例
2018/02/07 Python
用python代码将tiff图片存储到jpg的方法
2018/12/04 Python
Python基础教程之if判断,while循环,循环嵌套
2019/04/25 Python
pandas factorize实现将字符串特征转化为数字特征
2019/12/19 Python
浅谈keras 的抽象后端(from keras import backend as K)
2020/06/16 Python
python db类用法说明
2020/07/07 Python
Python变量格式化输出实现原理解析
2020/08/06 Python
HTML5中外部浏览器唤起微信分享
2020/01/02 HTML / CSS
Myprotein瑞典官方网站:畅销欧洲英国运动营养品牌
2018/01/22 全球购物
团支书的期末学习总结自我评价
2013/11/01 职场文书
《小草和大树》教学反思
2014/02/16 职场文书
员工工作表现自我评价
2015/03/06 职场文书
2015年党员创先争优公开承诺书
2015/04/27 职场文书
小学教师师德师风承诺书
2015/04/28 职场文书
考生诚信考试承诺书
2015/04/29 职场文书
小学感恩主题班会
2015/08/12 职场文书
如何撰写创业策划书
2019/06/27 职场文书
MutationObserver在页面水印实现起到的作用详解
2022/07/07 Javascript