javaScript

前端表单提交的那些事

# 1.数据处理

当表单在视图所展示的数据并不是后端需要的数据,或者后端返回的数据不是前端所要展示的内容,这时就需要进行数据转换,下面介绍几种常见的场景 我假设有下面的一组 form 基础数据

 data(){
    return {
      form:{
        name: '商品名称',
        id: '订单编号',
        nickName: '商品别名',
        num: '商品数量',
        price:'价格',
        tag: '0' // 1 表示特价  0 表示无特价
      },
    }
 },

复制代码

  • 1.1 场景 1 :过滤我不要的数据 场景:当前端 form 中的数据存在冗余的字段,也就是说后端并不需要这些字段,我们可以通过过滤把不必要的字段筛选掉
const noRequired = ['tag', 'nickName']; //不需要的字段
const formData = Object.keys(this.form)
  .filter(each => !noRequired.includes(each))
  .reduce((acc, key) => ((acc[key] = this.form[key]), acc), {});
  • 1.2 场景 2:只提取我要的数据 场景:后端不需要表单数据那么多数据,只需要一部分时可以用
const formData = JSON.parse(JSON.stringify(this.form, ['nickName', 'price']));
  • 1.3 场景 3 :覆盖数据 场景:当前表单有部分字段需要替换或覆盖新的数据时可用
