一、什么是AJAX,为什么要使用Ajax(请谈一下你对Ajax的认识)

  • ajax全称Asynchronous JavaScript and XML(异步的javascript和XML),为什么会有这么一种技术的出现呢,因为前端时常会有这样的需求,我们只要局部刷新,不需要整一个刷新的时候,便催生了这样的技术。
  • 在 Ajax应用中信息是通过XML数据或者字符串在浏览器和服务器之间传递的(json字符串居多)
  • 在浏览器端通过XMLHttpRequest对象的responseXMl属性,得到服务器端响应的XML数据。
  • AJAX优点:

    • 最大的一点是页面无刷新,用户的体验非常好。
    • 使用异步方式与服务器通信,具有更加迅速的响应能力。
    • 可以把以前一些服务器负担的工作转嫁到客户端,利用客户端闲置的能力来处理,减轻服务器和带宽的负担,节约空间和宽带租用成本。并且减轻服务器的负担,ajax的原则是“按需取数据”,可以最大程度的减少冗余请求,和响应对服务器造成的负担。
    • 基于标准化的并被广泛支持的技术,不需要下载插件或者小程序。
  • AJAX缺点:

    • ajax不支持浏览器back按钮。
    • 安全问题 AJAX暴露了与服务器交互的细节。
    • 对搜索引擎的支持比较弱。
    • 破坏了程序的异常机制。
    • 不容易调试。
  • AJAX应用和传统Web应用有什么不同?

    • 传统的web前端与后端的交互中,浏览器直接访问Tomcat的Servlet来获取数据。Servlet通过转发把数据发送给浏览器。
    • 当我们使用AJAX之后,浏览器是先把请求发送到XMLHttpRequest异步对象之中,异步对象对请求进行封装,然后再与发送给服务器。服务器并不是以转发的方式响应,而是以流的方式把数据返回给浏览器
    • XMLHttpRequest异步对象会不停监听服务器状态的变化,得到服务器返回的数据,就写到浏览器上【因为不是转发的方式,所以是无刷新就能够获取服务器端的数据】
  • AJAX是异步执行的,如图所示,异步执行不会阻塞.

二、ajax 的执行过程

  1. 创建XMLHttpRequest对象,也就是创建一个异步调用对象
  2. 创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息
  3. 设置响应HTTP请求状态变化的函数
  4. 发送HTTP请求
  5. 获取异步调用返回的数据
  6. 使用JavaScript和DOM实现局部刷新

基本示例:

//创建 XMLHttpRequest 对象
var ajax = new XMLHttpRequest();
// 规定请求的类型、URL 以及是否异步处理请求。
ajax.open('GET',url,true);
//发送信息至服务器时内容编码类型
ajax.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 
//发送请求
ajax.send(null);  
//接受服务器响应数据
ajax.onreadystatechange = function () {
    if (obj.readyState == 4 && (obj.status == 200 || obj.status == 304)) { 
    }
};

简单应用示例:

    oBtn.onclick = function () {
        //创建对象
        var xhr = getXMLHttpRequest();
        //当xhr对象的readyState属性发生改变的时候触发
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) { //ajax的状态4表示加载完成
                if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {// http的状态是以上才算正常
                    pp.innerHTML = xhr.responseText;
                } else {
                    throw new Error("文件读取错误");
                }
            }
        }
        //open方法表示配置这次请求
        xhr.open("get", "test.txt", true);
        //发送请求
        //get请求中,没有任何的上行主体的,所以写null
        xhr.send(null);
    }

    //工厂函数(兼容浏览器)
    function getXMLHttpRequest() {
        if (window.XMLHttpRequest) {
            //高级浏览器,IE7,IE7+
            return new XMLHttpRequest();
        } else {
            //老版本浏览器,IE6
            return new ActiveXObject("Microsoft.XMLHTTP");
        }
    }

2.1 open()方法

xhr.open("get","test.txt",true);

调用open方法并不会真正发送请求,而只是启动一个请求以备发送。

它接受三个参数:

  • 要发送的请求的类型
  • 请求的URL
  • 表示是否异步的布尔值。

