1. Introduction

In the last article, I introduced some concepts and some simple examples of refactoring. This time, I’ll take a closer look at one of the refactoring scenarios in the project — designing an extension mechanism for the API. The goal is to be able to respond flexibly to changing needs later. Of course, whether you need to design extensibility depends on the requirements of the API. If you have any suggestions, feel free to leave a comment.

2. Extensible presentation

2-1.prototype

This can be said to be the most original extension in JS. For example, native JS does not provide an API for shuffling arrays, but developers want to make it easy to use, so they can only extend array Prototype. The following code

// Extend array. prototype to add methods to scramble arrays.
Array.prototype.upset=function(){
    return this.sort((n1,n2) = >Math.random() - 0.5);
}

let arr=[1.2.3.4.5];
/ / call
arr.upset();
// Display the result
console.log(arr);Copy the code

The results

The functionality is implemented. But the above code, just use examples to explain extensibility, we have a good look. Don’t copy it, and don’t write it in your project. It’s basically banned now. The reason is very simple, as mentioned in the previous article. So let me repeat that.

This will contaminate the native Array, and other arrays created by others will also be contaminated, causing unnecessary overhead. The scariest thing is that if you name it the same way as the original method, it overwrites the original method.

Array.prototype.push=function(){console.log('wait')}  
let arrTest=[123]
arrTest.push()
/ / result: waiting for you
// What does the push method doCopy the code

2-2.jQuery

For extensibility, jQuery provides three apis :$.extend(), $.fn, and $.fn.extend(). $.fn.extend(); $.fn.extend(); $.fn.extend(); $.fn.extend()

Reference links:

Understanding $.extend(), $.fn, and $.fn.extend() for jquery custom plugins $.extend(), $.fn, and $.fn.extend()

2-3.VUE

To extend VUE, refer to the official website (plug-in). There are generally the following ways of extension:

1. Add global methods or attributes, such as vue-custom-element

2. Add global resources: directives/filters/transitions, such as vue-touch

3. Add some component options, such as vue-router, through the global mixin method

4. Add Vue instance methods by adding them to VUe.Prototype.

5. A library that provides its own API and also provides one or more of the features mentioned above, such as vue-Router

Vue-based extensions. In the component, the content of the plug-in provides an install method. The following

Using the component

Above several extensibility examples are native objects, libraries, framework extensions, we may feel a bit exaggerated and talk about, that is to share a common daily development of an example.

3. Instance-form validation

Having looked at the extensibility examples above, let’s look at another example that is also used a lot in daily development: form validation. This piece can be said to be very simple, but do well, do general is not easy. I looked at JavaScript Design Patterns and Development Practices and refactored the old form validation function with the policy pattern. Let’s do a simple analysis.

The following content, the code will be more, although the code is not difficult, but it is strongly recommended that you do not just read, read, write, debug, or as a reader, you may not know what my code means, it is easy to be confused. The following code will cover two things for you to learn: the open-closed principle and the policy pattern.

3-1. Original programme

/** * @description checkArr * @returns {Boolean} */
function validateForm(checkArr){
    let _reg = null, ruleMsg, nullMsg, lenMsg;
    for (let i = 0, len = checkArr.length; i < len; i++) {
        // If no field value is undefined, the current loop is not executed, and the next loop is executed
        if (checkArr[i].el === undefined) {
            continue;
        }
        // Set rule error message
        ruleMsg = checkArr[i].msg || 'Field format error';
        // If the value is empty, an error message is displayed
        nullMsg = checkArr[i].nullMsg || 'Field cannot be empty';
        // Set length error message
        lenMsg = checkArr[i].lenMsg || 'Field length range' + checkArr[i].minLength + "To" + checkArr[i].maxLength;
        // If this field is null, check it
        if (checkArr[i].noNull === true) {
            // If the field is empty, a message is returned
            if (checkArr[i].el === "" || checkArr[i].el === null) {
                returnnullMsg; }}// If there is rule verification for this field
        if (checkArr[i].rule) {
            // Set rules
            switch (checkArr[i].rule) {
                case 'mobile':
                    _reg = /^1[3|4|5|7|8][0-9]\d{8}$/;
                    break;
                case 'tel':
                    _reg = /^\d{3}-\d{8}|\d{4}-\d{7}|\d{11}$/;
                    break;
            }
            // If the field is not empty and the rule is incorrect, an error message is returned
            if(! _reg.test(checkArr[i].el) && checkArr[i].el ! = =""&& checkArr[i].el ! = =null) {
                returnruleMsg; }}// If the field is not empty and the length is incorrect, an error message is returned
        if(checkArr[i].el ! = =null&& checkArr[i].el ! = =' ' && (checkArr[i].minLength || checkArr[i].maxLength)) {
            if (checkArr[i].el.toString().length < checkArr[i].minLength || checkArr[i].el.toString().length > checkArr[i].maxLength) {
                returnlenMsg; }}}return false;
}Copy the code

