AJAX工作原理,底层代码分析
Discuz的ajax原理实际上是很简单的,当然,说到ajax肯定是用XMLHttpRequest这个对象了,不过我曾经也看到国外的牛人写过一篇 文章叫做不用XMLHttpRequest对象来实现ajax,而且还解决了跨域的问题,真是大开了眼界~!好了,说正事,Dz的ajax用到的文件不是 很多,列举如下:
工作原理如下图:
有了这些理论的基础就可以分析一下ajax的具体实现了,下面就以注册过程中的检查用户名在数据库是不是存在并给用户提示这样一个ajax过程进行全程分析。
用到如下的几个文件:
其实register.php这个文件没什么多大的关系,不过register.htm模板是通过它解析过来的,所以就提出来了。
大家都知道在注册过程中是在输完用户名并输入密码的时候才会触发事件检查是不是用户存在,所以很明显,这个是input的onBlur事件。
好了,现在看看./templates/default/register.htm这个文件。
接下来当然是要分析这个ajax.php是怎么一回事,它做了哪些使function(s)中能返回我们要的东西。由于只分析检查用户名这一个部分,我这里就只分析action=checkuser这一部分了。
./include/javascript/common.js
这个文件把Discuz用到的许多javascript的代码(主要为函数和浏览者的浏览器的判断等等)
./include/javascript/ajax.js
不用看就知道是创建一个可用的XMLHttpRequest对象用的(由于XMLHttpRequest在各个浏览器的创建不同,因此要对各个不同的浏览 器进行不同的创建,还好prototype.js中有一个通用的东东。当然,这里也封装了get, post这类的函数,刚发现5.5封装了更多的东西,有ajaxmenu,updatesecqaa,ignorepm。
./ajax.php
这个文件是ajax的后台处理文件,作用相当于MVC三层中的M(Model)和C(Controller)层,因为它负责和后台数据库通信并返回和处理 一些信息,比如新的用户在注册的时候通过XMLHttpRequest向ajax.php发送一个请求,这个请求是通过带参数用GET发出 去,ajax.php检查数据库中用户名是不是存在,并用global.func.php中定义的ajaxtemplate调用 showmessage_ajax模板返回一个XML文档,ajax这个对象实际上是解析这个xml文档的,具体的解析就是返回root这个元素中的 CDATA中的值,再用register.htm中的javascript调用div对象的innerhtml方法给login模板中的一个div给前端 用户提示用户是不是存在。
工作原理如下图:
Section I 部分common.js函数分析
这里用到ajax部分的不是很多,我只把几个核心的弄出来说一下,要不然工程太大了。。复制代码
- var userAgent = navigator.userAgent.toLowerCase();
- var is_webtv = userAgent.indexOf('webtv') != -1;
- var is_kon = userAgent.indexOf('konqueror') != -1;
- var is_mac = userAgent.indexOf('mac') != -1;
- var is_saf = userAgent.indexOf('applewebkit') != -1 || navigator.vendor == 'Apple Computer, Inc.';
- var is_opera = userAgent.indexOf('opera') != -1 && opera.version();
- var is_moz = (navigator.product == 'Gecko' && !is_saf) && userAgent.substr(userAgent.indexOf('firefox') + 8, 3);
- var is_ns = userAgent.indexOf('compatible') == -1 && userAgent.indexOf('mozilla') != -1 && !is_opera && !is_webtv && !is_saf;
- var is_ie = (userAgent.indexOf('msie') != -1 && !is_opera && !is_saf && !is_webtv) && userAgent.substr(userAgent.indexOf('msie') + 5, 3);
这一部分是用来判断来访者的浏览器类型的,很重要的一部分判断。对于构建跨平台的显示效果有至关重要的作用~!
复制代码
- function $(id) {
- return document.getElementById(id);
- }
呵呵,prototype那个开发牛人想出来的东东,用来快速得到一个对象。用法基本上是 var myObj = $('ojbId);想想省了多少打字的时间~!
复制代码
- function in_array(needle, haystack) {
- if(typeof needle == 'string') {
- for(var i in haystack) {
- if(haystack[i] == needle) {
- return true;
- }
- }
- }
- return false;
- }
这个函数可以算一个php函数用到了javascript上,作用就是检查是不是needle在haystack这个数组中。
复制代码
- function arraypush(a, value) {
- a[a.length] = value;
- return a.length;
- }
同样可以算一个php函数用到了javascript上,作用是把value这个值插到a这个数组的最后一位。
./include/javascript/ajax.js 分析
复制代码
- var Ajaxs = new Array();
- function Ajax(recvType, statusId) {
- var aj = new Object();
- aj.statusId = statusId ? document.getElementById(statusId) : null;
- aj.targetUrl = '';
- aj.sendString = '';
- aj.recvType = recvType ? recvType : 'XML';
- aj.resultHandle = null;
- aj.createXMLHttpRequest = function() {
- var request = false;
- if(window.XMLHttpRequest) {
- request = new XMLHttpRequest();
- if(request.overrideMimeType) {
- request.overrideMimeType('text/xml');
- }
- } else if(window.ActiveXObject) {
- var versions = ['Microsoft.XMLHTTP', 'MSXML.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.7.0', 'Msxml2.XMLHTTP.6.0', 'Msxml2.XMLHTTP.5.0', 'Msxml2.XMLHTTP.4.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP'];
- for(var i=0; i<versions.length; i++) {
- try {
- request = new ActiveXObject(versions[i]);
- if(request) {
- return request;
- }
- } catch(e) {
- //alert(e.message);
- }
- }
- }
- return request;
- }
- aj.XMLHttpRequest = aj.createXMLHttpRequest();
这一段是想尽一切办法建立一个XMLHttpRequest对象,无论是什么浏览器都能通用了。调用的时候是一个函数Ajax,有两个传入函数 recvType和statusId,recvType是ajax返回值的接受类型,有HTML和XML两种类型,Dz一般用的是XML类 型;statusID这个是用来指示状态的div。
复制代码
- aj.processHandle = function() {
- if(aj.statusId) {
- aj.statusId.style.display = '';
- }
- if(aj.XMLHttpRequest.readyState == 1 && aj.statusId) {
- aj.statusId.innerHTML = xml_http_building_link;
- } else if(aj.XMLHttpRequest.readyState == 2 && aj.statusId) {
- aj.statusId.innerHTML = xml_http_sending;
- } else if(aj.XMLHttpRequest.readyState == 3 && aj.statusId) {
- aj.statusId.innerHTML = xml_http_loading;
- } else if(aj.XMLHttpRequest.readyState == 4) {
- if(aj.XMLHttpRequest.status == 200) {
- for(k in Ajaxs) {
- if(Ajaxs[k] == aj.targetUrl) {
- Ajaxs[k] = null;
- }
- }
- if(aj.statusId) {
- aj.statusId.innerHTML = xml_http_data_in_processed;
- aj.statusId.style.display = 'none';
- }
- if(aj.recvType == 'HTML') {
- aj.resultHandle(aj.XMLHttpRequest.responseText, aj);
- } else if(aj.recvType == 'XML') {
- aj.resultHandle(aj.XMLHttpRequest.responseXML.lastChild.firstChild.nodeValue, aj);
- }
- } else {
- if(aj.statusId) {
- aj.statusId.innerHTML = xml_http_load_failed;
- }
- }
- }
- }
Ajax实例化后的对象aj的processHandle方法,作用当然就是处理ajax请求过程的函数。
具体作用有两点:
第一点,那就是对statusId这个div进行ajax请求过程全程提示,在这段代码的前三分之一的样子就是做这个用的。
注意在register.htm中有对过程的定义,以下的代码引用自./templates/default/register.htm
复制代码
- var profile_seccode_invalid = '{lang register_profile_seccode_invalid}';
- var profile_secanswer_invalid = '{lang register_profile_secqaa_invalid}';
- var profile_username_toolong = '{lang register_profile_username_toolong}';
- var profile_username_tooshort = '{lang register_profile_profile_username_tooshort}';
- var profile_username_illegal = '{lang register_profile_username_illegal}';
- var profile_passwd_illegal = '{lang register_profile_passwd_illegal}';
- var profile_passwd_notmatch = '{lang register_profile_passwd_notmatch}';
- var profile_email_illegal = '{lang register_profile_email_illegal}';
- var profile_email_invalid = '{lang register_profile_email_invalid}';
- var profile_email_censor = '{lang register_profile_email_censor}';
- var profile_email_msn = '{lang register_profile_email_msn}';
- var doublee = parseInt('$doublee');
- var lastseccode = lastsecanswer = lastusername = lastpassword = lastemail = '';
- var xml_http_building_link = '{lang xml_http_building_link}';
- var xml_http_sending = '{lang xml_http_sending}';
- var xml_http_loading = '{lang xml_http_loading}';
- var xml_http_load_failed = '{lang xml_http_load_failed}';
- var xml_http_data_in_processed = '{lang xml_http_data_in_processed}';
这个便是statusId具体中要提示的文字了,之所以要这样写当然是为了方便多语言。
第二点是最重要的,当XMLHttpRequest.status=200的时候,那么就表示请求成功并返回了东西,这个时候就用 resultHandle这个函数对返回的东西进行处理,可以看到还是分为HTML和XML两种情况分别调用不同的方法,一个是responsText一 个是responseXML。
这里是ajax的两个方法,一个是get,用来提交get数据的,比方说discuz对于注册的时候ajax的表情显示就是get方法;另外一个是post,用来提交post数据。复制代码
- aj.get = function(targetUrl, resultHandle) {
- if(in_array(targetUrl, Ajaxs)) {
- return false;
- } else {
- arraypush(Ajaxs, targetUrl);
- }
- aj.targetUrl = targetUrl;
- aj.XMLHttpRequest.onreadystatechange = aj.processHandle;
- aj.resultHandle = resultHandle;
- if(window.XMLHttpRequest) {
- aj.XMLHttpRequest.open('GET', aj.targetUrl);
- aj.XMLHttpRequest.send(null);
- } else {
- aj.XMLHttpRequest.open("GET", targetUrl, true);
- aj.XMLHttpRequest.send();
- }
- }
- aj.post = function(targetUrl, sendString, resultHandle) {
- if(in_array(targetUrl, Ajaxs)) {
- return false;
- } else {
- arraypush(Ajaxs, targetUrl);
- }
- aj.targetUrl = targetUrl;
- aj.sendString = sendString;
- aj.XMLHttpRequest.onreadystatechange = aj.processHandle;
- aj.resultHandle = resultHandle;
- aj.XMLHttpRequest.open('POST', targetUrl);
- aj.XMLHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
- aj.XMLHttpRequest.send(aj.sendString);
- }
- return aj;
- }
targetUrl, resultHandle这两个参数分别是要请求的地址和处理函数。
用到如下的几个文件:
./register.php
./ajax.php
./include/javascript/common.js
./include/javascript/ajax.js
./template/default/register.htm
其实register.php这个文件没什么多大的关系,不过register.htm模板是通过它解析过来的,所以就提出来了。
大家都知道在注册过程中是在输完用户名并输入密码的时候才会触发事件检查是不是用户存在,所以很明显,这个是input的onBlur事件。
好了,现在看看./templates/default/register.htm这个文件。
复制代码
- <tr>
- <td class="altbg1" width="21%"><span class="bold">{lang username}</span></td>
- <td class="altbg2"><div class="input"><input type="text" name="username" size="25" maxlength="15" id="username" onBlur="checkusername()"></div><div id="checkusername"></div>
- </td>
- </tr>
很简单的HTML代码,注意看到 onBlur="checkusername()"这里,继续往下深入,看看这个函数是做什么的。
复制代码
- function checkusername() {
- var username = trim($('username').value);
- if(username == lastusername) {
- return;
- } else {
- lastusername = username;
- }
- var cu = $('checkusername');
- var unlen = username.replace(/[^\x00-\xff]/g, "**").length;
- if(unlen < 3 || unlen > 15) {
- warning(cu, unlen < 3 ? profile_username_tooshort : profile_username_toolong);
- return;
- }
- ajaxresponse('checkusername', 'action=checkusername&username=' + username);
- }
第一行是得到username的值,也就是我们输入的,lastusername应该是我们在返回的时候不会让用户名消失用的,应该是写入cookie或者是其他。
接下来判断经过了替换的用户名是不是大于15或者小于3,是的话直接调用warning这个函数(稍后讲这样一个函数)并返回,不是的话就调用ajaxresponse函数发出ajax请求,看看ajaxresponse这个函数就是关键所在了。
复制代码
- function ajaxresponse(objname, data) {
- var x = new Ajax('XML', objname);
- x.get('ajax.php?inajax=1&' + data, function(s){
- var obj = $(objname);
- if(s == 'succeed') {
- obj.style.display = '';
- obj.innerHTML = '<img src="{IMGDIR}/check_right.gif" width="13" height="13">';
- obj.className = "warning";
- } else {
- warning(obj, s);
- }
- });
- }
ajaxresponse函数来了,这个作用就是实例化一个ajax对象,注意到我们先前说的recvType,就是第一个传入参数,这里固定成了 XML,Discuz喜欢用XML(^^),然后发出请求,这里全部用的是get(第二行),地址是ajax.php?inajax=1&加上传 入的参数,所以结合上面就变成:ajax.php?inajax=1&action=checkusername&username=用 户名,构造出来了一个URL,(大家可以自己测试一下看看返回的是什么东东,通过http://你的URL/ajax.php?inajax=1& amp;action=checkusername&username=用户名)通过XMLHttpRequest发出去,注意那个处理函 数:function(s),实际这个函数作为参数已经写出来了,如果最后返回的是succed,那么就在obj这个层里(在这个例子中对应 id='checkusername'这个层)显示一个带勾的图,否则的话还是调用warning这个函数。下面就分析这个函数。
复制代码
- function warning(obj, msg) {
- if((ton = obj.id.substr(5, obj.id.length)) != 'password2') {
- $(ton).select();
- }
- obj.style.display = '';
- obj.innerHTML = '<img src="{IMGDIR}/check_error.gif" width="13" height="13"> ' + msg;
- obj.className = "warning";
- }
warning函数前面两次提到,其实这个函数很简单,实现的作用就是在obj这个层里打一个叉,然后写上错误的原因,把obj这个层的CSS class设置成为warning。当然,最开始也验证了一下两次密码是否一致,这里就不说了。
接下来当然是要分析这个ajax.php是怎么一回事,它做了哪些使function(s)中能返回我们要的东西。由于只分析检查用户名这一个部分,我这里就只分析action=checkuser这一部分了。
复制代码
- elseif($action == 'checkusername') {
- $username = trim($username);
- $guestexp = '\xA1\xA1|^Guest|^\xD3\xCE\xBF\xCD|\xB9\x43\xAB\xC8';
- $censorexp = '/^('.str_replace(array('\\*', "\r\n", ' '), array('.*', '|', ''), preg_quote(($censoruser = trim($censoruser)), '/')).')$/i';
- if(preg_match("/^\s*$|^c:\\con\\con$|[%,\*\"\s\t\<\>\&]|$guestexp/is", $username) || ($censoruser && @preg_match($censorexp, $username))) {
- showmessage('profile_username_illegal');
- }
- $query = $db->query("SELECT uid FROM {$tablepre}members WHERE username='$username'");
- $username = dhtmlspecialchars(stripslashes($username));
- if($db->num_rows($query)) {
- showmessage('register_check_found');
- }
这里可以看到是标准的php判断了,有点点php基础就能看懂了,基本上的功能就是判断一个用户是不是在后台设置的禁用用户名中。
是的话就showmessage不合法(注:这里的showmessage不是我们理解的那个跳转,而是一个xml文档,为什么会这样我等会会介绍)
然后就从数据库找是不是有这样一个用户,如果是的话就showmessage 发现了已注册的用户名,不是话就都跳过,直接到最后的:
复制代码
- showmessage('succeed');
注意当所有的判断都成功的话就说明合法了,会调用showmessage来显示一个succeed。
最后说一下为什么这里的showmessage不是我们理解的那个跳转了。
注意在register.htm中的ajaxresponse函数有这样一句:x.get('ajax.php?inajax=1&' + data, function(s){
对了,inajax=1,就是这么一个参数,showmessage就天差万别了。如果你不记得showmessage这个函数是什么样了,请参考:
http://www.discuz.net/viewthread.php?tid=612195
和
http://www.discuz.net/viewthread.php?tid=612197,我在这里分析了一下。