2.2 send()方法

如果要发送请求,用send()方法。

要发送特定的请求,需要调用send()方法。

  • 它接受一个参数:请求主体发送的数据。
  • 如果不需要通过请求主体发送数据,则必须传入null,不能留空。
  • 请求主体:HTTP上行请求,有头部、主体。

    • 一般来说,GET请求是只有头部,没有主体
    • 而POST请求有请求主体。

一但调用send()方法,HTTP上行请求就将发出。

2.3 readyState属性

表示“就绪状态”

  • 0 (uninitialized) 未初始化
  • 1 (loading) XMLHttpRequest对象正在加载
  • 2 (loaded) XMLHttpRequest对象加载完毕
  • 3 (interactive) 正在传输数据
  • 4 (complete) 全部完成

一般来说,只需要使用4状态就可以了

只要这个属性值发生了变化,就会触发一个事件onreadystatechange事件,就可以使用xhr.onreadystatechange = function(){}来捕获readyState变化之后做的事情。

三、关于http的状态

ajax 也是使用 http 协议的,所以也需要了解 http协议的状态。

1XX    100-101    信息提示
2XX    200-206    成功
3XX    300-305    重定向
4XX    400-415    客户端错误
5XX    500-505    服务器错误

200 OK 服务器成功处理了请求(这个是我们见到最多的)
301/302 Moved Permanently(重定向)请求的URL已移走。Response中应该包含一个Location URL, 说明资源现在所处的位置
304 Not Modified(未修改)客户的缓存资源是最新的, 要客户端使用缓存
404 Not Found 未找到资源
501 Internal Server Error服务器遇到一个错误,使其无法对请求提供服务

这是比较齐全的状态表:

四、关于函数封装(ajax封装)

  • 变量、函数的作用域,是定义这个变量、函数时,包裹它的最近父函数。
  • 没有在任何function中定义的变量,称为全局变量。全局变量都是window对象的属性。所以,如果想在函数内,向全局暴露顶层变量,只需要把顶层变量设置为window对象的属性。
  • 越是大的项目,越需要让全局变量越少越好。这是为了防止不同工程师之间的程序,命名冲突。所以,每一个功能包,只能向全局暴露唯一的顶层变量,就是这个功能包自己的命名空间。
  • jQuery、YUI、underscore都是这样的做法。
  • 向外暴露全局变量,设置window的变量(也是这个函数的命名空间),类似jquery的$其实也就是window.$
  • 良好的代码风格

    • //=======================属性=======================
    • //=======================方法=====================
    • //=======================内部方法=====================
    • _代表内部方法或者属性,主要是给编程人员看的
    • 属性和方法写在前面,内部属性或者内部方法写在后面
  • 通过判断arguments.length来实现函数重载
