Ajax

有两本书让我对网页和Web服务器更加了解,一本是 《HeadFirst Ajax》 一本是谢孟军写的 《Go Web 编程》 前者主要讲解了现在的(因为过去的网页都是每次请求重新加载整个页面)网页是如何和服务器交互的,这种交互方式就叫Ajax。后者主要讲解了服务器 的逻辑,一个Web服务是如何处理网页请求的。

Ajax其实是一种网页设计的理念,过去的网页每次刷新都要重新加载整个网页,网页加载过程中无法继续操作页面,而网页加载过程又很慢,整个 用户体验很差。Ajax 通过javascript脚本(以后简称js)异步的请求网页中需要更新的部分,然后局部的刷新网页。这种至少有两个优点

  1. 网页值加载需要的部分占用带宽少
  2. 请求过程异步,不会影响用户体验

下面通过文件上传来解释Ajax过程,先从过去表单(Form)请求开始

过去的表单请求

表单用于向服务器传输数据,所有的浏览器都支持,表单大概张成这样

<form action="/upload" enctype="multipart/form-data" method="post">
    <input type="file" name="upload" multiple="multiple"/>
    <input type="submit" value="Upload file" />
</form>

最外层是一个 <form /> 标签,里面可以嵌入一些 <input /> 标签,<input/> 标签可以设置多种不同的类型比如

  1. type=”text” 设置类型为文本输入框
  2. type=”file” 设置类型为文件上传按钮
  3. type=”submit” 设置类型为提交按钮

<input/> 标签可以设置各种类型,见w3c的教程,点击提交按钮后, 会把其他input标签的内容同步提交给服务器,其实就是一个post请求,注意到 <input type="text" name="fname" /> 每个input都有一个 name 属性,服务器可以通过它区分不同字段的值。

Ajax 请求

Ajax 请求分成两部分,一部分是网页提供一个表单用来选择需要上传的文件,一部分是js用来上传文件

<form id="apk_form" enctype="multipart/form-data" method="post" action="upload">
    <input type="file" name="upload" id="fileToUpload"/>
</form>

注意没有 submit 类型的按钮,但是我们会用js监测文件选择按钮,等文件ready之后,就直接用js提交到服务器

//在网页加载的时候找到选择文件的按钮
var fileUploader = document.getElementById('fileToUpload')
//监测事件
fileUploader.onchange = function(){
//上传选择的第一个文件
  var file = fileUploader.files[0];
  if (file) {
      //new一个form对象
      var fd = new FormData();
      fd.append("upload", file);
      //new一个XMLHttpRequest请求
      var xhr = new XMLHttpRequest();
      xhr.addEventListener("load", function(evt){
        alert("上传成功:" + evt.target.responseText)
      }, false);
      xhr.addEventListener("error", function(evt){
        alert("上传失败")
      }, false);
      xhr.open("POST", "url to server");
      xhr.send(fd);
  }
}

准备工作

一般不会把大片的js代码嵌入到HTML页面(<script/>标签) 这样会使HTML文件太大且比较复杂,大多时候是把js代码单独写到一个文件中,然后使用在HTML页面中加载js代码,比如 index.html

<!DOCTYPE html>

<html>
  	<head>
    	<title>Beego</title>
    	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <script type="text/javascript" src="static/js/main.js"></script>
	</head>
  	
  	<body>
      ...
	</body>
</html>

这样在加载HTML网页(有一种更好的方法是把js文件放到HTML页面的末尾)的时候会自动下载 main.js 这个文件,我们会在这个文件中写一些js代码,比如

window.onload = initPage;
function initPage() {
// do somethign
}

这样在页面加载的时候会执行 initPage 方法,可以在这里面执行一些初始化的事情。

Ajax 请求

像上面那样直接书写 var xhr = new XMLHttpRequest(); 在一些浏览器上可能会无法工作,要考虑所有浏览器的情况就要多做一些工作,专门创建一个函数用来 创建一个http请求,这个函数会考虑各种浏览器的兼容问题。下面详细的演示Ajax请求一个图片。

