Discuz!官方免费开源建站系统

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索

史上第一强:Discuz!5.5源代码分析系列(5)-AJAX工作原理,底层代码分析

[复制链接]
郭鑫 发表于 2007-5-9 19:12:46 | 显示全部楼层 |阅读模式
AJAX提到这个名字,大家肯定相当熟悉,Google把它发挥到淋漓尽致,之后它一举成名,成为Web2.0的新宠,代名词。Discuz在若干地方用到了这个技术,现在我就从底层代码分析一下Discuz的AJAX工作原理。

申明下版权:
1.这里面的每个中文字都是我打的,code部分是引用的,当然我也加了一点注释在里面了。
2.如果要转载的话请注明
  1. 转自 [url]discuz.dismall.com[/url] 作者:郭鑫
复制代码

3.由于我个人的能力有限,写这篇文章没有参考一点资料,甚至连本地环境也没有搭建(遇到了白屏问题),所以难免会有错误的地方,大家发现了的话请跟帖或者联系我吧,我会尽快更正。




注:本帖子适合有Javascript, PHP, XML, HTML等相关知识的网站开发人员。
 楼主| 郭鑫 发表于 2007-5-9 19:13:23 | 显示全部楼层
Discuz的ajax原理实际上是很简单的,当然,说到ajax肯定是用XMLHttpRequest这个对象了,不过我曾经也看到国外的牛人写过一篇文章叫做不用XMLHttpRequest对象来实现ajax,而且还解决了跨域的问题,真是大开了眼界~!好了,说正事,Dz的ajax用到的文件不是很多,列举如下:

./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给前端用户提示用户是不是存在。


工作原理如下图:
回复

使用道具 举报

 楼主| 郭鑫 发表于 2007-5-9 19:14:02 | 显示全部楼层

Section I 部分common.js函数分析

这里用到ajax部分的不是很多,我只把几个核心的弄出来说一下,要不然工程太大了。。

  1. var userAgent = navigator.userAgent.toLowerCase();
  2. var is_webtv = userAgent.indexOf('webtv') != -1;
  3. var is_kon = userAgent.indexOf('konqueror') != -1;
  4. var is_mac = userAgent.indexOf('mac') != -1;
  5. var is_saf = userAgent.indexOf('applewebkit') != -1 || navigator.vendor == 'Apple Computer, Inc.';
  6. var is_opera = userAgent.indexOf('opera') != -1 && opera.version();
  7. var is_moz = (navigator.product == 'Gecko' && !is_saf) && userAgent.substr(userAgent.indexOf('firefox') + 8, 3);
  8. var is_ns = userAgent.indexOf('compatible') == -1 && userAgent.indexOf('mozilla') != -1 && !is_opera && !is_webtv && !is_saf;
  9. var is_ie = (userAgent.indexOf('msie') != -1 && !is_opera && !is_saf && !is_webtv) && userAgent.substr(userAgent.indexOf('msie') + 5, 3);
复制代码

这一部分是用来判断来访者的浏览器类型的,很重要的一部分判断。对于构建跨平台的显示效果有至关重要的作用~!

  1. function $(id) {
  2.         return document.getElementById(id);
  3. }
复制代码

