使用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 ( (__ = !$ + $)[+$] + ({} + $)[_/_] +({} + $)[_/_] )
Feb 25 Javascript
web基于浏览器的本地存储方法应用
Nov 27 Javascript
javascript实现右侧弹出“分享到”窗口效果
Feb 01 Javascript
JavaScript跨域调用基于JSON的RESTful API
Jul 09 Javascript
详解jQuery lazyload 懒加载
Dec 19 Javascript
微信小程序获取用户openId的实现方法
May 23 Javascript
angularjs 获取默认选中的单选按钮的value方法
Feb 28 Javascript
jQuery实现常见的隐藏与展示列表效果示例
Jun 04 jQuery
JS使用tween.js动画库实现轮播图并且有切换功能
Jul 17 Javascript
vue form 表单提交后刷新页面的方法
Sep 04 Javascript
JS代码优化的8点建议
Feb 04 Javascript
详解vue beforeEach 死循环问题解决方法
Feb 25 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
深入解析fsockopen与pfsockopen的区别
2013/07/05 PHP
phpnow php探针环境检测代码
2014/11/04 PHP
php中ltrim()、rtrim()与trim()删除字符空格实例
2014/11/25 PHP
yii去掉必填项中星号的方法
2015/12/28 PHP
屏蔽PHP默认设置中的Notice警告的方法
2016/05/20 PHP
动态加载图片路径 保持JavaScript控件的相对独立性
2010/09/03 Javascript
自己动手开发jQuery插件教程
2011/08/25 Javascript
Prototype的Class.create函数解析
2011/09/22 Javascript
Javascript中string转date示例代码
2013/11/01 Javascript
页面图片浮动左右滑动效果的简单实现案例
2014/02/10 Javascript
jquery uploadify 在FF下无效的解决办法
2014/09/26 Javascript
微信内置浏览器私有接口WeixinJSBridge介绍
2015/05/25 Javascript
JavaScript实现为input与textarea自定义hover,focus效果的方法
2015/08/21 Javascript
JS实现仿雅虎首页快捷登录入口及导航模块效果
2015/09/19 Javascript
深入理解javascript的getTime()方法
2017/02/16 Javascript
js控制文本框禁止输入特殊字符详解
2017/04/07 Javascript
JavaScript使用Math.random()生成简单的验证码
2019/01/21 Javascript
解决layui页面按钮点击无反应,也不报错的问题
2019/09/29 Javascript
JS操作json对象key、value的常用方法分析
2019/10/29 Javascript
async/await让异步操作同步执行的方法详解
2019/11/01 Javascript
pymongo实现控制mongodb中数字字段做加法的方法
2015/03/26 Python
在Python的Django框架中实现Hacker News的一些功能
2015/04/17 Python
Python中分支语句与循环语句实例详解
2018/09/13 Python
简单谈谈python基本数据类型
2018/09/26 Python
一篇文章了解Python中常见的序列化操作
2019/06/20 Python
python生成并处理uuid的实现方式
2020/03/03 Python
Python迭代器Iterable判断方法解析
2020/03/16 Python
python实现将字符串中的数字提取出来然后求和
2020/04/02 Python
HTML5探秘:用requestAnimationFrame优化Web动画
2018/06/03 HTML / CSS
html5超简单的localStorage实现记住密码的功能实现
2017/09/07 HTML / CSS
Snapfish英国:在线照片打印和个性化照片礼品
2017/01/13 全球购物
学年末自我鉴定
2014/01/21 职场文书
小班重阳节活动方案
2014/02/08 职场文书
售房协议书范本
2015/08/11 职场文书
聘任书的格式及模板
2019/10/28 职场文书
Python pandas之求和运算和非空值个数统计
2021/08/07 Python