function createRequest() {
    try {
        request = new XMLHttpRequest();
        } catch (tryMS) {
            try {
                request = new ActiveXObject("Msxml2.XMLHTTP");
            } catch (otherMS) {
                try {
                    request = new ActiveXObject("Microsoft.XMLHTTP");
                } catch (failed) {
                    request = null;
                }
            }
        }
    return request;
}

XMLHttpRequest 是大部分浏览器支持的请求,后面的两个 ActiveXObject 是微软特定的 http 请求(所以知道为啥大家痛恨IE了吗?)

function getDetails(url) {
    request = createRequest();
    if (request==null) {
        alert("Unable to create request");
        return;
    }
    request.open("GET",url,true);
    request.onreadystatechange = displayDetails;
    request.send(null);
}

function displayDetails() {
    if (request.readyState == 4) {
        if (request.status == 200) {
            //更新页面
            //detailDiv = document.getElementById("description");
            //detailDiv.innerHTML = request.responseText;
        }
    }
}

request.open 有三个参数

  1. string 设置请求类型 GETPOST等等
  2. string 设置请求的URL
  3. bool true 发起异步请求 false 发起同步请求

request.onreadystatechange 设置请求结束之后的回调函数, request.send(null) 可以用这个函数发送一些额外的表单数据,如果没有 可以直接填 null, 发送表单数据在前面有演示过。

request 对象拥有几个属性可以用来获取服务器返回的信息

  1. readyState 0 是初始化的值 1 浏览器请求 2 服务器响应 3 服务器在处理数据但是没有返回 4 服务器返回
  2. status HTTP协议中的状态码 200 OK
  3. responseXML 服务器数据用xml格式返回的时候
  4. responseText 服务器用文本格式返回数据的时候

分离样式和行为(其实就是分离css和js)

在上面的请求行为中有一段注释掉的代码

//更新页面
//detailDiv = document.getElementById("description");
//detailDiv.innerHTML = request.responseText;

这段代码在js中直接修改 HTML 页面中的元素,这样做是非常不好的,以后很难再从一片js代码中找到这句话来再次修改样式,如果把展示 单独放到css文件中,就可以通过css来统计修改样式了。假如有一个表单需要用户输入用户名,然后使用js代码对输入的用户名进行验证,根据 验证结果显示不同的状态,css 中设定三种状态

首先在 .css 文件中定义三种状态的样式,然后在js代码中设定状态。

//.html
<input type="text" name="usr_name" id="username" />

//.css
#username { padding: 0 20px 0 2px; width: 198px; }
#username.thinking { background: url("../images/inProcess.png"); }
#username.approved { background: url("../images/okay.png"); }
#username.denied { background: url("../images/inUse.png"); }

//.js

if (request.status == 200) {
    if (request.responseText == "okay") {
	document.getElementById("username").className = "approved";
    }else {
	alert("Sorry, that user name is taken.");
	document.getElementById("username").className = "denied";
    }
}

处理事件

一般情况下我们会这样处理一些事件,比如鼠标悬在某个标签上面的时候,给这个标签设计事件回调 currentBtn.onmouseover = showHint; 显示一段说明文字,但是同时又想改变标签的状态 currentBtn.onmouseover = buttonOver; 这个时候这两个事件只有最后一个设计的会执行,后一个覆盖了前一个回调函数。我们可以把这两件事放到一个函数里来完成,但是这样做并不是很好,因为本来就是两件事逻辑会弄得不清晰。解决这个问题可以给标签注册事件。

currentBtn.addEventListener("mouseover", showHint, false);
currentBtn.addEventListener("mouseover", buttonOver, false);

addEventListener 方法会给当前标签注册回调函数,有三个参数

  1. string 事件名称
  2. func 回调函数
  3. bool false 事件会向上传递 true 事件被截获

在IE上执行执行这样的代码会报错,因为IE无法设别 addEventListener 这样的方法,在IE上他是 currentBtn.attachEvent("onmouseover", showHint); , 虽然干了同一件事,但是IE和其他大部分浏览器都不一样(呵呵)。注意他们的不同

  1. 方法名字 addEventListener vs attachEvent
  2. 第一个参数 IE上的事件都是 on 开头的( eg: onmouseover)
  3. 第三个参数 IE上是没有第三个参数的, 因为IE只支持事件上传

但是问题来了,如何同时兼容IE和非IE浏览器,他们的差异这个大 ?

createRequest 方法一样,写一个工具方法判断不同类型浏览器来实现,以后都通过工具方法来添加事件。

function addEventHandler(obj, eventName, handler) {
    if (document.attachEvent) {
	obj.attachEvent("on" + eventName, handler);
    } else if (document.addEventListener) {
	obj.addEventListener(eventName, handler, false);
    }
}

function buttonOver() {
    //在非IE浏览器 this 指向了 btn 
    this.className = "active";
}

addEventHandler(btn, "mouseover", buttonOver);

在Firefox上运行这段代码是没有问题的,但是在 IE 上问题又来了,this.className = "active"; 这句话好像失效了,是因为在IE上 this. 指向的不是回调函数的拥有者(这个btn) 而是IE的事件处理框架(event framework). 所以我们需要新的工具函数来获取事件拥有者了

function getActivatedObject(e) {
	var obj;
	if (!e) {
		// early version of IE
		obj = window.event.srcElement;
	} else if (e.srcElement) {
		// IE 7 or later
		obj = e.srcElement;
	} else {
		// DOM Level 2 browser
		obj = e.target;
	}
	return obj;
}

所以现在的代码变成这样了

function addEventHandler(obj, eventName, handler) {
    if (document.attachEvent) {
	obj.attachEvent("on" + eventName, handler);
    } else if (document.addEventListener) {
	obj.addEventListener(eventName, handler, false);
    }
}

function buttonOver(e) {
    getActivatedObject(e).className = "active";
}

addEventHandler(btn, "mouseover", buttonOver);

框架和工具

  1. Prototype (http://www.prototypejs.org) 提供了非常底层的js工具
  2. jQuery (http://www.jquery.com) 目前最流行的js框架
  3. mooTools (http://mootools.net) 全功能的框架,拥有界面效果和Ajax请求工具
  4. script.aculo.us (http://script.aculo.us) Prototype的一个插件,提供了界面效果

JSON

现在比较流行的web架构大都使用REST API 来实现,一般都使用JSON最为中间交换数据的协议, JSON 是一种字符格式在js中还可以表示成js对象.

{
"id":"itemGuitar",
"description":"Pete Townshend once played this guitar ...",
"price":5695.99,
"urls":["http://www.thewho.com/",
"http://en.wikipedia.org/wiki/Pete_Townshend"]
}

使用eval()函数可以对字符串进行求值,比如 alert(eval("2 + 2")); 会得到 4 ,调用 eval({"id":"img","url":"xxxx"}) 会得到一个js对象。eval({"id":"img","url":"xxxx"}).id 会打印 “img”。如果传入的是一个json字符串,需要这样做告诉js这是一个 单一对象,在字符串外围加上 ‘(‘ 和 ‘)’。

var a = eval('(' + "{\"id\":\"img\",\"url\":\"xxxx\"}" + ')')
alert(a.id)
alert(a.url)

但是 eval() 函数太过强大,它会执行js代码,而有时候服务器返回的可能并不是json字符串,所以我们需要一个更安全的函数来解析服务器返回的字符串,http://www.json.org 提供了一个脚本 json2.js 可以用来解析json字符串

var  a = JSON.parse("{\"id\":\"img\",\"url\":\"xxxx\"}")
alert(a.id)
alert(a.url)

PS:以上内容大部分来自 《HEADFirstAjax》

nTop 16 November 2014
blog comments powered by Disqus