Function call mode

    let testData={
        phone:'18819323632'.        pwd:'112'
    }

    let _tips = validateForm([
        {el: testData.phone, noNull: true.nullMsg: 'Phone number cannot be empty'.rule: "mobile".msg: 'Wrong phone number format'},
        {el: testData.pwd, noNull: true.nullMsg: 'Password cannot be empty'.lenMsg:'Incorrect password length'.minLength:6.maxLength:18}]);// Field validation if an error message is returned
    if (_tips) {
        alert(_tips);
    }Copy the code

3-2. There are problems

In this way, I believe you are also uncomfortable, because the problem is really more.

1. A field may be entered through three judgments (null value, rule, length). If it is just a simple phone number rule check, it will go through two other unnecessary checks, resulting in unnecessary overhead. The running process looks like the following.



2. In the rule verification, only these types of verification are available. If you want to add other verification, such as a rule for adding a date, the verification cannot be completed. If you keep modifying the source code, it can lead to huge functions.

3. It’s not elegant and easy to use.

3-3. Alternatives

According to the above three problems 2-2, make improvement one by one.

It is difficult to optimize and refactor the internal code and increase extensibility without changing the way validateForm is called. It is not possible to rewrite this method because some places already use the API and it is not practical to change it one by one, so instead of modifying the validateForm, create a new API: Validate. In future projects, I also tried to guide my colleagues to abandon validateForm and use the new API.

The first one above optimizes the check rule so that each check (such as null value, length, rule) is a simple check and no unnecessary checks are performed. Run the process as follows.



