使用组件就像流水线上的工人;设计组件就像设计流水线的人,设计好了给工人使用。
完整项目地址:仿 ElementtUI 实现一个 Form 表单
一. 目标
仿 ElementUI 实现一个简单的 Form 表单,主要实现以下四点:
- Form
- FormItem
- Input
- 表单验证
我们先看一下 ElementUI 中 Form 表单的基本用法
<el-form :model="ruleForm" :rules="rules" ref="loginForm"> <el-form-item label="用户名" prop="name"> <el-input v-model="ruleForm.name"></el-input> </el-form-item> <el-form-item label="密码" prop="pwd"> <el-input v-model="ruleForm.pwd"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('loginForm')">登录</el-button> </el-form-item> </el-form>
在 ElementUI 的表单中,主要进行了 3 层嵌套关系, Form
是最外面一层, FormItem
是中间一层,最内层是 Input
或者 Button
。
二. 创建项目
我们通过 Vue CLI 3.x
创建项目。
使用 vue create e-form
创建一个目录。
使用 npm run serve
启动项目。
三. Form 组件设计
ElementUI 中的表单叫做 el-form
,我们设计的表单就叫 e-form
。
为了实现 e-form
表单,我们参考 ElementUI 的表单用法,总结出以下我们需要设计的功能。
- e-form 负责全局校验,并提供插槽;
- e-form-item 负责单一项校验及显示错误信息,并提供插槽;
- e-input 负责数据双向绑定;
1. Input 的设计
我们首先观察一下 ElementUI 中的 Input
组件:
<el-input v-model="ruleForm.name"></el-input>
在上面的代码中,我们发现 input
标签可以实现一个双向数据绑定,而实现双向数据绑定需要我们在 input
标签上做两件事。
- 要绑定 value
- 要响应 input 事件
当我们完成这两件事以后,我们就可以完成一个 v-model
的语法糖了。
我们创建一个 Input.vue 文件:
<template> <div> <!-- 1. 绑定 value 2. 响应 input 事件 --> <input type="text" :value="valueInInput" @input="handleInput"> </div> </template> <script> export default { name: "EInput", props: { value: { // 解释一 type: String, default: '', } }, data() { return { valueInInput: this.value // 解释二 }; }, methods: { handleInput(event) { this.valueInInput = event.target.value; // 解释三 this.$emit('input', this.valueInInput); // 解释四 } }, }; </script>
我们对上面的代码做一点解释:
**解释一:**既然我们想做一个 Input
组件,那么接收的值必然是父组件传进来的,并且当父组件没有传进来值的时候,我们可以它一个默认值 ""
。
**解释二:**我们在设计组件的时候,要遵循单向数据流的原则:父组件传进来的值,我们只能用,不能改。那么将父组件传进来的值进行一个赋值操作,赋值给 Input
组件内部的 valueInInput
,如果这个值发生变动,我们就修改内部的值 valueInInput
。这样我们既可以处理数据的变动,又不会直接修改父组件传进来的值。
**解释三:**当 Input
中的值发生变动时,触发 @input
事件,此时我们通过 event.target.value
获取到变化后的值,将它重新赋值给内部的 valueInInput
。
**解释四:**完成了内部赋值之后,我们需要做的就是将变化后的值通知父组件,这里我们用 this.$emit
向上派发事件。其中第一个参数为事件名,第二个参数为变化的值。
完成了以上四步,一个实现了双向数据绑定的简单的 Input
组件就设计完成了。此时我们可以在 App.vue 中引入 Input
组件观察一下结果。
<template> <div id="app"> <e-input v-model="initValue"></e-input> <div>{{ initValue }}</div> </div> </template> <script> import EInput from './components/Input.vue'; export default { name: "app", components: { EInput }, data() { return { initValue: '223', }; }, }; </script>
2. FormItem 的设计
<el-form-item label="用户名" prop="name"> <el-input v-model="ruleForm.name"></el-input> </el-form-item>
在 ElementUI 的 formItem
中,我们可以看到:
- 需要
label
来显示名称; - 需要
prop
来校验当前项; - 需要给
input
或button
预留插槽;
根据上面的需求,我们可以创建出自己的 formItem
,新建一个 FormItem.vue 文件 。
<template> <div> <!-- 解释一 --> <label v-if="label">{{ label }}</label> <div> <!-- 解释二 --> <slot></slot> <!-- 解释三 --> <p v-if="validateState === 'error'" class="error">{{ validateMessage }}</p> </div> </div> </template> <script> export default { name: "EFormItem", props: { label: { type: String, default: '' }, prop: { type: String, default: '' } }, data() { return { validateState: '', validateMessage: '' } }, } </script> <style scoped> .error { color: red; } </style>
和上面一样,我们接着对上面的代码进行一些解释:
**解释一:**根据 ElementUI 中的用法,我们知道 label
是父组件传来,且当传入时我们展示,不传入时不展示。
解释二: slot
是一个预留的槽位,我们可以在其中放入 input
或其他组件、元素。
解释三: p
标签是用来展示错误信息的,如果验证状态为 error
时,就显示。
此时,我们的 FormItem
组件也可以使用了。同样,我们在 App.vue 中引入该组件。
<template> <div id="app"> <e-form-item label="用户名" prop="name"> <e-input v-model="ruleForm.name"></e-input> </e-form-item> <e-form-item label="密码" prop="pwd"> <e-input v-model="ruleForm.pwd"></e-input> </e-form-item> <div> {{ ruleForm }} </div> </div> </template> <script> import EInput from './components/Input.vue'; import EFormItem from './components/FormItem.vue'; export default { name: "app", components: { EInput, EFormItem }, data() { return { ruleForm: { name: '', pwd: '', }, }; }, }; </script>
3. Form 的设计
到现在,我们已经完成了最内部的 input
以及中间层的 FormItem
的设计,现在我们开始设计最外层的 Form
组件。
当层级过多并且组件间需要进行数据传递时,Vue 为我们提供了 provide
和 inject
API,方便我们跨层级传递数据。
我们举个例子来简单实现一下 provide
和 inject
。在 App.vue 中,我们提供数据(provide)。
export default { name: "app", provide() { return { msg: '哥是最外层提供的数据' } } }; </script>
接着,我们在最内层的 Input.vue 中注入数据,观察结果。
<template> <div> <!-- 1、绑定 value 2、响应 input 事件--> <input type="text" :value="valueInInput" @input="handleInput"> <div>{{ msg }}</div> </div> </template> <script> export default { name: "EInput", inject: [ 'msg' ], props: { value: { type: String, default: '', } }, data() { return { valueInInput: this.value }; }, methods: { handleInput(event) { this.valueInInput = event.target.value; this.$emit('input', this.valueInInput); } }, }; </script>
根据上图,我们可以看到无论跨越多少层级, provide
和 inject
可以非常方便的实现数据的传递。
理解了上面的知识点后,我们可以开始设计 Form
组件了。
<el-form :model="ruleForm" :rules="rules" ref="loginForm"> </el-form>
根据 ElementUI 中表单的用法,我们知道 Form
组件需要实现以下功能:
- 提供数据模型 model;
- 提供校验规则 rules;
- 提供槽位,里面放我们的 FormItem 等组件;
根据上面的需求,我们创建一个 Form.vue 组件:
<template> <form> <slot></slot> </form> </template> <script> export default { name: 'EForm', props: { // 解释一 model: { type: Object, required: true }, rules: { type: Object } }, provide() { // 解释二 return { eForm: this // 解释三 } } } </script>
解释一:该组件需要用户传递进来一个数据模型 model
进来,类型为 Object
。 rules
为可传项。
解释二:为了让各个层级都能使用 Form
中的数据,需要依靠 provide
函数提供数据。
解释三:直接将组件的实例传递下去。
完成了 Form
组件的设计,我们在 App.vue 中使用一下:
<template> <div id="app"> <e-form :model="ruleForm" :rules="rules"> <e-form-item label="用户名" prop="name"> <e-input v-model="ruleForm.name"></e-input> </e-form-item> <e-form-item label="密码" prop="pwd"> <e-input v-model="ruleForm.pwd"></e-input> </e-form-item> <e-form-item> <button>提交</button> </e-form-item> </e-form> </div> </template> <script> import EInput from './components/Input.vue'; import EFormItem from './components/FormItem.vue'; import EForm from "./components/Form"; export default { name: "app", components: { EInput, EFormItem, EForm }, data() { return { ruleForm: { name: '', pwd: '', }, rules: { name: [{ required: true }], pwd: [{ required: true }] }, }; }, }; </script>
到目前为止,我们的基本功能就已经实现了,除了提交与验证规则外,所有的组件几乎与 ElementUI 中的表单一模一样了。下面我们就开始实现校验功能。
4. 设计校验规则
在上面设计的组件中,我们知道校验当前项和展示错误信息的工作是在 FormItem
组件中,但是数据的变化是在 Input
组件中,所以 FormItem
和 Input
组件是有数据传递的。当 Input
中的数据变化时,要告诉 FormItem
,让 FormItem
进行校验,并展示错误。
首先,我们修改一下 Input
组件:
methods: { handlerInput(event) { this.valueInInput = event.target.value; this.$emit("input", this.valueInInput); // 数据变了,定向通知 FormItem 校验 this.dispatch('EFormItem', 'validate', this.valueInput); }, // 查找指定 name 的组件, dispatch(componentName, eventName, params) { var parent = this.$parent || this.$root; var name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } } }
这里,我们不能用 this.$emit
直接派发事件,因为在 FormItem
组件中, Input
组件的位置只是一个插槽,无法做事件监听,所以此时我们让 FormItem
自己派发事件,并自己监听。修改 FormItem
组件,在 created
中监听该事件。
created() { this.$on('validate', this.validate); }
当 Input
组件中的数据变化时, FormItem
组件监听到 validate
事件后,执行 validate
函数。
下面,我们就要处理我们的 validate
函数了。而在 ElementUI 中,验证用到了一个底层库async-validator,我们可以通过 npm
安装这个包。
npm i async-validator
async-validator
是一个可以对数据进行异步校验的库,具体的用法可以参考上面的链接。我们通过这个库来完成我们的 validate
函数。继续看 FormItem.vue 这个文件:
<template> <div> <label v-if="label">{{ label }}</label> <div> <slot></slot> <p v-if="validateState === 'error' " class="error">{{ validateMessage }}</p> </div> </div> </template> <script> import AsyncValidator from "async-validator"; export default { name: "EFormItem", props: { label: { type: String, default: '' }, prop: { type: String, default: '' } }, inject: ["eForm"], // 解释一 created() { this.$on("validate", this.validate); }, mounted() { // 解释二 if (this.prop) { // 解释三 this.dispatch('EForm', 'addFiled', this); } }, data() { return { validateMessage: "", validateState: "" }; }, methods: { validate() { // 解释四 return new Promise(resolve => { // 解释五 const descriptor = { // name: this.form.rules.name => // name: [ { require: true }, { ... } ] }; descriptor[this.prop] = this.eForm.rules[this.prop]; // 校验器 const validator = new AsyncValidator(descriptor); const model = {}; model[this.prop] = this.eForm.model[this.prop]; // 异步校验 validator.validate(model, errors => { if (errors) { this.validateState = "error"; this.validateMessage = errors[0].message; resolve(false); } else { this.validateState = ""; this.validateMessage = ""; resolve(true); } }); }); }, // 查找上级指定名称的组件 dispatch(componentName, eventName, params) { var parent = this.$parent || this.$root; var name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } } } }; </script> <style scoped> .error { color: red; } </style>
我们对上面的代码做一个解释。
解释一:注入 Form
组件提供的数据 - Form
组件的实例,下面就可以使用 this.eForm.xxx
来使用 Form
中的数据了。
解释二:因为我们需要在 Form
组件中校验所有的 FormItem
,所以当 FormItem
挂载完成后,需要派发一个事件告诉 Form
:你可以校验我了。
解释三:当 FormItem
中有 prop
属性的时候才校验,没有的时候不校验。比如提交按钮就不需要校验。
<e-form-item> <input type="submit" @click="submitForm()" value="提交"> </e-form-item>
**解释四:**返回一个 promise 对象,批量处理所有异步校验的结果。
解释五: descriptor
对象是 async-validator
的用法,采用键值对的形式,用来检查当前项。比如:
// 检查当前项 // async-validator 给出的例子 name: { type: "string", required: true, validator: (rule, value) => value === 'muji', }
FormItem
中检查当前项完成了,现在我们需要处理一下 Form
组件中的全局校验。表单提交时,需要对 form
进行一个全局校验。大致的思路是:循环遍历表单中的所有派发上来的 FormItem
,让每一个 FormItem
执行自己的校验函数,如果有一个为 false
,则校验不通过;否则,校验通过。我们通过代码实现一下:
<template> <form> <slot></slot> </form> </template> <script> export default { props: { model: { type: Object, required: true }, rules: { type: Object } }, provide() { return { eForm: this, // provide this component's instance } }, data() { return { fileds: [], } }, created() { // 解释一 this.fileds = []; this.$on('addFiled', filed => this.fileds.push(filed)); }, methods: { async validate(cb) { // 解释二 // 解释三 const eachFiledResultArray = this.fileds.map(filed => filed.validate()); // 解释四 const results = await Promise.all(eachFiledResultArray); let ret = true; results.forEach(valid => { if (!valid) { ret = false; } }); cb(ret); } }, } </script> <style lang="scss" scoped> </style>
解释一:用 fileds
缓存需要校验的表单项,因为我们在 FormItem
中派发了事件。只有需要校验的 FormItem
会被派发到这里,而且都会保存在数组中。
if (this.prop) { this.dispatch('EForm', 'addFiled', this); }
解释二:当点击提交按钮时,会触发这个事件。
解释三:遍历所有被添加到 fileds
中的 FormItem
项,让每一项单独去验证,会返回 Promise 的 true
或 false
。将所有的结果,放在一个数组 eachFiledResultArray
中。
解释四:获取所有的结果,统一进行处理,其中有一个结果为 false
,验证就不能通过。
至此,一个最简化版本的仿 ElementUI 的表单就实现了。
四. 总结
当然上面的代码还有很多可以优化的地方,比如说 dispatch
函数,我们可以写一遍,使用的时候用 mixin
导入。由于篇幅关系,这里就不做处理了。
通过这次实现,我们首先总结一下其中所涉及的知识点。
- 父组件传递给子组件用 props
- 子组件派发事件,用 $emit
- 跨层级数据交互,用 provide 和 inject
- 用 slot 可以预留插槽
其次是一些思想:
- 单项数据流:父组件传递给子组件的值,子组件内部只能用,不能修改。
- 组件内部的 name 属性,可以通过 this.$parent.$options.name 查找。
- 想要批量处理很多异步的结果,可以用 promise 对象。
最后,文章会首先发布在我的 Github ,以及公众号上,欢迎关注,欢迎 star。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?
更新日志
- 小骆驼-《草原狼2(蓝光CD)》[原抓WAV+CUE]
- 群星《欢迎来到我身边 电影原声专辑》[320K/MP3][105.02MB]
- 群星《欢迎来到我身边 电影原声专辑》[FLAC/分轨][480.9MB]
- 雷婷《梦里蓝天HQⅡ》 2023头版限量编号低速原抓[WAV+CUE][463M]
- 群星《2024好听新歌42》AI调整音效【WAV分轨】
- 王思雨-《思念陪着鸿雁飞》WAV
- 王思雨《喜马拉雅HQ》头版限量编号[WAV+CUE]
- 李健《无时无刻》[WAV+CUE][590M]
- 陈奕迅《酝酿》[WAV分轨][502M]
- 卓依婷《化蝶》2CD[WAV+CUE][1.1G]
- 群星《吉他王(黑胶CD)》[WAV+CUE]
- 齐秦《穿乐(穿越)》[WAV+CUE]
- 发烧珍品《数位CD音响测试-动向效果(九)》【WAV+CUE】
- 邝美云《邝美云精装歌集》[DSF][1.6G]
- 吕方《爱一回伤一回》[WAV+CUE][454M]