使用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 相关文章推荐
总结一些js自定义的函数
Aug 05 Javascript
用js计算页面执行时间的函数
Dec 07 Javascript
Jquery数独游戏解析(一)-页面布局
Nov 05 Javascript
html a标签-超链接中confirm方法使用介绍
Jan 04 Javascript
JS验证控制输入中英文字节长度(input、textarea等)具体实例
Jun 21 Javascript
JavaScript检测上传文件大小的方法
Jul 22 Javascript
AngularJS bootstrap启动详解及实例代码
Sep 14 Javascript
js正则表达式惰性匹配和贪婪匹配用法分析
Dec 26 Javascript
JS实现带动画的回到顶部效果
Dec 28 Javascript
JavaScript函数节流和函数去抖知识点学习
Jul 31 Javascript
Element Dropdown下拉菜单的使用方法
Jul 26 Javascript
vue添加自定义右键菜单的完整实例
Dec 08 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
PHP callback函数使用方法和注意事项
2015/01/23 PHP
jQuery列表拖动排列具体实现
2013/11/04 Javascript
js中indexof的用法详细解析
2013/12/24 Javascript
写出高效jquery代码的19条指南
2014/03/19 Javascript
jquery+CSS3模拟Path2.0动画菜单效果代码
2015/08/31 Javascript
javascript字符串替换函数如何一次性全部替换掉
2015/10/30 Javascript
GitHub上一些实用的JavaScript的文件压缩解压缩库推荐
2016/03/13 Javascript
javascript图片预览和上传(兼容IE)
2017/03/15 Javascript
vue2.0 资源文件assets和static的区别详解
2018/04/08 Javascript
讲解vue-router之命名路由和命名视图
2018/05/28 Javascript
angularjs下ng-repeat点击元素改变样式的实现方法
2018/09/12 Javascript
vue单页面应用打开新窗口显示跳转页面的实例
2018/09/21 Javascript
Vue表单输入绑定的示例代码
2018/11/01 Javascript
javascript中floor使用方法总结
2019/02/02 Javascript
微信小程序引入VANT组件的方法步骤
2019/09/19 Javascript
js布局实现单选按钮控件
2020/01/17 Javascript
vuex入门最详细整理
2020/03/04 Javascript
Node.js API详解之 net模块实例分析
2020/05/18 Javascript
解决Nuxt使用axios跨域问题
2020/07/06 Javascript
[48:39]Ti4主赛事胜者组第一天 EG vs NEWBEE 2
2014/07/19 DOTA
python服务器与android客户端socket通信实例
2014/11/12 Python
详细分析python3的reduce函数
2017/12/05 Python
Python+selenium实现截图图片并保存截取的图片
2018/01/05 Python
使用python爬虫实现网络股票信息爬取的demo
2018/01/05 Python
对numpy数据写入文件的方法讲解
2018/07/09 Python
python版DDOS攻击脚本
2019/06/12 Python
CSS3中background-clip和background-origin的区别示例介绍
2014/03/10 HTML / CSS
韩国最大的购物网站:Gmarket
2019/06/20 全球购物
《听鱼说话》教学反思
2014/02/15 职场文书
调查研究项目计划书
2014/04/29 职场文书
2015年社区卫生工作总结
2015/04/21 职场文书
python 实现定时任务的四种方式
2021/04/01 Python
苹果电脑mac os中货币符号快捷输入
2022/02/17 杂记
Golang 结构体数据集合
2022/04/22 Golang
Windows Server 2012 R2 磁盘分区教程
2022/04/29 Servers
httpclient调用远程接口的方法
2022/08/14 Java/Android