let validate = function (arr) {
    let ruleData = {
        / * * *@descriptionCannot be null *@param val
         * @param msg
         * @return{x} * /
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        / * * *@descriptionMinimum length *@param val
         * @param length
         * @param msg
         * @return{x} * /
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        / * * *@descriptionMaximum length *@param val
         * @param length
         * @param msg
         * @return{x} * /
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        / * * *@descriptionIs the mobile phone number format *@param val
         * @param msg
         * @return{x} * /
        isMobile(val, msg){
            if(! / ^1[39 -]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    let ruleMsg, checkRule, _rule;
    for (let i = 0, len = arr.length; i < len; i++) {
        // If the field is not found
        if (arr[i].el === undefined) {
            return 'Field not found! '
        }
        // Iterate over the rule
        for (let j = 0; j < arr[i].rules.length; j++) {
            // Extract the rule
            checkRule = arr[i].rules[j].rule.split(":");
            _rule = checkRule.shift();
            checkRule.unshift(arr[i].el);
            checkRule.push(arr[i].rules[j].msg);
            // If the rule is wrong
            ruleMsg = ruleData[_rule].apply(null, checkRule);
            if (ruleMsg) {
                // Return an error message
                returnruleMsg; }}}}; let testData = { name:' ',
    phone: '18819522663',
    pw: 'asda'
}
// Validates the function call
console.log(validate([
    {
        // Check the data
        el: testData.phone,
        // Check the rule
        rules: [
            {rule: 'isNoNull', msg: 'Phone cannot be empty'}, {rule: 'isMobile', msg: 'Incorrect format of mobile number'}
        ]
    },
    {
        el: testData.pw,
        rules: [
            {rule: 'isNoNull', msg: 'Phone cannot be empty'},
            {rule:'minLength:6',msg:'Password length cannot be less than 6'}}]]));Copy the code

So this is the first step, and before we go to step 2, let’s just think, if the ruleData rule is not enough, for example, if I want to add a date range check, I have to modify ruleData to add a property. The following

let ruleData = {
    // Some previous rules
    / * * *@descriptionIs it a date range *@param val
     * @param msg
     * @return{x} * /
    isDateRank(val,msg) {
        let _date=val.split(', ');
        if(new Date(_date[0]).getTime()>=new Date(_date[1]).getTime()){
            returnmsg; }}}Copy the code

If there’s another rule, you have to change this one, and you’re violating the open-closed principle. If multiple people share this function, the rules may become too large, causing unnecessary overhead. For example, page A has the amount verification, but only page A has it. If we change according to the above way, the verification rule of amount will be loaded on page B, but it will not be used at all, resulting in a waste of resources.

So the open-close principle applies. Add extensibility to function validation rules. Before you do it, you should be confused, because a function can do checksums, and it can add checksums. If a function does two things, it violates the singleness principle. It is also difficult to maintain, so the recommended approach is to do interfaces. So let me write it like this.

let validate = (function (a) {
    let ruleData = {
        / * * *@descriptionCannot be null *@param val
         * @param msg
         * @return{x} * /
        isNoNull(val, msg){
            if(! val) {return msg
            }
        },
        / * * *@descriptionMinimum length *@param val
         * @param length
         * @param msg
         * @return{x} * /
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        / * * *@descriptionMaximum length *@param val
         * @param length
         * @param msg
         * @return{x} * /
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        / * * *@descriptionIs the mobile phone number format *@param val
         * @param msg
         * @return{x} * /
        isMobile(val, msg){
            if(! / ^1[39 -]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    return {
        / * * *@descriptionQuery interface@param arr
         * @return{x} * /
        check: function (arr) {
            let ruleMsg, checkRule, _rule;
            for (let i = 0, len = arr.length; i < len; i++) {
                // If the field is not found
                if (arr[i].el === undefined) {
                    return 'Field not found! '
                }
                // Iterate over the rule
                for (let j = 0; j < arr[i].rules.length; j++) {
                    // Extract the rule
                    checkRule = arr[i].rules[j].rule.split(":");
                    _rule = checkRule.shift();
                    checkRule.unshift(arr[i].el);
                    checkRule.push(arr[i].rules[j].msg);
                    // If the rule is wrong
                    ruleMsg = ruleData[_rule].apply(null, checkRule);
                    if (ruleMsg) {
                        // Return an error message
                        returnruleMsg; }}}},/ * * *@descriptionAdd rule interface *@param type
         * @param fn
         */
        addRule:function (type,fn) { ruleData[type]=fn; }}}) ();// Validate function calls - test cases
console.log(validate.check([
    {
        // Check the data
        el: testData.mobile,
        // Check the rule
        rules: [
            {rule: 'isNoNull', msg: 'Phone cannot be empty'}, {rule: 'isMobile', msg: 'Incorrect format of mobile number'}
        ]
    },
    {
        el: testData.password,
        rules: [
            {rule: 'isNoNull', msg: 'Phone cannot be empty'},
            {rule:'minLength:6',msg:'Password length cannot be less than 6'}}]]));// extension - add date range checksum
validate.addRule('isDateRank'.function (val,msg) {
    if(new Date(val[0]).getTime()>=new Date(val[1]).getTime()){
        returnmsg; }});// Test the newly added rule - date range validation
console.log(validate.check([
    {
        el:['the 2017-8-9 22:00:00'.'the 2017-8-8 24:00:00'],
        rules:[{
            rule:'isDateRank',msg:'Incorrect date range'}}]]));Copy the code

As shown in the code above, you need to add a date range validation to ruleData, which you can do here. But things that can’t access or modify ruleData have a protective effect. Another is that, for example, the date verification added on page A will only exist on page A and will not affect other pages. If date validation is possible elsewhere, consider globally adding a date validation rule to ruleData.

As for the third question, this idea may not be very elegant and convenient to call, but as far as I can think of, this is the best solution.

So this looks like we’ve done it, but you might feel like there’s one thing you can’t do, like this one, you can’t do it.

Because the check interface above, as long as there is an error, it will immediately jump out, will not check the next one. If you want to implement the following function, you need to implement, if there is a value check error, log the error message, continue to check the next, after all the checks have been performed, as shown in the flowchart below.



When the result is returned, the next interface must be exposed.

The code looks like this (ignore the alias attribute for now)

let validate= (function (a) {
    let ruleData = {
        / * * *@descriptionCannot be null *@param val
         * @param msg
         * @return{x} * /
        isNoNull(val, msg){
            if(! val) {return msg
            }
        },
        / * * *@descriptionMinimum length *@param val
         * @param length
         * @param msg
         * @return{x} * /
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        / * * *@descriptionMaximum length *@param val
         * @param length
         * @param msg
         * @return{x} * /
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        / * * *@descriptionIs the mobile phone number format *@param val
         * @param msg
         * @return{x} * /
        isMobile(val, msg){
            if(! / ^1[39 -]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    return {
        check: function (arr) {
            // The code is not shown repeatedly, above part
        },
        addRule:function (type,fn) {
            // The code is not shown repeatedly, above part
        },
        / * * *@descriptionVerify all interfaces *@param arr
         * @return{x} * /
        checkAll: function (arr) {
            let ruleMsg, checkRule, _rule,msgArr=[];
            for (let i = 0, len = arr.length; i < len; i++) {
                // If the field is not found
                if (arr[i].el === undefined) {
                    return 'Field not found! '
                }
                // If the field is empty and the rule is not a null rule

                // Iterate over the rule
                for (let j = 0; j < arr[i].rules.length; j++) {
                    // Extract the rule
                    checkRule = arr[i].rules[j].rule.split(":");
                    _rule = checkRule.shift();
                    checkRule.unshift(arr[i].el);
                    checkRule.push(arr[i].rules[j].msg);
                    // If the rule is wrong
                    ruleMsg = ruleData[_rule].apply(null, checkRule);
                    if (ruleMsg) {
                        // Record error informationmsgArr.push({ el:arr[i].el, alias:arr[i].alias, rules:_rule, msg:ruleMsg }); }}}// Return an error message
            return msgArr.length>0? msgArr:false; }}}) ();let testData = {
    name: ' ',
    phone: '188',
    pw: 'asda'
}
// extension - add date range checksum
validate.addRule('isDateRank'.function (val,msg) {
    if(new Date(val[0]).getTime()>=new Date(val[1]).getTime()){
        returnmsg; }});// Validates the function call
console.log(validate.checkAll([
    {
        // Check the data
        el: testData.phone,
        alias:'mobile'.// Check the rule
        rules: [
            {rule: 'isNoNull', msg: 'Phone cannot be empty'}, {rule: 'isMobile', msg: 'Incorrect format of mobile number'},{rule:'minLength:6',msg: 'Mobile phone number must not be less than 6'}
        ]
    },
    {
        el: testData.pw,
        alias:'pwd',
        rules: [
            {rule: 'isNoNull', msg: 'Phone cannot be empty'},
            {rule:'minLength:6',msg:'Password length cannot be less than 6'}
        ]
    },
    {
        el:['the 2017-8-9 22:00:00'.'the 2017-8-8 24:00:00'],
        rules:[{
            rule:'isDateRank',msg:'Incorrect date range'}}]]));Copy the code

As a result, all illegal data records are now returned. As for alias then, it’s now revealed. For example, if the page is vUE rendered, you can do so based on alias.





If it is rendered by jQuery, this can be done according to alias.





3-4. Backward compatibility scheme

Because the project used the previous verification API before, it cannot be used at the same time. Before the previous API is abandoned, it cannot be affected. So we need to rewrite the old validateForm to make it compatible with the new API: Validate.

    let validateForm=function (arr) {
        let _param= []._single= {};for(let i=0; i<arr.length; i++){_single= {};_single.el=arr[i].el;
            _single.rules=[];
            // If there is a non-null test
            if(arr[i].noNull){
                _single.rules.push({
                    rule: 'isNoNull',
                    msg: arr[i].nullMsg||'Field cannot be empty'})}// If there is a minimum length check
            if(arr[i].minLength){
                _single.rules.push({
                    rule: 'minLength:'+arr[i].minLength,
                    msg: arr[i].lenMsg ||'Field length range error'})}// If there is a maximum length check
            if(arr[i].maxLength){
                _single.rules.push({
                    rule: 'maxLength:'+arr[i].maxLength,
                    msg: arr[i].lenMsg ||'Field length range error'})}// If there is rule verification
            // Verify the conversion rule
            let _ruleData={
                mobile:'isMobile'
            }
            if(arr[i].rule){
                _single.rules.push({
                    rule: _ruleData[arr[i].rule],
                    msg: arr[i].msg ||'Field format error'})}_param.push(_single);
        }
        let _result=validate.check(_param);
        return _result?_result:false;
    }
    let testData={
        phone:'18819323632',
        pwd:'112'
    }
    let _tips = validateForm([
        {el: testData.phone, noNull: true, nullMsg: 'Phone number cannot be empty',rule: "mobile", msg: 'Wrong phone number format'},
        {el: testData.pwd, noNull: true, nullMsg: 'Password cannot be empty',lenMsg:'Incorrect password length',minLength:6,maxLength:18}]); console.log(_tips)Copy the code

4. Summary

So that’s all for today’s example, which is just to add extensibility to the API. This is a simple example, not a difficult one. So if you run this code in a browser, it makes a lot of sense. If you have any better suggestions for this example, or any questions about the code, feel free to leave them in the comments, so we can talk and learn from each other.



— — — — — — — — — — — — — — — — — — — — — — — — — gorgeous line — — — — — — — — — — — — — — — — — — — —

Want to know more, pay attention to my wechat public number: Waiting book pavilion