什么是策略模式?
策略模式就是一个个规则的封装,我们在使用的时候,可以直接使用这个规则。
比如在表单验证中,用户名需要检验是否为空,是否符合字符规范,是否有敏感词。我们可以使用if/else来检验,不过这样写代码的可维护性和扩展性就不好了,如果表单有几百个字段,每个字段有很多检验规则,导致维护性很差。
可以把每种检验规则都用策略模式单独封装起来,需要用哪种策略直接拿策略名称使用就行。
原生JS策略模式封装表单验证
html
结构如下:
<body>
<div class="form-wrapper">
<p>
<input type="text" class="form-item" name="username" placeholder="username"
/>
</p>
<p>
<input type="password" name="password" class="form-item" placeholder="password" />
</p>
<p>
<input type="radio" name="gender" class="form-item" value="male" checked>Male
<input type="radio" name="gender" class="form-item" value="female">FeMale
</p>
<p>
<select name="job" class="form-item">
<option value="">请选择</option>
<option value="web">web前端</option>
<option value="java">java后端</option>
</select>
</p>
<p>
<input type="checkbox" name="like" value="codding" class="form-item">codding
<input type="checkbox" name="like" value="reading" class="form-item">reading
<input type="checkbox" name="like" value="cokking" class="form-item">cokking
</p>
<p>
<textarea name="intro" class="form-item" cols="30" rows="10"></textarea>
</p>
<p>
<button type="submit" class="form-item">提交</button>
</p>
</div>
<script src="./index.js" type="module"></script>
</body>
我们先来准备数据和策略:
每一个表单元素的name就对应的策略模式中的每一个key
根据key
来拿到对应表单元素的检验规则,调用规则,传入表单项的值,返回的reg
可以得到这个表单项检验是否成功。
const formData = {
username: '',
password: '',
gender: '',
job: '',
like: [],
intro: ''
}
const validates = {
username: (value) => ({
reg: value.length > 3 && value.length < 10,
msg: 'username error'
}),
password: (value) => ({
reg: value.length < 10,
msg: 'password error'
}),
job: (value) => ({
reg: value.length > 0,
msg: 'job must select'
}),
like: (value) => ({
reg: value.length > 0,
msg: 'like must select'
}),
intro: (value) => ({
reg: value.length < 100,
msg: 'intro error'
}),
}
我们声明一个FormCheck
类来处理
将数据传入,策略集合传入,某个字段检验成功我要干的pass
回调,和检验不成功的我要干的noPass
回调,还有整个表单提交成功的时候(字段都检验成功),我要做的onSubmit
回调
new Validator('.form-wrapper', '.form-item', {
formData,
validates,
pass: (key, value) => {
console.log('pass', key, value)
},
noPass: (key, value, msg) => {
console.log('nopass', msg)
},
onSubmit: (data) => {
console.log('submitok', data)
}
})
- 创建类
Validator
class Validator {
constructor(el, itemEl, { formData, validates, pass, noPass, onSubmit }) {
// 拿到所有表单项元素-后面要给每个表单项绑定事件
this.app = document.querySelector(el)
this.itemEls = this.app.querySelectorAll(itemEl)
this.formData = formData
this.validates = validates
// 保存回调,合适的时机执行
this.pass = pass
this.noPass = noPass
this.onSubmit = onSubmit
this.init()
}
}
- 表单项绑定事件,在触发事件的时候,给对应的数据设置值
在绑定事件时,因为不同的表单元素的事件是不同的,比如text是input事件,而select是change事件
需要建立一个MAP来保存
const EVENT_MAPS = {
text: 'input',
password: 'input',
radio: 'click',
checkbox: 'click',
'select-one': 'change',
textarea: 'input',
submit: 'click'
}
// key分别表示每个表单元素的type
// type="text"的是input事件
// type="password"的是input事件
// type="radio"的是click事件
setValue
设置值,注意区分不同的表单元素。
const EVENT_MAPS = {
text: 'input',
password: 'input',
radio: 'click',
checkbox: 'click',
'select-one': 'change',
textarea: 'input',
submit: 'click'
}
class Validator {
constructor(el, itemEl, { formData, validates, pass, noPass, onSubmit }) {
this.app = document.querySelector(el)
this.itemEls = this.app.querySelectorAll(itemEl)
this.formData = formData
this.validates = validates
this.pass = pass
this.noPass = noPass
this.onSubmit = onSubmit
this.result = []
this.init()
}
init() {
this.bindEvent()
}
bindEvent() {
this.itemEls.forEach(el => {
const { type } = el
el.addEventListener(EVENT_MAPS[type], this.setValue.bind(this, el))
});
}
setValue(el) {
const { type, name, value } = el
switch (type) {
case 'submit':
this.onSubmitHandleClick()
break;
case 'checkbox':
if (this.formData[name].includes(value)) {
this.formData[name] = this.formData[name].filter(x => x != value)
} else {
this.formData[name] = [...this.formData[name], value]
}
break;
default:
this.formData[name] = value
break;
}
}
}
- 在进行
setValue
时,拦截数据的set。做检验 - 使用
proxy
代理来进行拦截
this.formData = proxyFormData(formData)
在setValue
中进行this.formData[name] = value 时候,就会被proxy
代理进行拦截。
更改constructor
this.formData = formData 变为 this.formData = this.proxyFormData(formData)
class Validator {
constructor(el, itemEl, { formData, validates, pass, noPass, onSubmit }) {
this.app = document.querySelector(el)
this.itemEls = this.app.querySelectorAll(itemEl)
this.formData = this.proxyFormData(formData)
this.validates = validates
this.pass = pass
this.noPass = noPass
this.onSubmit = onSubmit
this.result = []
this.init(formData)
}
}
proxyFormData(data) {
return new Proxy(data, {
get: (target, key) => {
return Reflect.get(target, key)
},
set: (target, key, value) => {
this.validate(key, value)
return Reflect.set(target, key, value)
}
})
}
validate(key, value) {
const keyValidator = this.validates[key]
if (keyValidator) {
const {reg, msg} = keyValidator(value)
if (!reg) {
// 该表单项检验不通过,执行noPass回调
this.noPass(key, value, msg)
return
}
// 该表单项检验通过,执行pass回调
this.pass(key, value)
}
}
- 在提交整个表单时,我们要检验整个表单是否通过(提交按钮在前面已经绑定了
click
事件,在setValue
中如果type是submit
,就执行onSubmitHandleClick
)
整个表单是否通过这个数据,我们可以用一个容器
存储,里面的每一个key
就是表单项name
,该项通过,对应的key
就是true
,否则就是false
更改constructor
新增 this.result = {}
class Validator {
constructor(el, itemEl, { formData, validates, pass, noPass, onSubmit }) {
this.app = document.querySelector(el)
this.itemEls = this.app.querySelectorAll(itemEl)
this.formData = this.proxyFormData(formData)
this.validates = validates
this.pass = pass
this.noPass = noPass
this.onSubmit = onSubmit
this.result = {}
this.init(formData)
}
init(formData) {
this.addResult(formData)
this.bindEvent()
}
// init时要给每个key都设置为flase
addResult(data) {
for(let k in data) {
if (this.validates[k]) {
this.result[k] = false
}
}
}
// set
setResult(key, bool) {
this.result[key] = bool
}
}
- 最后在提交时,去看
this.result
中的值,找出不通过的项,执行对应的noPass
回调。如果整个表单都通过,执行onSubmit
回调。
onSubmitHandleClick() {
const notPassIndex = Object.values(this.result).findIndex(x => !x)
if (notPassIndex !== -1) {
const falseKey = Object.keys(this.result)[notPassIndex]
const { msg } = this.validates[falseKey](this.formData[falseKey])
this.noPass(falseKey, this.formData[falseKey], msg )
return
}
this.onSubmit(JSON.parse(JSON.stringify(this.formData)))
}
- 整体代码
const EVENT_MAPS = {
text: 'input',
password: 'input',
radio: 'click',
checkbox: 'click',
'select-one': 'change',
textarea: 'input',
submit: 'click'
}
class Validator {
constructor(el, itemEl, { formData, validates, pass, noPass, onSubmit }) {
this.app = document.querySelector(el)
this.itemEls = this.app.querySelectorAll(itemEl)
this.formData = this.proxyFormData(formData)
this.validates = validates
this.pass = pass
this.noPass = noPass
this.onSubmit = onSubmit
this.result = {}
this.init(formData)
}
init(formData) {
this.addResult(formData)
this.bindEvent()
}
proxyFormData(data) {
return new Proxy(data, {
get: (target, key) => {
return Reflect.get(target, key)
},
set: (target, key, value) => {
this.validate(key, value)
return Reflect.set(target, key, value)
}
})
}
validate(key, value) {
const keyValidator = this.validates[key]
if (keyValidator) {
const {reg, msg} = keyValidator(value)
if (!reg) {
this.noPass(key, value, msg)
this.setResult(key, false)
return
}
this.setResult(key, true)
this.pass(key, value)
}
}
onSubmitHandleClick() {
const notPassIndex = Object.values(this.result).findIndex(x => !x)
if (notPassIndex !== -1) {
const falseKey = Object.keys(this.result)[notPassIndex]
const { msg } = this.validates[falseKey](this.formData[falseKey])
this.noPass(falseKey, this.formData[falseKey], msg )
return
}
this.onSubmit(JSON.parse(JSON.stringify(this.formData)))
}
addResult(data) {
for(let k in data) {
if (this.validates[k]) {
this.result[k] = false
}
}
}
setResult(key, bool) {
this.result[key] = bool
}
bindEvent() {
this.itemEls.forEach(el => {
const {type} = el
el.addEventListener(EVENT_MAPS[type], this.setValue.bind(this, el))
});
}
setValue(el) {
const { type, name, value } = el
switch (type) {
case 'submit':
this.onSubmitHandleClick()
break;
case 'checkbox':
if (this.formData[name].includes(value)) {
this.formData[name] = this.formData[name].filter(x => x != value)
} else {
this.formData[name] = [...this.formData[name], value]
}
break;
default:
this.formData[name] = value
break;
}
}
}
export default Validator
vue3策略模式封装表单验证
使用
FormCheck.create
静态方法来实例化的。
在watch的时候,去检验每一项。
<template>
<div class="login-form">
<div class="content">
<el-form :model="loginForm" v-if="activeName == 'account'">
<el-form-item prop="userName">
<el-input v-model="loginForm.userName" name="username" placeholder="请输入账号/手机号"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" name="password" type="password" placeholder="请输入登录密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" id="submit-btn">登 录</el-button>
</el-form-item>
<el-form-item class="other-opt"> </el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import { userValidate } from "@/validates";
import FormCheck from "@/utils/FormCheck";
const data = {
userName: "",
password: ""
};
const loginForm = FormCheck.create("#submit-btn", {
formData: data,
validates: userValidate,
pass(key, value) {
console.log(key, value);
},
noPass(key, value, msg) {
console.log(key, value, msg);
},
handleSubmit(data) {
console.log(data);
}
});
</script>
<style lang="scss" scoped>
.login-form {
background-color: #fff;
.content {
padding: 58px;
box-sizing: border-box;
}
.el-form {
padding-top: 40px;
.el-form-item {
margin-bottom: 20px;
}
.login-btn {
width: 100%;
height: 47px;
font-size: 18px;
}
}
::v-deep .el-input__inner {
height: 50px;
}
}
</style>
const userValidate = {
userName: (value) => ({
reg: value.length < 10,
msg: "用户名格式不正确"
}),
password: (value) => ({
reg: value.length < 6,
msg: "密码格式不正确"
})
};
export default userValidate;
import { onMounted, reactive, toRaw, watch } from "vue";
class FormCheck {
constructor(wrapper, { formData, validates, pass, noPass, handleSubmit }) {
this.wrapper = wrapper;
FormCheck.formData = reactive(formData);
this.validates = validates;
this.pass = pass;
this.noPass = noPass;
this.handleSubmit = handleSubmit;
this.result = {};
this.init();
}
init() {
this.addWatcher();
onMounted(this.bindEvent);
}
bindEvent = () => {
const submitEl = document.querySelector(this.wrapper);
submitEl.addEventListener("click", this.handleSubmitClick.bind(this), false);
};
addWatcher() {
for (const k in FormCheck.formData) {
const validateVal = this.validates[k];
if (validateVal) {
this.addResult(k);
watch(
() => FormCheck.formData[k],
(newValue) => {
const { reg, msg } = validateVal(newValue);
if (reg) {
this.setResult(k, true);
this.pass(k, newValue);
} else {
this.setResult(k, false);
this.noPass(k, newValue, msg);
}
}
);
}
}
}
addResult(key) {
this.result[key] = false;
}
setResult(key, flag) {
this.result[key] = flag;
}
handleSubmitClick() {
const formData = FormCheck.formData;
const noPassKeys = Object.keys(this.result).filter((x) => !this.result[x]);
noPassKeys.forEach((key) => {
const { msg } = this.validates[key](this.result[key]);
const value = formData[key];
this.noPass(key, value, msg);
});
if (!noPassKeys.length) {
this.handleSubmit(toRaw(formData));
}
}
static create(wrapper, { formData, validates, pass, noPass, handleSubmit }) {
// eslint-disable-next-line no-new
new FormCheck(wrapper, {
formData,
validates,
pass,
noPass,
handleSubmit
});
return FormCheck.formData;
}
}
export default FormCheck;
评论区