使用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 相关文章推荐
jquery 学习之二 属性 文本与值(text,val)
Nov 25 Javascript
javascript实现超炫的向上滑行菜单实例
Aug 03 Javascript
浅谈EasyUI常用控件的禁用方法
Nov 09 Javascript
jQuery中hover方法搭配css的hover选择器,实现选中元素突出显示方法
May 08 jQuery
Vue中Quill富文本编辑器的使用教程
Sep 21 Javascript
微信小程序从注册账号到上架(图文详解)
Jul 17 Javascript
vue实现一拉到底的滑动验证
Jul 25 Javascript
JS函数基本定义与用法示例
Jan 15 Javascript
微信小程序实现录制、试听、上传音频功能(带波形图)
Feb 27 Javascript
使用vue打包进行云服务器上传的问题
Mar 02 Javascript
Vue 实现可视化拖拽页面编辑器
Feb 01 Vue.js
Echarts如何重新渲染实例详解
May 30 Javascript
使用 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中通过正则表达式下载内容中的远程图片的函数代码
2012/01/10 PHP
PHP中3种生成XML文件方法的速度效率比较
2012/10/06 PHP
php while循环得到循环次数
2013/10/26 PHP
php获得客户端浏览器名称及版本的方法(基于ECShop函数)
2015/12/23 PHP
php实现统计目录文件大小的函数
2015/12/25 PHP
关于setInterval、setTimeout在jQuery中的使用注意事项
2011/09/28 Javascript
jQuery图片的展开和收缩实现代码
2013/04/16 Javascript
javascript中创建对象的几种方法总结
2013/11/01 Javascript
JS 数字转换研究总结
2013/12/26 Javascript
Jquery创建层显示标题和内容且随鼠标移动而移动
2014/01/26 Javascript
了解Javascript的模块化开发
2015/03/02 Javascript
js行号显示的文本框实现效果(兼容多种浏览器 )
2015/10/23 Javascript
jQuery+CSS实现滑动的标签分栏切换效果
2015/12/17 Javascript
js console.log打印对像与数组用法详解
2016/01/21 Javascript
分分钟玩转Vue.js组件(二)
2017/03/01 Javascript
利用10行js代码实现上下滚动公告效果
2017/12/08 Javascript
vue展示dicom文件医疗系统的实现代码
2018/08/27 Javascript
JS出现404错误原理及解决方案
2020/07/01 Javascript
ptyhon实现sitemap生成示例
2014/03/30 Python
在Python的循环体中使用else语句的方法
2015/03/30 Python
python+django加载静态网页模板解析
2017/12/12 Python
OpenCV2.3.1+Python2.7.3+Numpy等的配置解析
2018/01/05 Python
解决pandas .to_excel不覆盖已有sheet的问题
2018/12/10 Python
python itchat给指定联系人发消息的方法
2019/06/11 Python
详解python的变量缓存机制
2021/01/24 Python
荷兰之家英文站:Holland at Home
2016/10/26 全球购物
大唐面试试题(CPU,UNIX等等)
2012/01/11 面试题
中层干部岗位职责
2013/12/18 职场文书
《夏夜多美》教学反思
2014/02/17 职场文书
运动会闭幕式解说词
2014/02/21 职场文书
卫校毕业生个人自我鉴定
2014/04/28 职场文书
优秀护士演讲稿
2014/04/30 职场文书
中秋晚会活动方案
2014/08/31 职场文书
酒店餐厅2014重阳节活动策划方案
2014/09/16 职场文书
2014年测量员工作总结
2014/12/12 职场文书
SQL实现LeetCode(180.连续的数字)
2021/08/04 MySQL