随着项目越来越大,最近遇到了一个问题。需求需要修改账户,密码和电话号码的验证规则。 现在前端总共维护7个项目,5个pc项目和2微信页面。表单项涉及很多,并且验证规则分配在各个页面,导致修改验证规则,需要到每个页面中寻找修改,这样不仅耗时长并且容易出现漏改的情况。所以就在考虑如何基于现有的框架(element)把验证规则抽象到一个文件里面。
面临的问题
- 将验证规则和过滤规则抽象出来
- 不能影响现有规则,因为项目太多,不可能一次性把所有的验证都抽象出来,所以需要设计一个模式,可以每次抽出一部分,但是有不影响之前的验证。
- 最好不要加入太多的上限文,导致开发者疑惑,最好是对于使用者是透明的。
- 使用方式最好简单,使用者不需要了解太多。
- 考虑到规则中有限制输入长度的,所以解决的方式中应该包含自动设置input的maxlength才行。
解决方式
使用vue的函数组件,重新封装el-form-item组件,并且将规则注入,并且在入口文件重新以el-form-item的名称进行注册。这样既能保证原先的代码可正常运行,又能将规则注入集中。
开发中遇到的问题
- vue的函数组件无法传入具名slot。之前用.vue封装透明组件的时候,需要为了透明传入slot,需要自己用具名template slot重新包裹。这样slot少的情况下还行,多的情况下,代码就显得不美观了。本来以为使用vue的函数组件能够解决这个问题。但是通过ctx.chilren传入的只是slots.default的内容,根本不能传入具名slot内容。但是可以通过vue作者的一个helper来解决
function mapSlots (h, slots = {}) {
return Object.keys(slots).map(name => {
return h('template', { slot: name }, slots[name])
})
}
复制代码
- vue的函数组件的渲染时间很快。甚至不能通过ctx.parent来获取到正确的父节点,只能获取到当前单文件组件的实例。
- 由于在BaseFormItem中操作了原生dom设置了maxlength和placeholder等属性。在element-ui中2.0.11版本中会出现,触发验证placeholder失效和maxlength失效的问题。但是在最新的element-ui中没有这种问题,探究原因是之前的版本的el-input组件,采用
v-bind="$props"
来透传属性。而最新的版本通过v-bind="$attrs"
来透传属性。 - element-ui中的v-model触发在2.0.11中没有对键盘输入中文的情况做处理。导致每次输入文字都会触发input而不是在中文输入完成并且选择之后触发,这样会导致触发验证的时机不正确,出现意想不到到输入效果。在最新的element-ui版本中已经修复了这个问题。
- 还有由于项目的问题,轻易不能升级element-ui的版本,心累呀,这些问题只能自己处理。
代码实现
基于element-ui el-form-item封装的base-form-item组件
.
├── bindRules.js // 在input组件上直接绑定过滤规则和验证规则的指令
├── index.js // 入口文件
├── install.js // 注册函数
└── utils.js
复制代码
index.js
import FormItem from 'element-ui/packages/form-item';
import { mix, handleMessageLabel, bindRuleAndFilter, debounce } from './utils'
const parseRuleName = (ruleName) => {
return ruleName;
};
const rulesCensus = {};
const ids = {};
const pushId = (id) => {
ids[id] = true;
};
const pushRules = (rulesName, label, id) => {
let rules = rulesCensus[rulesName];
if (!rules) {
rulesCensus[rulesName] = {
count: 1,
label: label,
name: rulesName,
id: [id]
}
} else {
rules.count++;
rules.id.push(id)
}
};
const print = debounce(function () {
for (let k in rulesCensus) {
if (rulesCensus.hasOwnProperty(k)) {
const item = rulesCensus[k];
console.log(`校验名称:${item.name}, 校验字段:${item.label}, 使用次数:${item.count}, 对应页面url->${item.id}}`)
}
}
}, 300);
export default (
BuildInRules = {},
mixProp = {},
buildInFilter = {},
debug = false
) => {
debug && console.warn(`[components/baseFormItem]:debugger参数只应该用于在修改页面校验规则时,打印页面所用校验规则,方便查找规则进行修改`);
return {
functional: true,
components: {
FormItem
},
render (h, context) {
// issue https://github.com/vuejs/vue/issues/7710
function mapSlots (h, slots = {}) {
return Object.keys(slots).map(name => {
return h('template', { slot: name }, slots[name])
})
}
const props = context.props || {};
const ruleName = props.rules;
const label = props.label;
let rules = {};
const ruleNameIsStr = typeof ruleName === 'string';
if (ruleNameIsStr) {
const name = parseRuleName(ruleName);
rules = BuildInRules[name];
rules && mix(rules, mixProp);
if (!rules) {
console.error(`[components/baseFormItem]:找不到对应的校验${ruleName}规则`);
return h(FormItem, context.data, context.children);
}
rules = handleMessageLabel(rules, label);
// 处理规则引用统计和debug打印,用于在后期修改规则的时候,更容易查看到页面引用的规则和规则名称
if (debug) {
const id = ruleName + label + window.location.href;
if (!ids[id]) {
pushRules(ruleName, label, window.location.href);
}
pushId(id);
print();
console.log(` 表单字段名称:${label} 该字段对应的校验规则名称:${ruleName}`);
}
context.data.attrs.rules = rules;
}
const child = (context.children || [])[0];
const setChild = child && ruleNameIsStr;
setChild && setTimeout(() => {
bindRuleAndFilter(child, rules, buildInFilter)
});
return h(FormItem, context.data, mapSlots(h, context.slots()));
}
}
}
复制代码
utils.js
export const toArray = (v) => {
if (!Array.isArray(v)) {
return [v];
}
return v;
};
export const getValue = (rules, field = 'max', defaultVal = Infinity) => {
const rulesArr = toArray(rules);
return (rulesArr.find(i => i.hasOwnProperty(field)) || {})[field] || defaultVal;
};
export const replaceLabel = (target, label) => {
return target.replace(/\$/g, label).replace(/[::]/g, '');
};
export const handleMessageLabel = (rules, label = '') => {
const targetArr = toArray(rules);
return targetArr.map(i => {
const cloneRules = Object.assign({}, i);
['placeholder', 'message'].forEach(filed => {
if (cloneRules.hasOwnProperty(filed)) {
cloneRules[filed] = replaceLabel(cloneRules[filed], label);
}
});
// 给自定义的validator函数添加第四个位置的参数为当前要验证的label
if (i.hasOwnProperty('validator')) {
const validator = cloneRules.validator;
cloneRules.validator = function () {
const argv = [].slice.call(arguments);
argv[3] = label;
validator.apply(this, argv);
};
}
return cloneRules;
});
};
export const mix = (target, mixProp) => {
const targetArr = toArray(target);
targetArr.forEach(i => {
Object.assign(i, mixProp);
});
};
export const bindRuleAndFilter = (child, rules, buildInFilter, isAutoComplete, isDirective) => {
let component = child.componentInstance;
if (!component) {
return;
}
let input = component.$refs.input;
if (isAutoComplete) {
component = input;
input = component.$refs.input;
}
if (input && input.tagName === 'INPUT') {
if (!input.getAttribute('maxlength')) {
input.setAttribute('maxlength', getValue(rules, 'max', Infinity));
}
input.setAttribute('maxlength', getValue(rules, 'max', Infinity));
if (!input.getAttribute('placeholder')) {
input.setAttribute('placeholder', getValue(rules, 'placeholder', ''));
}
const filterName = getValue(rules, 'filterName', (v) => v);
let filterFun = filterName;
if (typeof filterName === 'string') {
filterFun = buildInFilter[filterName];
if (filterFun === undefined) {
console.error(`[components/baseFormItem]:找不到对应的过滤${filterName}规则`);
return;
}
}
if (filterFun) {
let isCompositions = false;
if (!input.beforeListener) {
// 处理中文输入问题
input.addEventListener('compositionstart', (e) => {
isCompositions = true;
});
input.addEventListener('input', (e) => {
if (isCompositions) {
return;
}
e.target.value = filterFun(e.target.value);
component.handleInput(e);
});
input.addEventListener('compositionend', (e) => {
isCompositions = false;
e.target.value = filterFun(e.target.value);
component.handleInput(e);
});
input.beforeListener = true;
}
}
}
}
/* eslint-disable */
export const debounce = (func, delay) => {
let timer = null;
return function () {
const argv = arguments;
const ctx = this;
if (timer) {
clearInterval(timer);
}
timer = setTimeout(function () {
func.call(ctx, ...argv)
}, delay);
}
}
复制代码
bindRules.js
import { bindRuleAndFilter, handleMessageLabel } from './utils';
// 支持在el-input组件上传入校验规则的指令,书写方式为v-rule={ label: '联系电话',rule: 'phoneCode' }
// 或者v-rule:联系电话="'phoneCode'"
export default (buildInRules, buildInFilters) => ({
name: 'rule',
update (el, bind, vnode) {
let label, ruleName;
let isAutoComplete = false;
if (typeof bind.value === 'object') {
const desc = bind.value;
ruleName = desc.rule;
label = desc.label;
isAutoComplete = desc.isAutoComplete;
} else {
label = bind.arg;
ruleName = bind.value;
}
if (!ruleName) return;
let rules = buildInRules[ruleName];
if (rules === undefined) {
throw new TypeError(`[directive-rule]:找不到对应的${ruleName}规则`);
}
rules = handleMessageLabel(rules, label);
bindRuleAndFilter(vnode, rules, buildInFilters, isAutoComplete, true)
}
})
复制代码
使用方式
// FormItem对应于上面的install文件,rules和inputFilter对应于抽象出来的验证规则和过滤规则
Vue.use(FormItem(rules, { trigger: 'change, blur' }, inputFilter));
复制代码
rule定义
password: {
// field为注入的label名称
validator (rules, value, cb, field) {
const reg = /\d/;
if (!reg.test(value)) {
return cb(new Error(`请输入正确格式的${field}`));
}
cb();
},
required: true,]
// 其中$会被替换成el-form-item的label名称。这样写的目的是方便规则复用和定制
placeholder: `$限制2到30个字符`,
max: 50
},
复制代码
filter定义
trim (v) {
if (!v) return v;
return v.replace(/\s/g, '');
},
复制代码