Object.assign(this.form, {
  tag: '商品1'
}
  • 1.4 场景 4 :字段映射 当前表单字段需要映射为其他字段名称时可用,如下对应的 name 的 key 值换为 Name
//单个字段映射情况
const formData = JSON.parse(
      JSON.stringify(this.form).replace(
        /name/g,
        'Name')
);

//多字段映射情况
const mapObj = {
      name: "Name",
      nickName: "NickName",
      tag: "Tag"
    };

const formData = JSON.parse(
      JSON.stringify(this.form).replace(
        /name|nickName|tag/gi,
        matched => mapObj[matched])
   );

ps:这种方式有bug,你知道是什么吗?
  • 1.5 场景 5 : 数据映射 当字段存在 0,1 等状态数,需要转换成为相对应的表示时可用,如下对应的 tag 字段,0 对应特价,1 对应无特价,进行映射转换
const formData = JSON.parse(
  JSON.stringify(this.form, (key, value) => {
    if (key == 'tag') {
      return ['特价', '无特价'][value];
    }
    return value;
  })
);
  • 1.6 场景 6: 数据合并 数据合并,将表单数据字段合并,注意的是,如果字段相同,会覆盖前面表单数据字段的数值
const query = { tenaId: '订单编号', id: '查询ID' };
const formData = {
  ...this.form,
  query
};
  • 1.7 把集合的元素按照 key 归类,key 由传入的参数返回。
groupBy()
const arr = [
    {name: '小孙', age: 18, score: 60, weight: 60},
    {name: '小王', age: 19, score: 70, weight: 55},
    {name: '小李', age: 18, score: 60, weight: 70},
    {name: '小刘', age: 20, score: 70, weight: 65},
    {name: '小赵', age: 18, score: 60, weight: 60},
    {name: '小钱', age: 19, score: 70, weight: 55},
    {name: '小周', age: 20, score: 60, weight: 50},
];
const example = (data, key) => {
    return data.reduce(function(prev, cur) {
        (prev[cur[key]] = prev[cur[key]] || []).push(cur);
        return prev;
    }, {});
};
console.log(example(arr, 'age'));

// object: {18: Array(3), 19: Array(2), 20: Array(2)}
18: Array(3)
  0: {name: "小孙", age: 18, score: 60, weight: 60}
  1: {name: "小李", age: 18, score: 60, weight: 70}
  2: {name: "小赵", age: 18, score: 60, weight: 60}
19: Array(2)
  0: {name: "小王", age: 19, score: 70, weight: 55}
  1: {name: "小钱", age: 19, score: 70, weight: 55}
20: Array(2)
  0: {name: "小刘", age: 20, score: 70, weight: 65}
  1: {name: "小周", age: 20, score: 60, weight: 50}
  • 1.8 简写为 flat(),接收一个数组,无论这个数组里嵌套了多少个数组,flatten 最后都会把其变成一个一维数组(扁平化)。
flatten()
const arr = [[1,2,3],[4,5,[6,7]]];
const a = arr.flatten(3);
console.log(a); // [1, 2, 3, 4, 5, 6, 7]

# 2.表单校验

当表单数据填写完成,需要进一步做表单提交传送后端服务器,但是前端需要做数据的进一步确实是否符合规则,比如是否为必填项、是否为手机号码格式

  • 2.1 简单版的单字段检查
data() {
    return {
       schema:{
          phone: {
            required:true
          },
       }
    };
 },
 methods: {
    // 判断输入的值
     validate(schema, values) {
       for(field in schema) {
          if(schema[field].required) {
            if(!values[field]) {
              return false;
            }
          }
        }
       return true;
    },
 }

console.log(this.validate(schema, {phone:'159195**34'}));
  • 2.2 简单版的多字段检查
data() {
    return {
       phoneForm: {
          phoneNumber: '',
          verificationCode: '',
          tips:''
       },
       schema:{
          phoneNumber: [{required: true, error: '手机不能为空'}, {
            regex: /^1[3|4|5|6|7|8][0-9]{9}$/,
            error: '手机格式不对',
          }],
          verificationCode: [{required: true, error: '验证码不能为空'}],
       }
    };
 },
 methods: {
    // 判断输入的值
     validate(schema, values) {
      const valArr = schema;
      for (const field in schema) {
        if (Object.prototype.hasOwnProperty.call(schema, field)) {
          for (const key of schema[field]) {
            if (key.required) {
              if (!valArr[field]) {
                valArr.tips = key.error;
                return false;
              }
            } else if (key.regex) {
              if (!new RegExp(key.regex).test(valArr[field])) {
                valArr.tips = key.error;
                return false;
              }
            }
          }
        }
      }
      return true;
    },
 }

console.log(this.validate(this.schema, this.phoneForm);

复制代码 2.3 Iview 组件库 form 表单组件的校验实现 Iview 的 Form 组件模块主要由 Form 和 FormItem 组成 Form 主要是对 form 做一层封装 FormItem 是一个包裹,主要用来包装一些表单控件、提示消息、还有校验规则等内容。 源码链接 我们可以清晰看到,iview 的 form 组件是通过 async-validator 工具库来作为表单验证的方法

//async-validator的基本使用
import schema from 'async-validator';
var descriptor = {
  address: {
    type: "object", required: true,
    fields: {
      street: {type: "string", required: true},
      city: {type: "string", required: true},
      zip: {type: "string", required: true, len: 8, message: "invalid zip"}
    }
  },
  name: {type: "string", required: true}
}
var validator = new schema(descriptor);
validator.validate({ address: {} }, (errors, fields) => {
  // errors for address.street, address.city, address.zip
});
// 复制代码
// 而在iview的 form 组件中主要定义了validate函数中使用 field.validate就是调用async-validator的方法,用来管理form-item组件下的验证
// ViewUI/src/components/form/form.vue
methods:{
    validate(callback) {
        return new Promise(resolve => {
            let valid = true;
            let count = 0;
            this.fields.forEach(field => {
                field.validate('', errors => {
                    if (errors) {
                        valid = false;
                    }
                     // 检验已完成的校验的数量是否完全,则返回数据有效
                    if (++count === this.fields.length) {
                        // all finish
                        resolve(valid);
                        if (typeof callback === 'function') {
                            callback(valid);
                        }
                    }
                });
            });
     });
    },
    //针对单个的校验
     validateField(prop, cb) {
       const field = this.fields.filter(field => field.prop === prop)[0];
         if (!field) {throw new Error('[iView warn]: must call validateField with valid prop string!'); }
           field.validate('', cb);
      }
     //表单规则重置
     resetFields() {
        this.fields.forEach(field => {
            field.resetField();
        });
   },
},
created () {
    //通过FormItem定义的prop来收集需要校验的字段,
    this.$on('on-form-item-add', (field) => {
        if (field) this.fields.push(field);
        return false;
    });
    // 移除不需要的字段
    this.$on('on-form-item-remove', (field) => {
        if (field.prop) this.fields.splice(this.fields.indexOf(field), 1);
        return false;
    });
 }

// Form.vue 中涉及到的 this.fields 里面的规则 是在其create生命周期时通过监听‘on-form-item-add’push进来的,‘on-form-item-add’事件是由form-item 组件 触发相对应的事件,并传入相对应的实例就可以创建数据的关联,以下是form-item的生命周期函数内容:
// ViewUI/src/components/form/form-item.vue
mounted () {
         // 如果定义了需要验证的字段
          if (this.prop) {
                // 向父亲Form组件添加field
                this.dispatch('iForm', 'on-form-item-add', this);
                Object.defineProperty(this, 'initialValue', {
                    value: this.fieldValue
                });
                this.setRules();
            }
 },
  beforeDestroy () {
    // 移除之前删除form中的数据字段
    this.dispatch('iForm', 'on-form-item-remove', this);
}
  methods: {
            setRules() {
                //获取所有规则
                let rules = this.getRules();
                if (rules.length&&this.required) {
                    return;
                }else if (rules.length) {
                    rules.every((rule) => {
                        this.isRequired = rule.required;
                    });
                }else if (this.required){
                    this.isRequired = this.required;
                }
                this.$off('on-form-blur', this.onFieldBlur);
                this.$off('on-form-change', this.onFieldChange);
                this.$on('on-form-blur', this.onFieldBlur);
                this.$on('on-form-change', this.onFieldChange);
            },
             getRules () {
             let formRules = this.form.rules;
             const selfRules = this.rules;
             formRules = formRules ? formRules[this.prop] : [];
             return [].concat(selfRules || formRules || []);
           },
           validate(trigger, callback = function () {}) {
                let rules = this.getFilteredRule(trigger);
                if (!rules || rules.length === 0) {
                    if (!this.required) {
                        callback();
                        return true;
                    }else {
                        rules = [{required: true}];
                    }
                }
                // 设置AsyncValidator的参数
                this.validateState = 'validating';
                let descriptor = {};
                descriptor[this.prop] = rules;
                const validator = new AsyncValidator(descriptor);
                let model = {};
                model[this.prop] = this.fieldValue;
                validator.validate(model, { firstFields: true }, errors => {
                    this.validateState = !errors ? 'success' : 'error';
                    this.validateMessage = errors ? errors[0].message : '';
                    callback(this.validateMessage);
                });
                this.validateDisabled = false;
          },
}
是否为手机号码:/^1[3|4|5|6|7|8][0-9]{9}$/
是否全为数字: /^[0-9]+$/
是否为邮箱:/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/
是否为身份证:/^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/
是否为Url:/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/
是否为IP/((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/
数组操作:
https://juejin.im/post/5f20e0d7f265da230b531f04?utm_source=gold_browser_extension
上次更新: