吵吵   2013-04-13  阅读:2,648

虽然OSC首页之前有一篇文章《我眼中的技术高手》对国内开发人员太过关注框架底层源码的行为表示了一些怀疑和鄙视,但我还是想把这篇文章完成。目的有二:一来通过这篇文章好好学习下优秀框架的源代码;二来巩固下JavaScript基础。

1、什么是Validate.js

Validate.js是一个轻量级的JavaScript表单验证框架。它不依赖其他类库,压缩后体积只有1KB(包括注释代码总共455行),并且可定制。但注意它只侧重数据有效性的验证,而不提供诸如错误消息样式定制等UI方面的功能特性。

2、一个例子学会怎么用

Validate.js

完整Demo如下:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Validate.js Hello World!</title>
        <style type="text/css">
        	.success_box {
        		color: green;
        	}

        	.error_box {
        		color: red;
        	}
        </style>
    </head>
    <body>
       <div class="success_box" style="display: none;">All of the fields were successfully validated!</div>
       <div class="error_box" style="display: none;">The email field must contain a valid email address.<br></div>
       <form name="exampleForm" method="post" action="#">
	        <label for="req">必填域:</label>
	        <input name="req" id="req"><br />

	        <label for="alphanumeric">只能包含数字字母的域:</label>
	        <input name="alphanumeric" id="alphanumeric"><br />

	        <label for="password">密码域:</label>
	        <input name="password" id="password" type="password"><br />

	        <label for="password_confirm">密码确认:</label>
	        <input name="password_confirm" id="password_confirm" type="password"><br />

	        <label for="email">Email:</label>
	        <input name="email" id="email"><br />

	        <label for="minlength">不少于8个字符的域:</label>
	        <input name="minlength" id="minlength"><br />

	        <label for="tos_checkbox">必须勾选Checkbox(比如必须同意条款)</label>
	        <input name="tos_checkbox" id="tos_checkbox" type="checkbox"><br />

	        <button class="button gray" type="submit" name="submit">Submit</button>
       </form>

       <script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
       <script type="text/javascript" src="http://rickharrison.github.com/validate.js/validate.min.js"></script>
       <script type="text/javascript">
       		$(function() {
       			var validator = new FormValidator('exampleForm', 
       			[{
				    name: 'req',
				    display: 'required',    
				    rules: 'required'
				}, {
				    name: 'alphanumeric',
				    rules: 'alpha_numeric'
				}, {
				    name: 'password',
				    rules: 'required'
				}, {
				    name: 'password_confirm',
				    display: 'password confirmation',
				    rules: 'required|matches[password]'
				}, {
				    name: 'email',
				    rules: 'valid_email'
				}, {
				    name: 'minlength',
				    display: 'min length',
				    rules: 'min_length[8]'
				}, {
				    name: 'tos_checkbox',
				    display: 'terms of service',
				    rules: 'required'
				}], function(errors, event) {
				    var SELECTOR_ERRORS = $('.error_box'),
				        SELECTOR_SUCCESS = $('.success_box');

				    if (errors.length > 0) {
				        SELECTOR_ERRORS.empty();

				        for (var i = 0, errorLength = errors.length; i < errorLength; i++) {
				            SELECTOR_ERRORS.append(errors[i].message + '<br />');
				        }

				        SELECTOR_SUCCESS.css({ display: 'none' });
				        SELECTOR_ERRORS.fadeIn(200);
				    } else {
				        SELECTOR_ERRORS.css({ display: 'none' });
				        SELECTOR_SUCCESS.fadeIn(200);
				    }

				    if (event && event.preventDefault) {
				        event.preventDefault();
				    } else if (event) {
				        event.returnValue = false;
				    }
				});
       		});
       </script>
    </body>
</html>

如下是效果截图:

Validate-js

核心代码是new一个FormValidator表单验证对象。FormValiator构造函数的第一个参数是待验证表单的name属性值;第二个参数是一个对象数组,数组中的每个对象分别对应表单中的一个待验证域,每个对象有两个必填属性name和rules,name是域的name属性值,而rules就映射相应的验证规则了,display是可选参数,用于显示错误消息时指明是哪个域;第三个参数是一个回调函数,回调函数的第一个参数errors是验证失败对象数组,如果数组长度为0则表明全部验证通过,反之则验证失败,可以读取errors数组对象中的信息。


3、框架整体分析

整体框架如下:

/*
 * Valiate.js 1.2.1
 */
(function(window, document, undefined) {
   // 默认配置参数
   var defaults = {
      // 验证失败消息
      messages: {
         required: 'The %s field is required.',
         matches: 'The %s field does not match the %s field.',
         ...
      },
      // 默认回调函数
      callback: function(errors) {

      }
   }

   // 验证规则对应的正则表达式
   var ruleRegex = /^(.+?)\[(.+)\]$/,
        numericRegex = /^[0-9]+$/,
        integerRegex = /^\-?[0-9]+$/,
        ...;

   /*
    * 暴露给外面的表单验证公共构造函数
    * @param formName - String - form的name属性值(比如<form name="myForm"></form>中的myForm)
    * @param fields - Array - [{
    *     name: 域的name属性值(比如 <input name="myField" />)
    *     display: 'Field Name'
    *     rules: required|matches[password_confirm]
    * }]
    * @param callback - Function - 验证完成后的回调函数
    *     @argument errors - 验证错误对象的数组
    *     @argument event - 事件对象
    */
   var FormValidator = function(formName, fields, callback) {
      // 初始化回调函数、errors、fields、form、messages、handlers

      // 为表单的submit事件添加事件监听
      var _onsubmit = this.form.onsubmit;

      this.form.onsubmit = (function(that) {
         return function(event) {
            try {
               return that._validateForm(event) && (_onsubmit === undefined || _onsubmit());
            } catch(e) {}
         };
       })(this);
   };

   // 其他辅助方法
   ...

   window.FormValidator = FormValidator;
})(window, document);

     主要利用立即函数表达式传入window、document全局对象。然后通过 window.FormValidator = FormValidator; 将表单验证构造器函数导出为window对象的属性。在构造函数内通过formName去document中查找对应的表单。通过传入的fields数组(包含各个属性的名称、验证规则和display属性等)初始化this.fields对象。通过传入的callback初始化this.callback对象,如果不传入callback或者传入的callback参数为undefined,那么验证完成后的回调函数就是默认的defaults.callback,即空函数。

初始化完成后,为表单的submit事件注册回调函数。每次表单验证的最关键的入口就是这个回调函数了。而这个回调函数中关键的地方又是_validateForm(event)调用。它会结合前面的fields、form、errors、messages、callback等等完成对表单中各个域的验证。只有全部验证通过它才返回true。否则返回false,阻止表单的默认提交。

这里为什么要用一个函数表达式传入外部函数的this变量呢?因为内部函数不能直接访问外部函数的this变量,所以这里把外部作用域中的this对象保存到一个闭包能够访问到的变量里,就可以让闭包访问外部的this对象了。而内部函数的参数命名为that,完全出自命名惯例。

下面我们详细地追溯下整个表单验证流程。

 

4、详细表单验证流程

前面已经说了_validateForm(element)调用是最核心的入口。那么我们就定位到_validateForm函数(名称以下划线开头,表示是私有函数,也是一种命名惯例),如下:

/*
     * @private
     * Runs the validation when the form is submitted.
     */

    FormValidator.prototype._validateForm = function(event) {
        this.errors = [];

        for (var key in this.fields) {
            if (this.fields.hasOwnProperty(key)) {
                var field = this.fields[key] || {},
                    element = this.form[field.name];

                if (element && element !== undefined) {
                    field.id = attributeValue(element, 'id');
                    field.type = (element.length > 0) ? element[0].type : element.type;
                    field.value = attributeValue(element, 'value');
                    field.checked = attributeValue(element, 'checked');

                    /*
                     * Run through the rules for each field.
                     */

                    this._validateField(field);
                }
            }
        }

        if (typeof this.callback === 'function') {
            this.callback(this.errors, event);
        }

        if (this.errors.length > 0) {
            if (event && event.preventDefault) {
                event.preventDefault();
            } else {
                // IE6 doesn't pass in an event parameter so return false
                return false;
            }
        }

        return true;
    };

简单地说,这里主要是循环this.fields数组,根据里面保存的待验证域信息(最主要的是name和rules信息),先完善其他信息:id、type、value和checked(这里的id、type、value、checked和HTML的同名属性表示的意义一致,这里完善的信息会被后面的_validateField函数调用时用到),然后对每一个field调用_validateField(field)。

在_validateField(field)调用过程,如果发现某个域未能通过验证,则会往this.errors中添加相应信息。这样上面的对所有域调用_validateField(field)结束后,可以根据this.errors判断是否验证成功。如果提供了回调函数,那么会把this.errors对象传给回调函数,这样回调函数就可以根据errors的详细信息,自定义验证成功或失败时的行为。当然如果验证失败(this.errors.length>0)的话,会通过event.preventDefault或return false的方式阻止表单提交的。

接下来再看看_validateField函数:

/*
    * @private
    * Looks at the fields value and evaluates it against the given rules
    */

   FormValidator.prototype._validateField = function(field) {
       var rules = field.rules.split('|');

       /*
        * If the value is null and not required, we don't need to run through validation
        */

       if (field.rules.indexOf('required') === -1 && (!field.value || field.value === '' || field.value === undefined)) {
           return;
       }

       /*
        * Run through the rules and execute the validation methods as needed
        */

       for (var i = 0, ruleLength = rules.length; i < ruleLength; i++) {
           var method = rules[i],
               param = null,
               failed = false,
               parts = ruleRegex.exec(method);

           /*
            * If the rule has a parameter (i.e. matches[param]) split it out
            */

           if (parts) {
               method = parts[1];
               param = parts[2];
           }

           /*
            * If the hook is defined, run it to find any validation errors
            */

           if (typeof this._hooks[method] === 'function') {
               if (!this._hooks[method].apply(this, [field, param])) {
                   failed = true;
               }
           } else if (method.substring(0, 9) === 'callback_') {
               // Custom method. Execute the handler if it was registered
               method = method.substring(9, method.length);

               if (typeof this.handlers[method] === 'function') {
                   if (this.handlers[method].apply(this, [field.value]) === false) {
                       failed = true;
                   }
               }
           }

           /*
            * If the hook failed, add a message to the errors array
            */

           if (failed) {
               // Make sure we have a message for this rule
               var source = this.messages[method] || defaults.messages[method],
                   message = 'An error has occurred with the ' + field.display + ' field.';

               if (source) {
                   message = source.replace('%s', field.display);

                   if (param) {
                       message = message.replace('%s', (this.fields[param]) ? this.fields[param].display : param);
                   }
               }

               this.errors.push({
                   id: field.id,
                   name: field.name,
                   message: message,
                   rule: method
               });

               // Break out so as to not spam with validation errors (i.e. required and valid_email)
               break;
           }
       }
   };

_validateField函数第一步做的是提取filed的验证规则rules。之所以是rules,而不是rule,是因为可以为每个域指定多个验证规则,比如一个域是email并且不允许为空,那么它对应的rules就是”required|valid_email”,多个规则之间用 | 分隔。

_validateField函数第二步做了一个快速判断,如果field允许为空,且它确实为空,那么直接返回,不再做详细的验证(因为没必要)。

然后对解析得到的rules数组循环处理。这里我们先看下ruleRegex,它是下面的正则:

var ruleRegex = /^(.+?)\[(.+)\]$/,

这个正则用来匹配类似”abc[hello]”这种字符串,那在这里有什么用呢?不知道你是否注意到前面Demo里的”min_length[8]”,没错它就是用来匹配这种同时指定了验证类型和验证参数的情况。所以method毫无疑问提取出的是验证类型,而param如果有的话提取出的是验证参数。然后看下面的代码:

var failed = false;
...

if (typeof this._hooks[method] === 'function') {
   if (!this._hooks[method].apply(this, [field, param])) {
      failed = true;
   }
} else if (method.substring(0, 9) === 'callback_') {
   // Custom method. Execute the handler if it was registered
   method = method.substring(9, method.length);

   if (typeof this.handlers[method] === 'function') {
      if (this.handlers[method].apply(this, [field.value]) === false) {
         failed = true;
      }
   }
}

failed是一个标记量,用来标记单个域是否验证成功。我们这里先只关注

if (typeof this._hooks[method] === 'function') {

分支,另一个分支后面会讲到它的用处。这里的_hooks是验证钩子,定义如下(我截取了一部分,因为都类似):

/*
    * @private
    * Object containing all of the validation hooks
    */

   FormValidator.prototype._hooks = {
       required: function(field) {
           var value = field.value;

           if ((field.type === 'checkbox') || (field.type === 'radio')) {
               return (field.checked === true);
           }

           return (value !== null && value !== '');
       },

       valid_email: function(field) {
           return emailRegex.test(field.value);
       },
       ...
   };

if(!this._hooks[method].apply(this, [field, param])),比如method为”min_length”、param为8的情况下,this._hooks[“min_length”]得到的就是:

min_length: function(field, length) {
   if (!numericRegex.test(length)) {
      return false;
   }

   return (field.value.length >= parseInt(length, 10));
}

将field、param(值为8)传入这个函数,将先利用numericRegex(/^[0-9]+$/)这个数字对应的正则验证field值是否满足要求,然后如果还指定了 最小长度,则还会进一步验证。钩子函数调用完成后设置failed临时变量值。然后根据failed的值,来设置this.errors对象内容,如下:

/*
 * If the hook failed, add a message to the errors array
 */
if (failed) {
   // Make sure we have a message for this rule
   var source = this.messages[method] || defaults.messages[method],
   message = 'An error has occurred with the ' + field.display + ' field.';

   if (source) {
      message = source.replace('%s', field.display);

      if (param) {
         message = message.replace('%s', (this.fields[param]) ? this.fields[param].display : param);
      }
   }

   this.errors.push({
      id: field.id,
      name: field.name,
      message: message,
      rule: method
   });

   // Break out so as to not spam with validation errors (i.e. required and valid_email)
   break;
}

这里先搞清楚messages是什么东西,它是域验证失败时的要显示的错误消息模板。代码已经内置了一些错误消息模板,如下:

var defaults = {
        messages: {
            required: 'The %s field is required.',
            matches: 'The %s field does not match the %s field.',
            valid_email: 'The %s field must contain a valid email address.',
        },
        ...
};

那为什么像下面这样用呢?

var source = this.messages[method] || defaults.messages[method]

这是有趣的地方之一,这代表我们可以为某个规则自定义错误消息模板,自定义的错误消息模板存在this.messages对象中,并且自定义的错误消息模板优先于默认的错误消息模板。通过什么方法自定义呢?别急,搜索下代码哪里设置过this.messages,很快就找到下面这个函数:

/*
 * @public
 * Sets a custom message for one of the rules
 */
FormValidator.prototype.setMessage = function(rule, message) {
   this.messages[rule] = message;

   // return this for chaining
   return this;
};

代码很简单,值得一提的是return this;,这行代码可谓“化腐朽为神奇”,这么一小段代码就使得setMessage方法可以链式调用,比如:

validator
   .setMessage('check_email', 'That email is already taken. Please choose another.')
   .setMessage('required', 'This field should not be empty!');
         

好了,基本的框架都分析得差不多了,最后提一提前面略过的代码:

else if (method.substring(0, 9) === 'callback_') {
   // Custom method. Execute the handler if it was registered
   method = method.substring(9, method.length);

   if (typeof this.handlers[method] === 'function') {
      if (this.handlers[method].apply(this, [field.value]) === false) {
         failed = true;
      }
   }
}

这是validate.js的另外一个有趣的地方,即允许自己扩展验证规则。

具体怎么扩展呢?

首先为某个域指定rule为下面这样:

rules: 'required|callback_check_email'

然后通过registerCallback函数注册自定义钩子验证回调函数:

validator.registerCallback('check_email', function(value) {
    if (emailIsUnique(value)) {
        return true;
    }
    
    return false;
});

registerCallback的实现非常简单,只是用一个handlers对象来维护自定义的钩子回调函数:

FormValidator.prototype.registerCallback = function(name, handler) {
   if (name && typeof name === 'string' && handler && typeof handler === 'function') {
      this.handlers[name] = handler;
   }

   // return this for chaining
   return this;
};
至

于为什么扩展验证规则的名称必须以callback_开头,相信你看到下面这行代码就能心领神会了!

else if (method.substring(0, 9) === 'callback_') {

5、涉及的一些有用正则

正则表达式几乎是任何一门高级编程语言中的重要组成部分,各种JavaScript框架中也经常出现它们的身影,validate.js自然也如此。

我这里就提出几个我认为蛮有用的正则:

// IP地址正则
var ipRegex = /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})$/i;

// Base64编码对应正则
base64Regex = /[^a-zA-Z0-9\/\+=]/i;

// URL对应正则
var urlRegex = /^((http|https):\/\/(\w+:{0,1}\w*@)?(\S+)|)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;

// Email对应正则
var emailRegex = /^[a-zA-Z0-9.!#$%&amp;'*+\-\/=?\^_`{|}~\-]+@[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*$/;

结束语:

熬了这么久终于趁今晚抽出时间把这篇博客写完了,也算是免去虎头蛇尾之嫌了!

参考:

http://rickharrison.github.com/validate.js/

https://github.com/rickharrison/validate.js/blob/master/validate.js

吵吵微信朋友圈,请付款实名加入:

吵吵 吵吵

2条回应:“Valide.js框架源码完全解读”

  1. 工厂标语说道:

    专业性强

发表评论

电子邮件地址不会被公开。 必填项已用*标注