侧边栏壁纸
博主头像
woku博主等级

学可以无术,但不能不博。 术可以无量,但不能不专。

  • 累计撰写 5 篇文章
  • 累计创建 11 个标签
  • 累计收到 2 条评论

策略模式封装表单验证

woku
2022-11-29 / 2 评论 / 1 点赞 / 204 阅读 / 2,567 字

什么是策略模式?

策略模式就是一个个规则的封装,我们在使用的时候,可以直接使用这个规则。
比如在表单验证中,用户名需要检验是否为空,是否符合字符规范,是否有敏感词。我们可以使用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;
1

评论区