今天在知乎看到一个问题:前端面试时总让写原生Ajax真的很有意义吗?觉得雕兄的回答对我现阶段准备面试很有启发。他总结了自己作为面试官的提问思路,即以 AJAX 为主线,先抛出怎么写原生 AJAX 的问题,然后逐步扩展到 HTTP 协议、同源策略、跨域实现方法、前端安全等方面。下面,我将对这个问题做一些总结。

谁适合看这篇文章:对 XMLHttpRequest、HTTP 协议、前端安全已有初步的了解,自己又不想懒得记笔记的同学。

大致思路是这样的

AJAX_review

号外: 阮一峰老师最近写了两篇相关介绍文章,内容简洁生动,强烈推荐:

浏览器同源政策及其规避方法

跨域资源共享 CORS 详解

1. 原生 XHR 怎么写?

1.1 XMLHttpRequest 基础

最简单的,先用 XHR 发起一个 GET 请求为例,这个 GET 请求从服务器获取一个文本文件 example.txt, 并将其文本内容用提示框显示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = handleResponse;
xhr.open("get", "example.txt", true);
xhr.send(null);

var handleResponse = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};

下面把这段代码扩展开来说一下:

  • 首先,创建 XHR 对象

var xhr = new XMLHttpRequest(); // IE7+

  • 定义(指定)回调处理函数

xhr.onreadystatechange = handleResponse;

onreadystatechange 的回调函数在每次 readystate 发生变化时,都会调用 handleResponse 函数。

描述
0 未初始化。尚未调用 open()方法。
1 启动。已经调用 open()方法,但尚未调用 send()方法。
2 发送。已经调用 send()方法,但尚未接收到响应
3 接收。已经接收到部分响应数据。
4 完成。已经接收到全部响应数据,而且已经可以在客户端使用了

* 表中为 readystate 不同值的含义,一般使用只要判断是否为 4 就行了

if (xhr.readyState == 4){...}

  • 初始化异步 XHR 请求(此时请求尚未发出)

xhr.open('get','example.txt',true)

open() 接受三个参数:请求的类型(”get”、”post”等)、请求资源的 URL 和表示是否异步发送请求的布尔值。

更多请求类型请看本文第 4 点。

  • 发出 XHR 请求,至此请求已经发出,浏览器等待服务器的响应。

xhr.send(null)

send() 方法的参数是请求主体要发送的数据,如果不需要发送数据,则必须传入 null,因为这个参数对有些浏览器是必需的。

  • 服务器响应回调函数

function handleResponse(){...}

当收到服务器的响应后,响应的数据会自动填充 XHR 对象的属性。

  • responseText:作为响应主体被返回的文本。
  • responseXML:如果响应的内容类型是”text/xml”或”application/xml”,这个属性中将保存包含着响应数据的 XML DOM 文档。对非 XML 数据而言,此属性值为 null
  • status:响应的 HTTP 状态,详见本文第3点。
  • statusText:HTTP 状态的说明。

此时 handleResponse() 函数将判断 readystate 是否为4(响应数据接收完毕),且 http status 是否为 200(表示成功返回)或者304(表示服务器数据未更改,继续使用浏览器缓存),判断为后,将对返回结果进行处理。

1.2 更多的 XMLHttpRequest (Level2)

前面的例子讲了简单的 GET 请求,如果我们现在需要将本地的数据发送到服务器,就需要使用 POST 请求。常见的问题是怎么将本地的一个表单提交到服务器或者将本地的一个文件上传到服务器。

  • 例子1:提交一个表单

XMLHttpRequest Level2添加了一个新的类型FormData

var data = new FormData(document.forms[0]);

formData 带有一个append()方法,接收一个键值对作为参数,可用于向表格添加数据。

data.append("name", "Nicholas");

此外,xhr 的send()方法需要将表单发送给服务器

xhr.send(data);

其它和 GET 方法类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function submitForm = function(){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);

} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("post","postexample.php", true);
var form = document.getElementById("user-info");
form.append("name", "Nicholas"); // 添加一个键值对
xhr.send(new FormData(form));
}

* 参见:《JavaScript 高级程序设计(第3版)》P578,有少量更改

  • 例子2:上传一个文件,并实时显示上传进度

XMLHttpRequest Level2 新增了进度事件,在这里我们可以用来显示上传进度

事件 含义
progress 在接收到响应数据的第一个字节时触发。
error 在接收响应期间持续不断地触发。
abort 在请求发生错误时触发。
load 在因为调用 abort()方法而终止连接时触发。
loadend 在接收到完整的响应数据时触发。
loadstart 在通信完成或者触发 error、abort 或 load 事件后触发。

部分代码:

1
2
3
4
5
6
7
8
xhr.onprogress = updateProgress;	// 上传进度
xhr.upload.onprogress = updateProgress; // 下载进度

function updateProgress(event) { // 回调函数
if (event.lengthComputable) {
   var percentComplete = event.loaded / event.total;
}
}

更详细的可以看下本文附录中阮一峰那篇博客。

2. 怎么处理回调?

  • 请求是否完成(readystate = 4)

  • 返回数据是否正确(status >=200 && status<300 || status = 304)

  • 对返回数据进行处理(alert(xhr.responseText)…)

代码参考本文第 1 点handleResponse()函数。

3. 常见 status 响应状态码?

