学习HTML5 API (pushState/replaceState)

    ajax大家都很熟悉了,我们知道ajax会有一些弊端,H5出来了两个新的API叫做pushState和replaceState,这两个可以很好的解决ajax不能历史返回的问题。先推荐一个博客,推荐的原因是要知其所以然,知道历史才能够更好的理解。下面是我的整理,由于是自己的整理,所以很多无关这两个API的,可以直接忽略,跳过直接看后面重点部分和demo。。


    前言-ajax与传统web应用的比较

    简单来说:
    第一:是局部刷新,传统web应用要请求并重新读取一个整个页面。
    第二:仅仅向服务器发送并取回必要的数据,不会像传统的web应用一样浪费很多带宽。
    第三:减少了数据交换,减少了响应时间,响应的更快。

    用户体验好,用户体验好,用户体验好

    传统的Web应用交互由用户触发一个HTTP请求到服务器,服务器对其进行处理后再返回一个新的HTHL页到客户端, 每当服务器处理客户端提交的请求时,客户都只能空闲等待,并且哪怕只是一次很小的交互、只需从服务器端得到很简单的一个数据,都要返回一个完整的HTML页,而用户每次都要浪费时间和带宽去重新读取整个页面。这个做法浪费了许多带宽,由于每次应用的交互都需要向服务器发送请求,应用的响应时间就依赖于服务器的响应时间。这导致了用户界面的响应比本地应用慢得多。
    与此不同,AJAX应用可以仅向服务器发送并取回必需的数据,它使用SOAP或其它一些基于XML的Web Service接口,并在客户端采用JavaScript处理来自服务器的响应。因为在服务器和浏览器之间交换的数据大量减少,结果我们就能看到响应更快的应用。同时很多的处理工作可以在发出请求的客户端机器上完成,所以Web服务器的处理时间也减少了。


    ajax的工作原理

    Ajax的工作原理相当于在用户和服务器之间加了—个中间层(AJAX引擎),使用户操作与服务器响应异步化。并不是所有的用户请求都提交给服务器,像—些数据验证和数据处理等都交给Ajax引擎自己来做, 只有确定需要从服务器读取新数据时再由Ajax引擎代为向服务器提交请求。

    其实Ajax主要就是通过XMLHttpRequest对象来向服务器发送异步请求,然后从服务器端获得相应的数据,利用相应的数据来局部更新相应的页面。


    ajax的缺点和优点

    咱们先讲优点:

    1. 无刷新更新数据避免修改了那些不需要改变的信息,从而减少等待时间,用户体验很好。
    2. 异步通信中途不会阻止用户操作,具有更加迅速的响应能力。优化了b/s的沟通。
    3. 前后端负载均衡,后面的ajax的工作原理会讲到,ajax可以吧以前必须要服务器承担的工作留给客户端。利用客户端闲置的能力来处理,减轻服务器和带宽的负担,节约空间和宽带租用成本。并且减轻服务器的负担,ajax的原则是“按需取数据”,可以最大程度的减少冗余请求,和响应对服务器造成的负担。
    4. 基于标准,被支持
    5. 数据与呈现分离有利于分工合作。减少非技术人员对web应用程序错误。

    再来看看缺点:

    1. ajax破坏了浏览器本身的机制,不能够有back和history的功能。
    2. ajax的安全问题,这使得开发者在不经意间会暴露更多的数据和服务器逻辑。ajax也难以避免一些一直的安全弱点,sql注入,xss等等。
    3. 破坏程序的异常处理机制(这一点在怎么理解?)
    4. 违背URL和资源定位的初衷,例如,我给你一个URL地址,如果采用了Ajax技术,也许你在该URL地址下面看到的和我在这个URL地址下看到的内容是不同的。这个和资源定位的初衷是相背离的。
    5. 对搜索引擎支持较弱。对搜索引擎的支持比较弱。如果使用不当,AJAX会增大网络数据的流量,从而降低整个系统的性能。
    6. 还有不容易调试那些问题。

    重点:怎么解决不能有back和history这一问题?

    先来看看history对象:

    1
    2
    3
    4
    5
    6
    window.history.back();
    window.history.forward();
    window.history.go(-1);
    window.history.go(2);
    //通过检查浏览器历史记录的length属性来找到历史记录堆栈中的页面总数
    var num = window.history.length;

    H5引入了history.pushState(),history.replaceState()这两个API,他们允许添加和修改history实体。需要和window.onpopstate事件一起工作。这两个方法用来修改referrer,可以被用在修改状态后而为XMLHttpRequest对象创建的http header中。

    pushState就是向history添加当前页面的记录,replaceState的参数和用法和pushState相同,区别在于replaceState是用于修改当前页面在history中的记录。这个我们后面也会在例子中用到。

    其实pushState就是一个往堆栈添加一条历史记录。可以形象的理解成push。

    pushState(data,title,[,url]):data为一个对象或者为null,它会在触发window的popstate事件时,作为参数的state属性传递过去,title为页面的标题,现在的浏览器忽略掉了这个参数,所以写啥都没用,url为页面的URL,如果不写的话,就是当前页了。

    replaceState(data,title,[,url]):参数时相同的,这种更改不会访问该URL。也就是不会刷新。会让用户看到的url发生变化,如果我们需要访问该URL,还要去手动的触发。

    replaceState不会在window.history里新增历史记录点,其效果类似于window.location.replace(url),都是不会在历史记录点里新增一个记录点的。当你为了响应用户的某些操作,而要更新当前历史记录条目的状态对象或URL时,使用replaceState()方法会特别合适。


    PJAX

    刚刚我们已经说到了那两个API,我们的PJAX就是基于上面新特性来的。PJAX是别人开发的一个项目,地址是:https://github.com/defunkt/jquery-pjax。

    它不仅仅支持了局部刷新这一特点,而且在刷新页面的同时,浏览器地址栏上面的地址是会更改的,用浏览器回退功能也能够回退回去,正常我们用ajax是实现不了的。

    PJAX的基本思路是,当用户点击一个链接,通过ajax更新页面变化的部分,然后使用HTML5的pushState修改浏览器的URL地址,这样有效地避免了整个页面的重新加载。如果浏览器不支持history的两个新API或者JS被禁用了,那这个链接就只能跳转并重新刷新整个页面了。和传统的ajax设计稍微不同,ajax通常是从后台获取JSON数据,然后由前端解析渲染,而PJAX请求的是一个在服务器上生成好的HTML碎片

    看看如果我们自己写是怎么样利用这两个API的。


    先来看一个完整的例子

    demo的地址我放在了http://cailidan.cn/web/demo/testajax这里。
    一个很简单的php文件,通过接收不同的id,并且返回不同的数据。显示到textarea上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?php
    $id = isset($_POST['id'])?$_POST['id']:1;
    switch($id){
    case 1:
    echo "这是一个ajax请求---page1";
    break;
    case 2:
    echo "这是一个ajax请求---page2";
    break;
    case 3:
    echo "这是一个ajax请求---page3";
    break;
    default:
    echo "default ajax";
    }
    ?>

    下面的例子是这样一个过程:

    1. 当我们访问index.html的时候,会自动显示成http://localhost:801/testajax/index.html?page=1,原因就是我们使用了用history.replaceState更改当前的浏览器历史;并且在textarea部分显示这是一个ajax请求---page1
    2. 当我们点击每一个li的时候,对应的textarea会发生变化,这没有什么新奇的,但当我们看浏览器的URL,发现对应的url也会发生变化,如http://localhost:801/testajax/index.html?page=2这个是我们利用replaceState来做到的。
    3. 然后我们试一下点击回退,发现是可以回到上一级哒。是不是很棒呢?
    4. 我们的思路是,先给每个li绑定一个事件,然后当我们点击这个li的时候,调用ajax,并把当前的url拼凑后pushState。当我们首次访问的时候query=undefined,我们获取到undefined后,就默认是第一个,此时我们利用replaceState改变状态栏的信息。若状态栏的query有值。,则判断query对应的li是哪个。如果找到了该li,触发其click事件,如果没有的话,证明query传参错误。然后默认去触发第一个。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>测试pjax</title>
    </head>
    <body>
    <ul id="list">
    <li><a href="test.php?page=1" data-id="1">page1</a></li>
    <li><a href="test.php?page=2" data-id="2">page2</a></li>
    <li><a href="test.php?page=3" data-id="3">page3</a></li>
    <li><a href="test.php?page=4" data-id="4">page4</a></li>
    </ul>
    <textarea name="" id="textarea" cols="30" rows="10">
    </textarea>
    <script src="./jquery-1.12.0.min.js"></script>
    <script>
    $(document).ready(function(){
    var list = $("#list a").bind("click",function(e){
    var id = $(e.target).attr('href').split('?')[1].split("=")[1];
    var query = this.href.split("?")[1];
    //调用ajax,返回相应的值入textarea
    if(history.pushState && query){
    $.ajax({
    type:"POST",
    data:{id:id},
    url:"test.php",
    success:function(data){
    $("#textarea").html(data);
    }
    })
    var title = "测试page"+$(this).text();
    document.title = title;
    if(e){
    history.pushState({title:title},title,location.href.split('?')[0]+"?"+query);
    }
    }
    return false;
    });
    var trigger = function(target){
    var query = location.href.split("?")[1];
    var target = target || null;
    //开始时query为空,就让target为第一个li选项
    if(typeof query == "undefined"){
    target = list[0];
    //把url状态栏中的内容显示为如http://localhost:801/testajax/index.html?page=1
    history.replaceState(null, document.title, location.href.split("#")[0] + "?" + target.href.split("?")[1]) + location.hash;
    trigger(target);
    }else{
    //如果已经有query了,获取里面的query值找到对应的li,并设置target为相应的li
    list.each(function(){
    if(target === null && this.href.split("?")[1] === query){
    target = this;
    }
    });
    //如果上面target没有赋值,也就是说query找不到对应的li,证明query不存在相应的记录
    //此时就设置为如http://localhost:801/testajax/index.html
    //如果target有值,则触发click事件,调用ajax.
    if(!target){
    history.replaceState(null,document.title,location.href.split('?')[0]);
    trigger();
    }else{
    $(target).trigger("click");
    }
    }
    }
    if(history.pushState){
    trigger();
    //每次浏览器前进或者后退的时候触发
    $(window).bind("popstate",function(){
    trigger();
    });
    }
    })
    </script>
    </body>
    </html>

    顺便记录几个XMLHttpRequest的属性

    onreadystatechange 每次状态改变所触发事件的事件处理程序。

    responseText 从服务器进程返回数据的字符串形式。

    responseXML 从服务器进程返回的DOM兼容的文档数据对象。

    status 从服务器返回的数字代码,比如常见的404(未找到)和200(已就绪)

    status Text 伴随状态码的字符串信息

    readyState 对象状态值

    0 (未初始化) 对象已建立,但是尚未初始化(尚未调用open方法)
    
    1 (初始化) 对象已建立,尚未调用send方法
    
    2 (发送数据) send方法已调用,但是当前的状态及http头未知
    
    3 (数据传送中) 已接收部分数据,因为响应及http头不全,这时通过responseBody和responseText获取部分数据会出现错误,
    
    4 (完成) 数据接收完毕,此时可以通过通过responseXml和responseText获取完整的回应数据。
    

    总结

    感谢osChina张鑫旭的这篇文章。了解了H5新的API,也大致的知道是怎么用的了。