(function () {
    var myAjax = {};  //空对象
    //向外暴露这么一个全局变量
    //就是这个函数的命名空间
    window.myAjax = myAjax;

    //=======================属性=======================
    myAjax.version = "0.2.0";

    //=======================方法=======================
    myAjax.get = function () {
        //参数个数
        var argLength = arguments.length;
        var URL, json, callback;
        if (argLength == 2 && typeof arguments[0] == "string" && typeof arguments[1] == "function") {
            //两个参数
            URL = arguments[0];
            callback = arguments[1];
            //传给我们的核心函数来发出Ajax请求
            myAjax._doAjax("get", URL, null, callback);
        } else if (argLength == 3 && typeof arguments[0] == "string" && typeof arguments[1] == "object" && typeof arguments[2] == "function") {
            //3个参数
            URL = arguments[0];
            json = arguments[1];
            callback = arguments[2];
            //传给我们的核心函数来发出Ajax请求
            myAjax._doAjax("get", URL, json, callback);
        } else {
            throw new Error("get方法参数错误!");
        }
    }

    myAjax.post = function () {
        //参数个数
        var argLength = arguments.length;
        if (argLength == 3 && typeof arguments[0] == "string" && typeof arguments[1] == "object" && typeof arguments[2] == "function") {
            //3个参数
            var URL = arguments[0];
            var json = arguments[1];
            var callback = arguments[2];
            //传给我们的核心函数来发出Ajax请求
            myAjax._doAjax("post", URL, json, callback);
        } else {
            throw new Error("post方法参数错误!");
        }
    }

    //post方式提交所有表单
    myAjax.postAllForm = function (URL, formId, callback) {
        //将表单数据转为json
        var json = myAjax._formSerialize(formId);
        myAjax._doAjax("post", URL, json, callback);
    }

    //=======================内部方法=====================
    //将JSON转换为URL查询参数写法
    //传入{"id":12,"name":"考拉"}
    //返回id=12&name=%45%45%ED
    myAjax._JSONtoURLparams = function (json) {
        var arrParts = [];  //每个小部分的数组
        for (k in json) {
            //组成参数数组,然后用& 连接
            arrParts.push(k + "=" + encodeURIComponent(json[k]));//需要uri编码特殊字符串,例如中文或者符号
        }
        return arrParts.join("&");
    }

    //最核心的发出Ajax请求的方法
    myAjax._doAjax = function (method, URL, json, callback) {
        //Ajax的几个公式
        if (XMLHttpRequest) {
            var xhr = new XMLHttpRequest();
        } else {
            var xhr = ActiveXObject("Microsoft.XMLHTTP");
        }

        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4) {
                if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
                    callback(null, xhr.responseText);
                } else {
                    callback("文件没有找到" + xhr.status, null);
                }
            }
        }

        //现在要根据请求类型进行判断
        if (method == "get") {
            //请求类型是get
            //如果用户传输了json,此时要连字
            if (json) {
                //判断URL本身是否有?,没有就需要&连接
                var combineChar = URL.indexOf("?") == -1 ? "?" : "&";
                //将json转为url参数后拼接
                URL += combineChar + myAjax._JSONtoURLparams(json);
            }
            //增加一个随机数参数,防止缓存
            var combineChar = URL.indexOf("?") == -1 ? "?" : "&";
            URL += combineChar + Math.random().toString().substr(2);
            
            xhr.open("get", URL, true);
            xhr.send(null);
        } else if (method == "post") {
            //增加一个随机数参数,防止缓存
            var combineChar = URL.indexOf("?") == -1 ? "?" : "&";
            URL += combineChar + Math.random().toString().substr(2);
            
            xhr.open("post", URL, true);
            //post需要有header
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xhr.send(myAjax._JSONtoURLparams(json));
        }
    }
})();

五、关于ajax缓存问题

当Ajax第一次发送请求后,会把请求的URL和返回的响应结果保存在缓存内,当下一次调用Ajax发送相同的请求时,注意,这里相同的请求指的是URL完全相同,包括参数,浏览器就不会与服务器交互,而是直接从缓存中把数据取出来,这是为了提高页面的响应速度和用户体验。(服务端也会收到请求响应304)

浏览器会自作主张的把所有异步请求来的文件缓存,当下一次请求的URL和之前的一样,那么浏览器将不会发送这个请求,而是直接把缓存的内容当做xhr.responseText

需要注意的是,post 请求方式不会被缓存,只有 get 请求方式会被缓存。

5.1 如何避免 ajax 缓存问题

方法1:随机数

//随机数,我们不要0. 只要小数点后面的数字:
var random = Math.random().toString().substring(2);
 //URL上面就拼接一个随机字符串,保证每次URL不一样
myAjax.get("text.txt?" + random,function(err,data){
    alert(data);
});

方法2:时间戳
从1970年1月1日0:00到这一刻的毫秒数。就叫做时间戳。英语属于timestamp。
JS里面时间戳就是

//时间戳:
var timestamp = Date.parse(new Date());
 //URL上面就拼接一个随机字符串,保证每次URL不一样
myAjax.get("text.txt?" + timestamp,function(err,data){
    alert(data);
});
总的来说,原理就是通过将 get 请求的 url 做成每次都不一样,这样就不会被浏览器缓存了。

六、json检测

判断返回的 json 数据是否可用,这个只是属性一些日常使用 ajax 的点而已。

6.1 使用 JSON.parse

通过JSON.parse转换为json格式,如果无法转换,会报错。