原因 含义
1XX Informational(信息性状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理完毕
3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
4XX Client Error(客户端错误状态码) 服务器无法处理请求
5XX Server Error(服务器错误状态码) 服务器处理请求出错

* 参见:《图解HTTP》P54

几个重要的 status:

  • 200 OK 请求成功,此时 responseText 的内容已经就绪。
  • 204 No Content 该状态码代表服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。另外,也不允许返回任何实体的主体
  • 301 Moved Permanently 永久性重定向。该状态码表示请求的资源已被分配了新的URI,以
    后应使用资源现在所指的URI。
  • 302 Found 临时性重定向。该状态码表示请求的资源已被分配了新的URI,希
    望用户(本次)能使用新的URI 访问。
  • 304 See Other 请求资源尚未被修改,浏览器可以继续使用缓存中的数据。
  • 401 401 Unauthorized 发送的请求需要有通过HTTP 认证(BASIC 认证、
    DIGEST 认证)的认证信息。
  • 403 Forbidden 明对请求资源的访问被服务器拒绝
  • 404 无法找到请求的资源 || 服务器端拒绝请求且不想说明理由
  • 500 Internal Server Error
  • 503 Service Unavailable 超负载或正在进行停机维护

4. HTTP 常见请求方法

方法 描述
GET 从服务器获取资源
POST 向服务器提交要被处理的数据
HEAD 与 GET 相同,但只返回 HTTP 报头,不返回文档主体。
PUT 上传指定的 URI 表示。
DELETE 删除指定资源。
OPTIONS 返回服务器支持的 HTTP 方法。
CONNECT 把请求连接转换到透明的 TCP/IP 通道。

*参见:HTTP 方法:GET 对比 POST – w3school

5. 什么是同源策略(Same-Origin Policy)?

同协议,同域名,同端口。
不同源的客户端脚本在没明确授权的情况下,不能读写对方的资源。同源策略可以防止 Cookie 劫持等安全问题。

6. 如何发起跨域请求?

常见的跨域请求方法有:CORS、JSONP、postMessage等,下面分别讲一下,重点是CORS和JSONP:

  • CORS(Cross-Origin Resource Sharing,跨源资源共享):

IE8+ 中使用 XDominRequest 实现 CORS,使用方法和 XMLHttpRequest 类似,区别参见《JavaScript 高级程序设计》P582

1
2
3
4
5
6
var xdr = new XDomainRequest(); // IE8+
xdr.onload = function(){
alert(xdr.responseText);
};
xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null);

其他浏览器对CORS的实现:

要请求位于另一个域中的资源,使用标准的 XHR 对象并在 open()方法中传入绝对 URL:

xhr.open("GET", "https://api.github.com/user",true);

在 HTTP 请求头部加入额外的Origin信息:
Origin: http://sstruct.github.io/

此外,还需要设置服务器Access-Control-Allow-Origin 头部回发相同的源信息(如果是公共资源,可以回发”*”)。例如:

Access-Control-Allow-Origin: http://sstruct.github.io/

  • JSONP(JSON with padding,填充式 JSON 或参数式 JSON)

JSONP 和 JSON 差不多,只不过是被当作函数参数的 JSON:

1
2
3
4
5
6
7
function handleResponse(response){
alert("You’re at IP address " + response.ip + ", which is in " +
response.city + ", " + response.region_name);
}
var script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);

参见:《JavaScript 高级程序设计》P587

JSONP 的缺点也要讲一下,首先,JSONP 是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码。其次,难以确定 JSONP 请求是否失败。

题外话,我是不会使用 JSONP 的,已经有更好的方法,为什么还用这种蹩脚的方式。

  • postMessage

HTML5 新增方法(IE8+),语法:

otherWindow.postMessage(message, targetOrigin);

7. 再聊聊 Cookie 等浏览器缓存方面

这部分可以看详说 Cookie, LocalStorage 与 SessionStorage - 咀嚼之味,总结的很好,我就不废话了

  • Cookie 本身可以通过 document.cookie 设置,这里补充一下 Cookie 的缺点:
  • 只能存储4kb左右
  • 每次会随着http请求带给服务器
  • 安全性问题(存储的数据是明文的, 不要存储密码等数据)
  • 为了提高安全性,可以添加 secure 属性,只在 https 等安全情况下才发送 Cookie

8. 前端安全 CSRF, XSS

XSS(Cross-Site Scripting,跨站点脚本)

XSS 全称“跨站脚本”,是注入攻击的一种。其特点是不对服务器端造成任何伤害,而是通过一些正常的站内交互途径,例如发布评论,提交含有 JavaScript 的内容文本。这时服务器端如果没有过滤或转义掉这些脚本,作为内容发布到了页面上,其他用户访问这个页面的时候就会运行这些脚本。

CSRF(Cross-Site Request Forgery,跨站点请求伪造)

CSRF 顾名思义,是伪造请求,冒充用户在站内的正常操作。我们知道,绝大多数网站是通过 cookie 等方式辨识用户身份(包括使用服务器端 Session 的网站,因为 Session ID 也是大多保存在 cookie 里面的),再予以授权的。所以要伪造用户的正常操作,最好的方法是通过 XSS 或链接欺骗等途径,让用户在本机(即拥有身份 cookie 的浏览器端)发起用户所不知道的请求。

更详细的内容请看这里(手酸):

总结 XSS 与 CSRF 两种跨站攻击 - 无知的 TonySeek

9. 前端模块化和根据项目写个插件(略)

参考资料:

XMLHttpRequest Level 2 使用指南 - 阮一峰

CORS跨域资源共享 - segmentfault

前端通信进阶 - segmentfault

聊一聊 cookie - segmentfault

XMLHttpRequest - WEB API 接口|MDN

JavaScript 的同源策略|MDN

postMessage - WEB API 接口|MDN

HTTP 方法:GET 对比 POST – w3school

JavaScript 高级程序设计(第3版)

图解HTTP