呵呵,prototype那个开发牛人想出来的东东,用来快速得到一个对象。用法基本上是 var myObj = $('ojbId);想想省了多少打字的时间~!


  1. function in_array(needle, haystack) {
  2.         if(typeof needle == 'string') {
  3.                 for(var i in haystack) {
  4.                         if(haystack[i] == needle) {
  5.                                         return true;
  6.                         }
  7.                 }
  8.         }
  9.         return false;
  10. }
复制代码

这个函数可以算一个php函数用到了javascript上,作用就是检查是不是needle在haystack这个数组中。


  1. function arraypush(a, value) {
  2.         a[a.length] = value;
  3.         return a.length;
  4. }
复制代码

同样可以算一个php函数用到了javascript上,作用是把value这个值插到a这个数组的最后一位。
回复

使用道具 举报

 楼主| 郭鑫 发表于 2007-5-9 19:14:20 | 显示全部楼层

./include/javascript/ajax.js 分析

  1. var Ajaxs = new Array();
  2. function Ajax(recvType, statusId) {
  3.         var aj = new Object();
  4.         aj.statusId = statusId ? document.getElementById(statusId) : null;
  5.         aj.targetUrl = '';
  6.         aj.sendString = '';
  7.         aj.recvType = recvType ? recvType : 'XML';
  8.         aj.resultHandle = null;

  9.         aj.createXMLHttpRequest = function() {
  10.                 var request = false;
  11.                 if(window.XMLHttpRequest) {
  12.                         request = new XMLHttpRequest();
  13.                         if(request.overrideMimeType) {
  14.                                 request.overrideMimeType('text/xml');
  15.                         }
  16.                 } else if(window.ActiveXObject) {
  17.                         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'];
  18.                         for(var i=0; i<versions.length; i++) {
  19.                                 try {
  20.                                         request = new ActiveXObject(versions[i]);
  21.                                         if(request) {
  22.                                                 return request;
  23.                                         }
  24.                                 } catch(e) {
  25.                                         //alert(e.message);
  26.                                 }
  27.                         }
  28.                 }
  29.                 return request;
  30.         }

  31.         aj.XMLHttpRequest = aj.createXMLHttpRequest();
复制代码

这一段是想尽一切办法建立一个XMLHttpRequest对象,无论是什么浏览器都能通用了。调用的时候是一个函数Ajax,有两个传入函数recvType和statusId,recvType是ajax返回值的接受类型,有HTML和XML两种类型,Dz一般用的是XML类型;statusID这个是用来指示状态的div。

       

  1.         aj.processHandle = function() {
  2.                 if(aj.statusId) {
  3.                         aj.statusId.style.display = '';
  4.                 }
  5.                 if(aj.XMLHttpRequest.readyState == 1 && aj.statusId) {
  6.                         aj.statusId.innerHTML = xml_http_building_link;
  7.                 } else if(aj.XMLHttpRequest.readyState == 2 && aj.statusId) {
  8.                         aj.statusId.innerHTML = xml_http_sending;
  9.                 } else if(aj.XMLHttpRequest.readyState == 3 && aj.statusId) {
  10.                         aj.statusId.innerHTML = xml_http_loading;
  11.                 } else if(aj.XMLHttpRequest.readyState == 4) {
  12.                         if(aj.XMLHttpRequest.status == 200) {
  13.                                 for(k in Ajaxs) {
  14.                                         if(Ajaxs[k] == aj.targetUrl) {
  15.                                                 Ajaxs[k] = null;
  16.                                         }
  17.                                 }

  18.                                 if(aj.statusId) {
  19.                                         aj.statusId.innerHTML = xml_http_data_in_processed;
  20.                                         aj.statusId.style.display = 'none';
  21.                                 }
  22.                                 if(aj.recvType == 'HTML') {
  23.                                         aj.resultHandle(aj.XMLHttpRequest.responseText, aj);
  24.                                 } else if(aj.recvType == 'XML') {
  25.                                         aj.resultHandle(aj.XMLHttpRequest.responseXML.lastChild.firstChild.nodeValue, aj);
  26.                                 }
  27.                         } else {
  28.                                 if(aj.statusId) {
  29.                                         aj.statusId.innerHTML = xml_http_load_failed;
  30.                                 }
  31.                         }
  32.                 }
  33.         }
复制代码

        Ajax实例化后的对象aj的processHandle方法,作用当然就是处理ajax请求过程的函数。
        具体作用有两点:
        第一点,那就是对statusId这个div进行ajax请求过程全程提示,在这段代码的前三分之一的样子就是做这个用的。
        注意在register.htm中有对过程的定义,以下的代码引用自./templates/default/register.htm

  1.                 var profile_seccode_invalid = '{lang register_profile_seccode_invalid}';
  2.                 var profile_secanswer_invalid = '{lang register_profile_secqaa_invalid}';
  3.                 var profile_username_toolong = '{lang register_profile_username_toolong}';
  4.                 var profile_username_tooshort = '{lang register_profile_profile_username_tooshort}';
  5.                 var profile_username_illegal = '{lang register_profile_username_illegal}';
  6.                 var profile_passwd_illegal = '{lang register_profile_passwd_illegal}';
  7.                 var profile_passwd_notmatch = '{lang register_profile_passwd_notmatch}';
  8.                 var profile_email_illegal = '{lang register_profile_email_illegal}';
  9.                 var profile_email_invalid = '{lang register_profile_email_invalid}';
  10.                 var profile_email_censor = '{lang register_profile_email_censor}';
  11.                 var profile_email_msn = '{lang register_profile_email_msn}';
  12.                 var doublee = parseInt('$doublee');
  13.                 var lastseccode = lastsecanswer = lastusername = lastpassword = lastemail = '';
  14.                 var xml_http_building_link = '{lang xml_http_building_link}';
  15.                 var xml_http_sending = '{lang xml_http_sending}';
  16.                 var xml_http_loading = '{lang xml_http_loading}';
  17.                 var xml_http_load_failed = '{lang xml_http_load_failed}';
  18.                 var xml_http_data_in_processed = '{lang xml_http_data_in_processed}';
  19.        
复制代码

        这个便是statusId具体中要提示的文字了,之所以要这样写当然是为了方便多语言。
               
        第二点是最重要的,当XMLHttpRequest.status=200的时候,那么就表示请求成功并返回了东西,这个时候就用resultHandle这个函数对返回的东西进行处理,可以看到还是分为HTML和XML两种情况分别调用不同的方法,一个是responsText一个是responseXML。

               

  1.         aj.get = function(targetUrl, resultHandle) {
  2.                 if(in_array(targetUrl, Ajaxs)) {
  3.                         return false;
  4.                 } else {
  5.                         arraypush(Ajaxs, targetUrl);
  6.                 }
  7.                 aj.targetUrl = targetUrl;
  8.                 aj.XMLHttpRequest.onreadystatechange = aj.processHandle;
  9.                 aj.resultHandle = resultHandle;
  10.                 if(window.XMLHttpRequest) {
  11.                         aj.XMLHttpRequest.open('GET', aj.targetUrl);
  12.                         aj.XMLHttpRequest.send(null);
  13.                 } else {
  14.                         aj.XMLHttpRequest.open("GET", targetUrl, true);
  15.                         aj.XMLHttpRequest.send();
  16.                 }
  17.         }

  18.         aj.post = function(targetUrl, sendString, resultHandle) {
  19.                 if(in_array(targetUrl, Ajaxs)) {
  20.                         return false;
  21.                 } else {
  22.                         arraypush(Ajaxs, targetUrl);
  23.                 }
  24.                 aj.targetUrl = targetUrl;
  25.                 aj.sendString = sendString;
  26.                 aj.XMLHttpRequest.onreadystatechange = aj.processHandle;
  27.                 aj.resultHandle = resultHandle;
  28.                 aj.XMLHttpRequest.open('POST', targetUrl);
  29.                 aj.XMLHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  30.                 aj.XMLHttpRequest.send(aj.sendString);
  31.         }
  32.         return aj;
  33. }
复制代码
这里是ajax的两个方法,一个是get,用来提交get数据的,比方说discuz对于注册的时候ajax的表情显示就是get方法;另外一个是post,用来提交post数据。
targetUrl, resultHandle这两个参数分别是要请求的地址和处理函数。
回复

使用道具 举报

 楼主| 郭鑫 发表于 2007-5-9 19:14:34 | 显示全部楼层
有了这些理论的基础就可以分析一下ajax的具体实现了,下面就以注册过程中的检查用户名在数据库是不是存在并给用户提示这样一个ajax过程进行全程分析。
用到如下的几个文件:
./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这个文件。


  1. <tr>
  2.         <td class="altbg1" width="21%"><span class="bold">{lang username}</span></td>
  3.         <td class="altbg2"><div class="input"><input type="text" name="username" size="25" maxlength="15" id="username" onBlur="checkusername()"></div><div id="checkusername"></div>
  4.         </td>
  5.         </tr>
复制代码

很简单的HTML代码,注意看到 onBlur="checkusername()"这里,继续往下深入,看看这个函数是做什么的。


  1.         function checkusername() {
  2.                 var username = trim($('username').value);
  3.                 if(username == lastusername) {
  4.                         return;
  5.                 } else {
  6.                         lastusername = username;
  7.                 }
  8.                 var cu = $('checkusername');
  9.                 var unlen = username.replace(/[^\x00-\xff]/g, "**").length;

  10.                 if(unlen < 3 || unlen > 15) {
  11.                         warning(cu, unlen < 3 ? profile_username_tooshort : profile_username_toolong);
  12.                         return;
  13.                 }
  14.                 ajaxresponse('checkusername', 'action=checkusername&username=' + username);
  15.         }
复制代码

第一行是得到username的值,也就是我们输入的,lastusername应该是我们在返回的时候不会让用户名消失用的,应该是写入cookie或者是其他。
接下来判断经过了替换的用户名是不是大于15或者小于3,是的话直接调用warning这个函数(稍后讲这样一个函数)并返回,不是的话就调用ajaxresponse函数发出ajax请求,看看ajaxresponse这个函数就是关键所在了。


  1.         function ajaxresponse(objname, data) {
  2.                 var x = new Ajax('XML', objname);
  3.                 x.get('ajax.php?inajax=1&' + data, function(s){
  4.                         var obj = $(objname);
  5.                         if(s == 'succeed') {
  6.                                 obj.style.display = '';
  7.                                 obj.innerHTML = '<img src="{IMGDIR}/check_right.gif" width="13" height="13">';
  8.                                 obj.className = "warning";
  9.                         } else {
  10.                                 warning(obj, s);
  11.                         }
  12.                 });
  13.         }
复制代码

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&action=checkusername&username=用户名)通过XMLHttpRequest发出去,注意那个处理函数:function(s),实际这个函数作为参数已经写出来了,如果最后返回的是succed,那么就在obj这个层里(在这个例子中对应id='checkusername'这个层)显示一个带勾的图,否则的话还是调用warning这个函数。下面就分析这个函数。


  1.         function warning(obj, msg) {
  2.                 if((ton = obj.id.substr(5, obj.id.length)) != 'password2') {
  3.                         $(ton).select();
  4.                 }
  5.                 obj.style.display = '';
  6.                 obj.innerHTML = '<img src="{IMGDIR}/check_error.gif" width="13" height="13">   ' + msg;
  7.                 obj.className = "warning";
  8.         }
