origin
Recently, I have been delayed for a long time by the huge demand for Jasmine products. I have not written for a long time, and I feel a little worried. Therefore, I hereby settle down to relieve my anxiety ๐. Today, the main share is about some knowledge of form verification, we should all know, is to verify user name, email, mobile phone number what, although tasteless, but abandoned the pity ๐ฌ. Generally speaking, form validation can be divided into two types: Instant verification (local check) and asynchronous validation (such as user name is available, verification code, etc.), can be understood as is the front end of calibration and the back-end (front and back side in the work are to check, to ensure the accuracy and effectiveness of the final data, I believe you should also have check ๐), and we focused on today’s front end form validation.
The target
๐, first of all, let’s briefly say the target functions to be realized:
- Has basic form validation capabilities
- Provides some built-in validation rules
- Provide the ability to open up to the outside world
Form validation can actually exist off the page. It is essentially a function that takes two parameters (data and rule), validates it, and returns an error message if the validation fails. We should all understand the meaning, and have written, but how to write elegant points, or to make the development of a more convenient use, let’s go from 0 to 1 to see ๐ง.
The first edition
So, all things are difficult before they are easy ๐คจ, where to start? Obviously, there are two steps:
- Firstly, the values and rules to be verified are obtained.
- Then the corresponding rule verification is performed and the verification result is returned.
Specifically, we write a function that takes two arguments (data and rules), and it should return an error object, like ๐ :
function validate(data, rules) {
// ...
}
// The data looks something like this
let data = {
name: 'xxx'.phone: '138xxxxxxxx'
}
// The rule looks like this. Why does it look like this
let rules = [{
key: 'name'.required: true.minLen: 6.maxLen: 10
}, {
key: 'phone'.pattern: 'phone'
}]
// The error message looks something like this
let errors = {
name: {
required: 'mandatory'.minLen: 'Too short'.maxLen: 'It's too long'
},
phone: {
pattern: 'Wrong phone format'}}Copy the code
This seemingly simple piece of code above is actually a bit of a mystery, but I would like to highlight two or three points:
- Rules is an array, because in practical work we often need to check in order, so to write in the form of array, we should check the corresponding data according to the order of rules.
- Each data may return multiple error messages. Should we show just one or all of them? You can think about ๐ค… Ok, the answer to the puzzle is revealed, usually we only need to record one error, because the page usually only shows one error message, that is, one data is wrong, do not verify the other errors of the data, there is no need, but this article will show all the errors ๐ฏ, haha.
- In addition, each rule
required
The field is always the highest priority, it is special to other rules, after all, there is no value, other rules are not useful.
Then we just need to improve the validation function, the general idea is to loop rules, get the corresponding data value to verify, if there are any errors write errors, like the following ๐ :
function validate(data, rules) {
let errors = {}; // If there are any errors, put them here
rules.forEach(rule= > {
let val = data[rule.key]
if (rule.required) {
if(! val && val ! = =0) {
setDefaultObj(errors, rule.key) // This function is below to make sure errors[rule-key] is an object
errors[rule.key].required = 'mandatory'
return // If the value is not specified, the value is returned directly}}if (rule.pattern) {
if (rule.pattern === 'phone') {
if(!/^1\d{10}$/.test(val)) { // Check the phone briefly
setDefaultObj(errors, rule.key)
errors[rule.key].pattern = 'Mobile phone format error'}}}if (rule.minLen) {
if (val.length < rule.minLen) {
setDefaultObj(errors, rule.key)
errors[rule.key].minLen = 'Too short'}}if (rule.maxLen) {
if (val.length > rule.maxLen) {
setDefaultObj(errors, rule.key)
errors[rule.key].maxLen = 'Too long'}}console.log(errors)
});
}
function setDefaultObj(obj, key) { // Make sure it is an object for easy assignment
obj[key] = obj[key] || {}
}
Copy the code
Let’s execute the above function with Node and see the following result:Yes, the above is our first version of all the code, has been written ๐, the content is not much to understand.
But this is far from enough. While the basic functionality is implemented, the drawbacks are also obvious:
- If I add a few more checks, how fat does this function have to get
- Too much if-else means we need to make it elegant
- No reusability, right
- There is also what seems to be repetitive logic
- If we want to change a rule we have to change it in a function, which violates the open-closed principle
So let’s make a small change ๐คจ (small change happy, big change harm body), of course you can still think about ๐ค…
The second edition
So the first thing that we can think of is to take the if-else out, take the checksum logic out of the outside, so how do I say that? We all know that functions are also objects, so we can write validation methods directly to function properties like fn.required = () => {} or fn.pattern = () => {}.
function validate(data, rules) {
let errors = {}; // If there are any errors, put them here
rules.forEach(rule= > {
let val = data[rule.key]
if (rule.required) {
let error = validate.required(val)
if (error) {
setDefaultObj(errors, rule.key)
errors[rule.key] = error
return}}if (rule.pattern) {
let error = validate.pattern(val, rule.pattern)
if (error) {
setDefaultObj(errors, rule.key)
errors[rule.key].pattern = error
}
}
if (rule.minLen) {
let error = validate.minLen(val, rule.minLen)
if (error) {
setDefaultObj(errors, rule.key)
errors[rule.key].minLen = error
}
}
if (rule.maxLen) {
let error = validate.maxLen(val, rule.maxLen)
if (error) {
setDefaultObj(errors, rule.key)
errors[rule.key].maxLen = error
}
}
console.log(errors)
});
}
validate.required = (val) = > {
if(! val && val ! = =0) {
return 'mandatory'
}
}
validate.pattern = (val, pattern) = > { // Pattern can be user-defined or built-in
if (pattern === 'phone') {
if(!/^1\d{10}$/.test(val)) {
return 'Mobile phone format error'}}else if(! pattern.test(val)) {return 'Mobile phone format error'
}
}
validate.minLen = (val, minLen) = > {
if (val.length < minLen) {
return 'Too short'
}
}
validate.maxLen = (val, maxLen) = > {
if (val.length > maxLen) {
return 'Too long'}}Copy the code
After a look, you may god ๐ค, the amount of code seems to have nothing to reduce, and even the repetition of the more obvious, quite inelegant ah. Heck, that’s true, but compared to the first version, you can see that we’ve removed the rules, at least not all of them, from the validate function. You can just change the rules outside of the function and add other rules outside of the function. But this is not enough, the shortcomings of the preceding paragraph also seem to remain, especially the following paragraph is very repetitive, you can see that every if-else is similar, only one word is different, so we can continue to rewrite it. Specific how to rewrite, and can think about ๐ค…
if (rule.required) {}
if (rule.pattern) {}
if (rule.minLen) {}
if (rule.maxLen) {}
Copy the code
The third edition
The simple idea is to iterate over it, but note that the keys in each rule: ‘XXX’ and required: true are special, so we need to exclude them and iterate over the other rules, which can be treated as equal. Specifically look at the following code ๐, there should be annotations can understand ๐ :
function validate(data, rules) {
let errors = {}; // If there are any errors, put them here
rules.forEach(rule= > {
let val = data[rule.key]
if (rule.required) { // Required, separate processing is appropriate
let error = validate.required(val)
if (error) {
setDefaultObj(errors, rule.key)
errors[rule.key] = error
return}}let restKeys = Object.keys(rule).filter(key= >key ! = ='key'&& key ! = ='required'); // Filter out key and required
restKeys.forEach(restKey= > {
if (validate[restKey]) { // The rule may not exist, so you need to give the user a warning or an error
let error = validate[restKey](val, rule[restKey])
if (error) {
setDefaultObj(errors, rule.key)
errors[rule.key][restKey] = error
}
} else {
throw `${restKey}The rule does not exist}})});console.log(errors)
return errors
}
Copy the code
Ha ha ๐, now it looks really comfortable, but still slightly awkward, versatility and scalability seems not strong enough. If someone changes this, will it affect other people? Or would rule one conflict? So, there are still problems. In fact, our checksum is now public, and we need to divide the rules into two types, one is public, and one is custom (can override public and does not affect others). (๐) (ES6) (๐) (ES6) (ES6) (๐) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6) (ES6)
The fourth edition
Ok, now let’s use the class to rewrite the above check function (do not understand the class of the suggestion to look at the first, very simple, is to change the way, good habit), here directly on the code ๐ :
class Validator {
constructor(){}static addRule (name, fn) { // Add a new rule globally
Validator.prototype[name] = fn
}
validate(data, rules) {
let errors = {}
rules.forEach(rule= > {
let val = data[rule.key]
if (rule.required) {
let error = this.required(val)
if (error) {
this.setDefaultObj(errors, rule.key)
errors[rule.key] = error
return}}let restKeys = Object.keys(rule).filter(key= >key ! = ='key'&& key ! = ='required');
restKeys.forEach(restKey= > {
if (this[restKey]) {
let error = this[restKey](val, rule[restKey])
if (error) {
this.setDefaultObj(errors, rule.key)
errors[rule.key][restKey] = error
}
} else {
throw `${restKey}The rule does not exist}})});console.log(errors)
}
required (val) {
if(! val && val ! = =0) {
return 'mandatory'
}
}
pattern (val, pattern) { // Pattern can be user-defined or built-in
if (pattern === 'phone') {
if(!/^1\d{10}$/.test(val)) {
return 'Mobile phone format error'}}else if(! pattern.test(val)) {return 'Mobile phone format error'
}
}
minLen (val, minLen) {
if (val.length < minLen) {
return 'Too short'
}
}
maxLen (val, maxLen) {
if (val.length > maxLen) {
return 'Too long'
}
}
setDefaultObj (obj, key) {
obj[key] = obj[key] || {}
}
}
// Of course, the usage will change, but the printed error message will be the same
let validator = new Validator()
validator.validate(data, rules)
Copy the code
Is it a bit of a sense to see the moon ๐คฏ? No calculation ๐, anyway, the above writing method and the first version of the original should be a small step, but also easy to expand and maintain, good ๐๐๐. Of course, you can review the fourth edition for ten years, add and delete five times, and make it more complete and elegant.
wake
In practice, we often write if-else and then don’t want to change it, which should be a bit awkward ๐, so do I. But in short, want to write elegant, it is necessary to write more changes, better than the original is progress, this is a step-by-step process, rather than a step in place. Finally, I hope that this article can be helpful to everyone, great praise of the boundless ๐๐๐… Ps: After writing the article, the product suddenly told me about the next demand. I listened to it and it took me a long time to write.