概念
Ajax
Ajax,Asynchronous JavaScript and XML,字面意思:異步的 JavaScript 和 XML,是指一種創建交互式網頁應用的網頁開發技術。
用於異步地去獲取XML作為數據交換的格式,當然,現在的 ajax 並不僅僅局限於XML作為數據交換格式,還可以像純文本、XML、HTML、JSON 等格式均可。
特點
- 使用腳本操縱HTTP和Web服務器進行數據交換,不會導致頁面重載。
- 避免頁面重載(這是Web初期的標准做法)的能力使Web應用感覺更像傳統的桌面應用。
- Web應用可以使用Ajax技術把用戶的交互數據記錄到服務器中;也可以開始只顯示簡單頁面,之后按需加載額外的數據和頁面組件來提升應用的啟動時間。
異步原理
Ajax 的 A 就是 asynchronous 的簡寫,表示異步。
同步和異步:
同步,按照代碼書寫的順序,一個任務一個任務的來執行。
異步,並不是按照代碼書寫的順序,通常會結合回調和事件來執行相應代碼。
在同步中,如果有一個任務耗時較長,整個的后面任務都需要等待。
在異步中,可以將耗時較長先放起來,執行其他的,其他的執行完畢,回頭再執行這個。
同步:提交請求->等待服務器處理->處理完畢返回 阻塞模式。
異步:請求通過事件觸發->服務器處理->處理完畢。非阻塞模式。
目的
為什么需要Ajax
首先,我們都了解軟件的兩種架構形態,C/S(Client/Server)和B/S(Browser/Server)。它們的區別如下:
C/S,在客戶端安裝了客戶端軟件之后,整個程序的運行,分擔到客戶端和服務器端。用戶在操作的時候,體驗更好,響應速度非常快。
B/S,所務的服務都放在服務器端,通過瀏覽器使用服務器,用戶在操作的時候,體驗不太好,響應速度特別慢。
B/S的好處,就是不需要安裝客戶端,軟件維護和更新比較方便,用戶體驗不好。
C/S的好處,用戶體驗夠好,響應及時,但是軟件的維護和更新比較麻煩。
隨着互聯網的發展,越來越多的C/S應用慢慢 轉成的B/S,這也是未來的趨勢。但是矛盾很突出,B/S的響應速度慢。需要B/S模式具備C/S模式的快速響應特點。
Ajax的出現就是為了解決這個問題-----異步刷新。
在異步刷新機制,整個頁面不用跳轉,只需要更新需要變化的地方。
Ajax 能干什么
- 表單交互
- 動態標簽頁
- 及時編輯
- 地圖類應用
- 移動APP
- ...
宗旨:提升頁面的訪問速度,提高用戶體驗。
發展歷程
2005年2月,Adaptive Path公司的Jesse James Garrett最早提出這個概念。它出現在Garrett的文章“Ajax: A New Approach to Web Applications”中。這篇文章描述了混合使用XHTML、CSS、JavaScript、DOM、XMLHttpRequest進行Web開發將會成為一種新的趨勢。
同年,Google在三大產品中使用了Ajax技術:
- Google Suggest
- Google Maps
- Gmail。
實際上,Ajax發展經歷了三個時代:
- Ajax前傳,使用iframe實現局部刷新技術;
- Ajax正傳,傳統的Ajax;
- Ajax新傳,下一代Ajax,反向Ajax。
早起 Ajax 實現
2005年,沒有 Ajax 的概念,但是異步刷新的需要還是非常多。常用的方式就是 iframe。
實現原理
第一,iframe有一個src屬性,通過設置src屬性,發出請求,發出請求的時候不會跳轉;
第二,iframe其實就是指向單獨的頁面,可以在其中進行任意的操作,可以在iframe中書寫js代碼,可以去操作父頁面。
iframe 的優缺點
優點,實現起來特別簡單,兼容性好。
缺點,功能比較弱,在服務端需要寫大量的代碼。
簡單示例:
前台代碼:
<body>
<h2>用戶注冊</h2>
<form action="/reg" method="post">
<p>
<label for="username">用戶名:</label>
<input type="text" id="username" name="username">
<span id="msg"></span>
</p>
<p>
<label for="password">密 碼:</label>
<input type="password" id="password" name="password">
</p>
<p><input type="submit" value="注冊"></p>
</form>
<iframe src="" frameborder="0" width="0" height="0" id="iframe1"></iframe>
<script>
var iframe1 = document.getElementById('iframe1');
var username = document.getElementById('username');
username.addEventListener('blur',function(){
iframe1.src = "/check?username=" + this.value;
});
</script>
</body>
服務器端代碼:
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const app = express();
app.get('/',(req,res)=>{
res.sendFile(path.join(__dirname,'login.html'));
});
app.post('/reg',(req,res)=>{
let username = req.body.username;
let password = req.body.password;
// 檢測用戶名是否可用,典型的做法需要去連接數據庫,取出所有用戶名,進行查詢比較
// TODO
// 這種方式,可以滿足需求,但是用戶體驗極差
// 每次都是填寫完所有的表單內容,才能做一些檢測
// 整個頁面已經發生刷新跳轉
});
// 模仿數據庫
let users = ['admin','test'];
app.get('/check',(req,res)=>{
let username = req.query.username;
if (inArray(username,users)) {
res.send("<script>window.parent.document.getElementById('msg').innerHTML='對不起,用戶名已被占用,請重新注冊!'</script>");
} else {
res.send("<script>window.parent.document.getElementById('msg').innerHTML='恭喜,用戶名可用!'</script>");
}
});
app.listen(3000,()=>{
console.log('server is listening in port 3000...');
});
// 簡單封裝一個方法,用於檢測一個值是否在數組中存在
function inArray(str,arr){
for(let i=0; i<arr.length; i++){
if(str == arr[i]){
return true;
break;
}
}
return false;
}
完整的Ajax通信流程
1.首先要創建一個 Ajax 對象
var xhr = new XMLHttpRequest();
狀態:readyState:
status:
responseText:
2.初始化
狀態:readyState:
status:
responseText:
3.調用 open()
方法,開啟一個請求,但沒有向服務器端發起請求
xhr.open(method, url [,async = true]);
狀態:readyState: 1
status:
responseText:
4.調用 send()
方法,正式向服務器端發起請求
get
xhr.send([data=null]);
post
xhr.setRequestHeader(header,value);
header:content-type
表單value:application/x-www-form-urlencoded
文件上傳value:multipart/form-data
狀態:readyState:2
status:
responseText:
5.當服務器端返回數據,瀏覽器端接收數據時
狀態:readyState:3
status:
responseText:
6.當瀏覽器端結束請求的時候
xhr.onreadystatechange = function(callback){
if(xhr.readyState == 4){
if( (xhr.status >= 200 && xhr.status < 300) || xhr.status==304 ){
callback(xhr.responseText);
} else {
alert('request was unsuccessful:' + xhr.status);
}
}
}
狀態:readyState:4
status:200
responseText:<!DOCTYPE html>響應返回值
XMLHttpRequest 對象
定義
- 通用定義:XMLHttpRequest是一套可以在Javascript、VbScript、Jscript等腳本語言中通過http協議傳送或從接收XML及其他數據的一套API。
- 來自MSDN的解釋:XMLHttpRequest提供客戶端同http服務器通訊的協議。客戶端可以通過該對象向http服務器發送請求並使用XML文檔對象模型處理回應。
屬性
onreadystatechange* 指定當readyState屬性改變時的事件處理句柄。只寫
readyState 返回當前請求的狀態,只讀.
responseBody 將回應信息正文以unsigned byte數組形式返回.只讀
responseStream 以Ado Stream對象的形式返回響應信息。只讀
responseText 將響應信息作為字符串返回.只讀
responseXML 將響應信息格式化為Xml Document對象並返回,只讀
status 返回當前請求的http狀態碼.只讀
statusText 返回當前請求的響應行狀態,只讀
方法
abort 取消當前請求
getAllResponseHeaders 獲取響應的所有http頭
getResponseHeader 從響應信息中獲取指定的http頭
open 創建一個新的http請求,並指定此請求的方法、URL以及驗證信息(用戶名/密碼)
send 發送請求到http服務器並接收回應
setRequestHeader 單獨指定請求的某個http頭
創建xhr對象
Microsoft最早把XMLHttpRequest對象引入到IE5中,且在IE5和IE6中它只是一個ActiveX對象。IE7之前的版本不支持非標准的XMLHttpRequest()構造函數,兼容如下:
function createXHR(){
var xhr = null;
try{
xhr = new XMLHttpRequest();
}catch(e1){
try{
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}catch(e2){
try{
xhr = new ActiveXObject("Msxml2.XMLHTTP");
}catch(e3){
throw new Error("XMLHttpRequest is not supported");
}
}
}
return xhr;
}
請求方式
兩種請求方式,有所不同。
get 請求
- 數據大小,受限於瀏覽器,大部分2k的限制,但每個瀏覽器不一樣,chrome 是8k
- 數據形式,查詢字符串方式,名值對的形式,中間用
&
鏈接 - 安全性,不太安全
- 數據的傳遞通過查詢字符串,在url后面用
?
跟上 xhr.send([data=null])
post請求
- 數據大小,原則上沒有限制,php.ini中默認上限是8M
- 數據形式,把
form
表單的數據給請求出來以xml形式傳遞給服務器 - 數據的傳遞得使用
send
方法,其中數據以鍵值對的形式來傳遞 - 安全性,比較安全
- 需要設置請求頭信息
- 需要設置請求頭部
xhr.setRequestHeader("content-type","application/x-www-form-urlencoded");
xhr.send(Formdata);
post請求注意事項
- 使用setRequestHeader()把傳遞的數據組織為xml格式
- 調用send()方法時,需要傳遞數據
- 該方式請求的同時也可以傳遞get參數信息,使用get方式來接收該信息即可
請求參數序列化
get請求中,如果參數是對象,那么我們需要對它進行轉換,編碼之后,不需要解碼,瀏覽器會自動解碼;
var url = "example.json?" + serialize(formdata);
xhr.open('get', url, true);
xhr.send(null);
post請求中,有特殊符號(=和&)和中文需要編碼,encodeURIComponent
方法
xhr.open('post', 'example.json', true);
xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
xhr.send(serialize(formdata));
序列化方法:
function serialize(data){
if(!data) return '';
var pairs = [];
for(var name in data){
if(!data.hasOwnProperty(name)) continue;
if(typeof data[name] === 'function') continue;
var value = data[name].toString();
name = encodeURIComponent(name);
value = encodeURIComponent(value);
pairs.push(name + '=' + value);
}
return pairs.join('&');
}
狀態說明
0(未初始化)
此階段確認xhr對象是否創建成功,為調用open方法做好准備,值為0表示對象已經存在,否則瀏覽器報錯
1(載入)
對xhr進行初始化,調用open方法,根據參數完成對象狀態的設置,並調用send開始向服務器端發送請求,值為1表示正在向服務器端發送請求
2(載入完成)
接收服務器端的響應數據,但獲得的還是服務器響應的原始數據,並不能在客戶端使用。值為2表示已經接收完全部響應數據,並為下一階段對數據進行解析做好准備
3(交互)
解析接收到的服務器端響應數據,根據服務器頭部返回的MIME把數據轉換成對應格式,為在客戶端調用做好准備。3表示正在解析數據
4(完成)
確認全部數據已經解析為客戶端可用的格式,解析已經完成。值為4表示數據解析完畢,可以通過xhr對象的屬性取得數據
返回數據格式
xml responseXML
和 text responseText
作為文本返回時,返回的是字符串,但有些字符串比較特殊,如html代碼、json對象,所以有如下幾種形式
- 純文本字符串,如1、0、yes、no等,直接處理
- 返回html字符串,以innerHTML的方式寫回到頁面中
- json格式,由js解析
跨域資源訪問
同源策略
兩個頁面擁有相同的協議(protocol)端口(port)和主機(host),那么這兩個頁面就屬於同一個源(origin)
不滿足同源策略的資源訪問,叫跨域資源訪問
注意
如果是協議和端口造成的跨域問題“前台”是無能為力的
在跨域問題上,域僅僅是通過“URL的首部”來識別而不會去嘗試判斷相同的ip地址對應着兩個域或兩個域是否在同一個ip上。
(“URL的首部”指window.location.protocol +window.location.host,也可以理解為“Domains, protocols and ports must match”。)
實現方法
CORS
CORS(Cross-Origin Resource Sharing)跨域資源共享,定義了必須在訪問跨域資源時,瀏覽器與服務器應該如何溝通。
CORS背后的基本思想就是使用自定義的HTTP頭部讓瀏覽器與服務器進行溝通,從而決定請求或響應是應該成功還是失敗。
目前,所有瀏覽器都支持該功能,IE瀏覽器不能低於IE10。整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。
對於開發者來說,CORS通信與同源的Ajax通信沒有差別,代碼完全一樣。
瀏覽器一旦發現Ajax請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。
參考阮老師的文章:http://www.ruanyifeng.com/blog/2016/04/cors.html
JSONP
JSONP(JSON with Padding)是一個非官方的協議,它允許在服務器端集成Script tags返回至客戶端,通過javascript callback的形式實現跨域訪問(這僅僅是JSONP簡單的實現形式)。單向的數據請求。
JSONP的原理與實現思路
- Web頁面通過調用外部js文件,可實現跨域請求。擴展一下:但凡有src屬性的標簽都具有跨域能力,例如
<script><img><iframe>
。 - 跨域服務器動態生成數據並存入js文件(通常json后綴),返回給客戶端,供客戶端調用;因為對json數據格式支持度都非常好。
- 為了便於客戶端使用數據,形成一個非正式傳輸協議,稱為JSONP。該協議重點是允許用戶傳遞一個callback參數給服務器,然后服務器返回數據時 將此callback參數作為函數名包裹住JSON數據,使得客戶端可以隨意定制自己的函數來自動處理返回數據。
因為通過script標簽引入的js是不受同源策略的限制的。所以我們可以通過script標簽引入一個js或者是一個其他后綴形式(如php,jsp等)的文件,此文件返回一個js函數的調用
示例如下:(服務器端用node.js)
瀏覽器端代碼:
動態生成script
標簽,創建一個callback
,以查詢字符串形式跟在src
的url
后邊
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP</title>
</head>
<body>
<button id="btn">JSONP</button>
<script>
var btn = document.getElementById('btn');
function show(data){
console.log(data);
}
btn.onclick = function(){
var url = "http://localhost:4000/test?callback=show";
var script = document.createElement('script');
script.setAttribute('src',url);
document.getElementsByTagName('head')[0].appendChild(script);
}
</script>
</body>
</html>
服務器端代碼:
const express = require('express');
const path = require('path');
const app = express();
app.get('/',(req,res)=>{
res.sendFile(path.join(__dirname,'index.html'));
});
app.listen(3000,()=>{
console.log('http server is listening in port 3000');
});
跨域服務器端代碼:獲取請求url
中的callback
,把callback
和json
數據組合后返回,這里需要注意,node.js返回數據的時候,設置res.type("text/javascript");
,
const express = require('express');
const path = require('path');
const app = express();
app.get('/test',(req,res)=>{
let test = require('./test.json');
const callback = req.query.callback;
res.type("text/javascript");
res.send(callback+'('+JSON.stringify(test)+')');
});
app.listen(4000,()=>{
console.log('http server is listening in port 4000');
});
json數據:
{
"title" : "JSONP",
"time" : "2017-1-1"
}
結果:
圖1
圖2
優缺點
JSONP的優點:它不像XMLHttpRequest對象實現的Ajax請求那樣受到同源策略的限制;
它的兼容性更好,在更加古老的瀏覽器中都可以運行,不需要XMLHttpRequest或ActiveX的支持;
並且在請求完畢后可以通過調用callback的方式回傳結果
JSONP的缺點:它只支持GET請求而不支持POST等其它類型的HTTP請求;
它只支持跨域HTTP請求這種情況,不能解決不同域的兩個頁面之間如何進行JavaScript調用的問題
CORS和JSONP對比
- JSONP只能實現GET請求,而CORS支持所有類型的HTTP請求。
- 使用CORS,開發者可以使用普通的XMLHttpRequest發起請求和獲得數據,比起JSONP有更好的錯誤處理。
- JSONP主要被老的瀏覽器支持,它們往往不支持CORS,而絕大多數現代瀏覽器都已經支持了CORS。
- CORS與JSONP相比,無疑更為先進、方便和可靠。
其他實現方法
- 通過document.domain跨域
- 通過location.hash跨域
- 通過HTML5的postMessage方法跨域
- 通過window.name跨域
- CSST (CSS Text Transformation)
一種用 CSS 跨域傳輸文本的方案
優點:相比 JSONP 更為安全,不需要執行跨站腳本。
缺點:沒有 JSONP 適配廣,CSST 依賴支持 CSS3 的瀏覽器。
原理:通過讀取 CSS3 content 屬性獲取傳送內容。通過創建一個 link 請求到 css 文件,然后通過 computedStyle = window.getComputedStyle
獲取到指定元素的 style 對象,再通過 computedStyle .content
獲取到內容