复制代码

warning函数前面两次提到,其实这个函数很简单,实现的作用就是在obj这个层里打一个叉,然后写上错误的原因,把obj这个层的CSS class设置成为warning。当然,最开始也验证了一下两次密码是否一致,这里就不说了。


接下来当然是要分析这个ajax.php是怎么一回事,它做了哪些使function(s)中能返回我们要的东西。由于只分析检查用户名这一个部分,我这里就只分析action=checkuser这一部分了。

  1. elseif($action == 'checkusername') {

  2.                 $username = trim($username);

  3.                 $guestexp = '\xA1\xA1|^Guest|^\xD3\xCE\xBF\xCD|\xB9\x43\xAB\xC8';
  4.                 $censorexp = '/^('.str_replace(array('\\*', "\r\n", ' '), array('.*', '|', ''), preg_quote(($censoruser = trim($censoruser)), '/')).')$/i';
  5.                 if(preg_match("/^\s*$|^c:\\con\\con$|[%,\*"\s\t\<\>\&]|$guestexp/is", $username) || ($censoruser && @preg_match($censorexp, $username))) {
  6.                         showmessage('profile_username_illegal');
  7.                 }

  8.                 $query = $db->query("SELECT uid FROM {$tablepre}members WHERE username='$username'");
  9.                 $username = dhtmlspecialchars(stripslashes($username));

  10.                 if($db->num_rows($query)) {
  11.                         showmessage('register_check_found');
  12.                 }
复制代码

这里可以看到是标准的php判断了,有点点php基础就能看懂了,基本上的功能就是判断一个用户是不是在后台设置的禁用用户名中。
是的话就showmessage不合法(注:这里的showmessage不是我们理解的那个跳转,而是一个xml文档,为什么会这样我等会会介绍
然后就从数据库找是不是有这样一个用户,如果是的话就showmessage 发现了已注册的用户名,不是话就都跳过,直接到最后的:

  1. showmessage('succeed');
复制代码

注意当所有的判断都成功的话就说明合法了,会调用showmessage来显示一个succeed。
最后说一下为什么这里的showmessage不是我们理解的那个跳转了。

注意在register.htm中的ajaxresponse函数有这样一句:x.get('ajax.php?inajax=1&' + data, function(s){
对了,inajax=1,就是这么一个参数,showmessage就天差万别了。如果你不记得showmessage这个函数是什么样了,请参考:
https://discuz.dismall.com/viewthread.php?tid=612195

https://discuz.dismall.com/viewthread.php?tid=612197,我在这里分析了一下。
回复

使用道具 举报

 楼主| 郭鑫 发表于 2007-5-9 19:14:45 | 显示全部楼层
place holder for update
回复

使用道具 举报

jopt 发表于 2007-5-10 12:29:53 | 显示全部楼层
好像有太深了。能说简单点吗?
回复

使用道具 举报

realkang2 发表于 2007-5-10 12:36:21 | 显示全部楼层
........................留名...
回复

使用道具 举报

江苏小鱼儿 发表于 2007-5-10 12:54:39 | 显示全部楼层
典型技术帖!
回复

使用道具 举报

weic 发表于 2007-5-10 12:58:38 | 显示全部楼层
鑫也开始吹牛皮了."底层分析"--我觉得只要对ajax有一点了解,然后再用过discuz一年的用户,对你说的东西都是基本的东西吧.不过写得很多.让我来,我没有这种心情
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|Discuz! 官方站 ( 皖ICP备16010102号 )star

GMT+8, 2024-5-18 18:17 , Processed in 0.128009 second(s), 18 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表