var jsonObj = JSON.parse(str);

6.2 用hasOwnProperty进行判断

hasOwnProperty 这个方法能够判断对象里面是否有某个键属性。

var obj = {"a":1,"b":2};
console.log(obj.hasOwnProperty("aaa"));

这个示例比较详细,并且加入了错误之后的处理:

         //得到页面上的用户名的文本框、下拉列表
        var oUsername = document.getElementById("username");
        var oDomain = document.getElementById("domain");

        //得到good、bad、tuijian
        var oTuijian = document.getElementById("tuijian");
        var oBadTip = document.getElementById("badTip");
        var oGoodTip = document.getElementById("goodTip");

        //得到4个li(事先给定或者从其他接口获取的)
        var tuijianLis = oTuijian.getElementsByTagName("li");

        //失去焦点和改变下拉列表,都是做同一个事情
        oUsername.onblur = check;
        oDomain.onchange = check;

        function check(){
            clearAllTip();  //清除所有提示框
            //得到值
            var username = oUsername.value;  //文本框
            //获取所有用户选中的邮箱选项,并放入到domain数组
            var domain = (function(){
                //得到所有option
                var options = oDomain.getElementsByTagName("option");
                //遍历,看看哪个被selected了
                for(var i = 0 ; i < options.length ; i++){
                    if(options[i].selected){
                        return options[i].value;
                    }
                }
            })();

            //如果这个值是空,那么什么也不做。
            if(!username) {
                return;
            }
            //正则验证合法性
            //6~18个字符,可使用字母、数字、下划线,需以字母开头
            var reg = /^[A-Za-z][\w]{5,17}$/;
            if (!reg.test(username)) {
                showWrong("6~18个字符,可使用字母、数字、下划线,需以字母开头");
                return; //不合法的时候,就返回,不执行下面的语句了
            }

            //这里请求一个静态json,实际上要请求后台php页面。
            myAjax.post("check.json",{"username" : username},function(err,data){
                if(err){
                    showWrong("服务器错误,稍后再试");
                    return;
                }
                //转为json格式:
                var dataJSON = JSON.parse(data);
                //获得result对象(即获取服务器返回的验证结果)
                var result = dataJSON.result;
                //如果没有result对象。就创造一个result对象
                if(!result){
                    var result = {}; //因为需要给后续的hasOwnProperty校验
                }
                //检测是否可用
                if(result.hasOwnProperty(domain)){//服务器验证结果跟用户选项一致的时候
                    showRight("恭喜,可用!");
                }else{ //服务器验证结果跟用户选项不一致的时候
                    //就要给用户显示推荐的邮箱
                    oTuijian.style.display = "block";   //显示推荐框
                    //我们要依次查找这些域名是否可用(事先给定或者从其他接口获取的)
                    var domainArray = ["163.com","126.com","yeah.net"];
                    //我们再写一个结果数组
                    var usableArray = [];
                    //遍历domainArray,把domainArray中的每一个项,进行检测
                    //检测result对象中是不是有这个属性
                    //直接获取了判断的结果的数组
                    for(var i = 0 ; i < domainArray.length ; i++){
                        var tOrf = result.hasOwnProperty(domainArray[i]) ? true : false;
                        usableArray.push(tOrf);
                    }
                    console.log(usableArray);

                    //遍历4个li标签,根据我们的结果数组来决定他们
                    //是否有disable类、里面的span的内容、b的内容
                    for(var i = 0 ; i < tuijianLis.length ; i++){
                        var thisli = tuijianLis[i];

                        //通过判断的结果的数组的值来控制是否设置class
                        //决定这个li是否有disable类
                        thisli.className = usableArray[i] ? "" : "disable";

                        //往span里面写内容
                        //得到这唯一一个span
                        var thisspan = thisli.getElementsByTagName("span")[0];
                        //有时候需要重新解析一些值的格式
                        if(domainArray[i] == "vip163"){
                            domainArray[i] = "vip.163.com";
                        }
                        thisspan.innerHTML = username + "@" + domainArray[i];

                        //往b里面写内容
                        var thisb = thisli.getElementsByTagName("b")[0];
                        //通过判断的结果的数组的值来控制显示内容
                        thisb.innerHTML = usableArray[i] ? "可以使用" : "已经被占用";
                    }
                }
            });
        }

        //得到焦点
        oUsername.onfocus = clearAllTip;

        function clearAllTip(){
            //让所有的提示框消失
            oTuijian.style.display = "none";
            oBadTip.style.display = "none";
            oGoodTip.style.display = "none";
        }

        //显示错误提示框
        function showWrong(info){
            oBadTip.innerHTML = info;
            oBadTip.style.display = "block";
        }

        //显示正确提示框
        function showRight(info){
            oGoodTip.innerHTML = info;
            oGoodTip.style.display = "block";
        }

七、关于跨域问题

已经在另外一篇文章里面说过了,jsonp 是其中一种解决办法。

微信:地址
blog:地址

7.1 使用jsonp

//给按钮添加监听
        oBtn.onclick = function(){
            //得到用户填写的手机号
            var danhao = odanhao.value;
            var kuaidigongsi = okuaidigongsi.value;

            //创建script
            var script = document.createElement("script");
            script.src = "https://sp0.baidu.com/9_Q4sjW91Qh3otqbppnN2DJv/pae/channel/data/asyncqury?cb=xixi&appid=4001&com=" + kuaidigongsi +"&nu=" + danhao +"&vcode=&token=&_=1438916675664"

            //追加然后删除
            document.body.appendChild(script);
            document.body.removeChild(script);
        }

        function xixi(data){
            console.log(data);
        }

八、关于ajax的示例:瀑布流

要实现2个地方:

  1. 滚动到底部判断(包含视口的底部和总的底部)
  2. 瀑布流里面的内容需要错位显示

8.1 滚动到底部判断

我们需要知道:

  • 总文档高度
  • 已经滚动的高度
  • 视口高度,通过$(document).height(); 获取,视口底部来触发ajax 获取下一页的数据
  • 总文档高度-已经卷动高度-视口高度 < 200 基本上就是滚动到底了,滚动到文档底部就停止 ajax 请求。

  • scroll事件,一定是要截流的。因为用户滚一个鼠标滚轮的“小咯噔”就触发一次scroll事件;滑动滚动条的时候,是每一像素触发一次这个事件。还有pageDown、下箭头按钮,都能触发scroll事件。
  • 如何判断文章是否到头,说白了前端开发工程师不知道一共有多少页。比如今天又53页,明天就有55页了,所以你的JS里面无法写死一个文章总页数。所以办法就是,请求下去,请求到page.php?pagenum=54的时候,发现终止标记,或者这个页面返回的json是空,就表示到头了。

8.2 瀑布流里面的内容需要错位显示

  • 这里分成三列瀑布流,组成一个数组管理

    • 这个数组会不断计算三列之中的最小值
    • 然后按照每次的最小值进行高度插入
  • 图片判断是否加载完成需要用load方法,并且图片需要先new image才能加载方法
  • 图片的插入次序不是固定的(ajax异步),所以用之前的数组进行管理,每次都对最小值的高度插入值,这样就能保证每次都往最靠里面的图片位置进行放置

    • 并且需要使用绝对位置值,因为css里面,需要使用绝对值撑开位置(left 和top)

瀑布流的数组样例如下:

// 第一行
// [0,0,0]   minIndex: 0 left 0 top 20  [558,0,0 ]
// [558,0,0] minIndex: 1 left 300 top 20 [558,386,0]
// [558,386,0] minIndex:2 left 600 top 20 [558,386,722]
//第二行
//[558,386,722] minIndex:1 left 300 top 406 [558, 943, 722]
//[558, 943, 722] minIndex:0 left 0 top 578 [1193, 943, 722]
//[1193, 943, 722] minIndex:2 left 600 top 742 [1193, 943, 1128]

8.3 整个代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <title>Document</title>
    <style type="text/css">
        <!--省略样式-->
    </style>
</head>
<body>
<!--加载logo,默认隐藏-->
<div class="waterfall">

</div>
<div class="end">
    到最后了亲!
</div>
<script type="text/javascript" src="js/jquery-1.11.3.min.js"></script>
<script type="text/javascript" src="js/underscore.js"></script>
<!-- 模板,用underscore解析 -->
<script type="type/template" id="feed-template">
    <div class="feed-item">
        <p>
            <img src="<%= imgurl %>" alt=""/>
        </p>
        <p class="biaoti">
            <%= title %>
        </p>
        <p class="neirong">
            <%= content %>
        </p>
        <p class="zuozhe">
            <%= author %>
        </p>
    </div>
</script>
    var $waterfall = $(".waterfall");
    //得到模板
    var templateString = $("#feed-template").html();
    //准备模板函数,通过underscore将模板函数转为html模板,全局使用,所以单独拿出来
    var compile = _.template(templateString);

    //准备总高度数组
    var colAllHeight = [0, 0, 0]; //三个表示页面的瀑布的三列的每一个块的高度

    var pagenum = 1;    //页码

    getAndRender(1); //先渲染第一页的内容

    var lock = true;    //函数截流

    //窗口卷动监听
    //每滚动一次都会触发
    $(window).scroll(function () {
        //jquery帮我们做了关于滚动的三个兼容处理:总文档高度,已经卷动高度,视口高度
        var scrollTop = $(window).scrollTop();
        var windowHeight = $(window).height();
        var documentHeight = $(document).height();
        //已经滚动到底部并且已经被lock
        if (documentHeight - windowHeight - scrollTop < 200 && lock) {
            lock = false; //解除锁定
            pagenum++; //滚动一次加一次页数
            getAndRender(pagenum); //根据页数渲染数据,并且里面会重新锁定
        }
    });

    function getAndRender(pagenum) {
        //让加载logo显示
        $waterfall.addClass("loading");
        //发出Ajax请求
        //这里的页数是用简单的文件的数字编号来代替
        $.get("json/json" + pagenum + ".txt", function (data, statusText) { //jq的ajax的get方法
            //把字符串转为对象
            var dataJSON = JSON.parse(data);
            //news这个数组,仔细想想,news这个数组里面装的是什么?
            var dictionaryArray = dataJSON.news;

            //如果数组为空,就表示到最后了
            if (dictionaryArray.length == 0) {
                $(".end").show();
                $waterfall.removeClass('loading');
                return;
            }

            //遍历从接口获取的数据
            for (var i = 0; i < dictionaryArray.length; i++) {
                var thisDictionary = dictionaryArray[i];
                //马上发出请求这个字典里面图片的请求
                var image = new Image();
                //一旦设置src,上行HTTP请求将发出
                image.src = thisDictionary.imgurl;
                image.index = i; //设置这个image的索引值
                //监听这个图片是不是加载完毕
                $(image).load(function () {
                    //这张图片加载完毕了
                    //console.log(this.index + "号图片加载完毕");

                    //填充字典
                    //哪个图片已经填充完了,就注入几号字典
                    //例如第一个图片,传入转为html模板的函数
                    var compiledString = compile(dictionaryArray[this.index]);

                    //得到这个盒子,变为jQuery对象
                    var $box = $(compiledString);

                    //上DOM
                    $waterfall.append($box);

                    //寻找最小列
                    var min = _.min(colAllHeight);
                    //寻找最小列的索引
                    var minIndex = _.indexOf(colAllHeight, min);
                    //绝对定位:
                    $box.css("left", 300 * minIndex);
                    $box.css("top", colAllHeight[minIndex] + 20);
                    //将自己的高度,也加到数组的指定列中:
                    colAllHeight[minIndex] += $box.outerHeight() + 20;
                    //淡入
                    $box.fadeIn();
                    //让加载滚动的logo有高度,跟随移动位置
                    $waterfall.css("height", _.max(colAllHeight));
                    $waterfall.removeClass("loading");

                    lock = true;
                });
            }
        });
    }

线上猛如虎
2.2k 声望178 粉丝

你们都有梦想的,是吧.怀抱着梦想并且正朝着梦想努力的人,寻找着梦想的人,我想为这些人加油呐喊!