靜態(tài)頁(yè)面
在瀏覽器腳本的概念沒(méi)有出現(xiàn)之前,所有的網(wǎng)頁(yè)都是靜態(tài)的卷哩。我們知道瀏覽器的工作模式是:
- 瀏覽器向網(wǎng)站服務(wù)器發(fā)起請(qǐng)求
- 網(wǎng)站接受瀏覽器的請(qǐng)求吸祟,返回一些字符串(比如一些組成頁(yè)面的 HTML 字符串)
- 瀏覽器接收到網(wǎng)站返回的用于組成頁(yè)面的字符串后匙头,就可以關(guān)閉連接了
- 瀏覽器將組成頁(yè)面的字符串渲染到屏幕上漫谷,使得用戶可以看到一個(gè)可視化的結(jié)果
看起來(lái)就像下面這樣:
?
Client Request
+-------------+ +--------+
+------+ | User Agent | +--------------------------------> | |
| User +------> | | Server |
+--^---+ | (Browser) | <--------------------------------+ | |
| +-------+-----+ +--------+
| | Server Response
| |
| |
| +---------v--------+
| | Close Connection |
| +---------+--------+
| |
| |
| +--------v--------+
^---------+ Render response |
+-----------------+
我們看到,一旦用戶代理(瀏覽器)關(guān)閉了和服務(wù)器之間的鏈接之后蹂析,客戶端和服務(wù)器之間將不能繼續(xù)通信舔示。
動(dòng)態(tài)頁(yè)面
為了讓頁(yè)面可以給用戶帶來(lái)更多的交互,瀏覽器開(kāi)發(fā)廠商們制造出了名為瀏覽器腳本的東西识窿。比如你在瀏覽一個(gè)頁(yè)面的時(shí)候斩郎,你覺(jué)得頁(yè)面的字體太小了。在靜態(tài)頁(yè)面的時(shí)候喻频,頁(yè)面制作者在右上角給你提供了名為 “放大字體” 的按鈕缩宜,你點(diǎn)擊那個(gè)按鈕,然后開(kāi)啟一輪新的請(qǐng)求甥温,顯著的說(shuō)就是說(shuō)你感覺(jué)到瀏覽器刷新了锻煌。這其實(shí)是瀏覽器重新從服務(wù)器加載頁(yè)面的資源,只不過(guò)這一次的資源是用于顯示字體放大后的頁(yè)面姻蚓。
瀏覽器腳本就是一小段由瀏覽器執(zhí)行的代碼宋梧,頁(yè)面制作者將這一小段代碼,和網(wǎng)頁(yè)面的內(nèi)容(比如一篇優(yōu)美的散文狰挡,和它右上角的 “放大字體” 按鈕)一起返回給瀏覽器捂龄。瀏覽器接收到頁(yè)面資源后,首先就是先將散文和 “放大字體” 按鈕顯示出來(lái)加叁。注意到返回的內(nèi)容實(shí)際上還有一段由瀏覽器執(zhí)行的代碼倦沧,頁(yè)面制作者在這段帶代碼中告訴瀏覽器:如果用戶點(diǎn)擊了 “放大字體” 按鈕,那么你就將頁(yè)面的字體放大它匕。于是展融,當(dāng)你點(diǎn)擊 “放大字體” 按鈕之后,瀏覽器嚴(yán)格執(zhí)行頁(yè)面制作者在腳本中撰寫的內(nèi)容 - 將頁(yè)面的字體放大豫柬。
異步加載
注意在靜態(tài)頁(yè)面中瀏覽器和服務(wù)器之間的通信過(guò)程告希。瀏覽器在向服務(wù)器發(fā)起了對(duì)頁(yè)面的請(qǐng)求之后扑浸,在服務(wù)器沒(méi)有將頁(yè)面的內(nèi)容返回之前,頁(yè)面是無(wú)法被顯示出來(lái)的燕偶,最顯著的特征就是我們?cè)邳c(diǎn)擊了瀏覽器的 “刷新” 按鈕之后喝噪,頁(yè)面會(huì) “白屏” 一小段時(shí)間。
起初瀏覽器腳本是沒(méi)有網(wǎng)絡(luò)通信的功能的指么,只能做一些頁(yè)面的特效仙逻,比如“點(diǎn)擊按鈕放大了字體”。不過(guò)瀏覽器廠商發(fā)現(xiàn)涧尿,如果給腳本賦予網(wǎng)絡(luò)通信的功能,將使得頁(yè)面制作者可以給用戶提供更好的頁(yè)面交互體驗(yàn)檬贰。于是在早期的 IE 瀏覽器中姑廉,首先賦予了瀏覽器腳本的通信功能。
瀏覽器腳本可以和服務(wù)器進(jìn)行網(wǎng)絡(luò)通信之后翁涤,頁(yè)面制作者可以做出具有更好體驗(yàn)的頁(yè)面桥言。比如你現(xiàn)在需要搜索商品,假設(shè)是要買一本編程的書葵礼,你在網(wǎng)頁(yè)的搜索框中輸入了 “編程的數(shù)”号阿,很明顯是輸錯(cuò)了,你將 “書” 錯(cuò)輸成了 “數(shù)”鸳粉。在你點(diǎn)擊了 “搜索” 按鈕之后扔涧,進(jìn)過(guò)短暫的白屏之后,頁(yè)面中顯示了:
找不到關(guān)于 “編程的數(shù)” 的產(chǎn)品届谈,你是不是要找 “編程的書”
很不錯(cuò)枯夜,網(wǎng)站給了我們一個(gè)提示,這樣我們就可以發(fā)現(xiàn)自己的輸入錯(cuò)誤艰山。不過(guò)這個(gè)體驗(yàn)還是有待提高的湖雹,因?yàn)槊恳淮蔚乃阉鞫紩?huì)有一個(gè)短暫的 “白屏”,在白屏期間用戶只能等待曙搬。在瀏覽器腳本可以通信之后摔吏,搜索就可以以一個(gè)異步的方式進(jìn)行:
- 用戶在瀏覽器中輸入搜索頁(yè)面的地址 “http://search.shop.com”
- 瀏覽器會(huì)向網(wǎng)站請(qǐng)求搜索頁(yè)面的內(nèi)容,用于顯示這個(gè)頁(yè)面
- 網(wǎng)站在返回頁(yè)面的顯示內(nèi)容的同時(shí)纵装,包含了一小段腳本征讲,腳本的內(nèi)容是告訴瀏覽器 “用戶在點(diǎn)擊了搜索之后,你給用戶一個(gè)提示搂擦,讓用戶知道服務(wù)器正在緊張的搜索用戶所需的資源稳诚,然后你顯示了提示后,你再向服務(wù)器請(qǐng)求搜索的結(jié)果瀑踢,當(dāng)?shù)玫剿阉鹘Y(jié)果后扳还,你再把搜索結(jié)果顯示給用戶”
這樣的話才避,用戶不必在搜索時(shí)面對(duì)頁(yè)面的刷新時(shí)的 “白屏” 了,有一個(gè)提示框告訴用戶稍等片刻氨距。
同源策略
為了定位網(wǎng)絡(luò)上的資源桑逝,我們采用了統(tǒng)一資源定位符 URL,就像是一個(gè)門牌號(hào)一樣俏让, URL 標(biāo)識(shí)出資源在網(wǎng)絡(luò)上的位置楞遏。我們?yōu)g覽的網(wǎng)頁(yè),其中的內(nèi)容可能會(huì)來(lái)自不同的提供者首昔,比如散文來(lái)自一位作家寡喝,而其中的配圖來(lái)自一位美術(shù)家。散文的 URL 是 http://writer.com/new-world
勒奇,配圖的 URL 是 http://artist.com/new-world
预鬓。
我們需要有一種方式將網(wǎng)絡(luò)上的資源(比如散文和圖畫)標(biāo)識(shí)出來(lái),區(qū)別它們是來(lái)自于不同的作者赊颠。如果我們將顆粒度定位到每一個(gè)獨(dú)立的資源格二,理論上是可行的,但是我們知道作家不可能只有一篇散文竣蹦,而美術(shù)家也不會(huì)只有一幅畫顶猜。于是我們選擇了使用:通信協(xié)議,完整的域名痘括,以及端口號(hào)去描述一個(gè)源长窄,只有三者都相同,才標(biāo)識(shí)兩個(gè)資源是同源的纲菌。
下面的幾個(gè)資源是同源的:
http://example.com/
http://example.com:80/
http://example.com/path/file
下面的資源是不同源的:
http://example.com/
http://example.com:8080/
http://www.example.com/
https://example.com:80/
https://example.com/
http://example.org/
http://ietf.org/
現(xiàn)在知道了同源抄淑,那么同源策略是什么意思呢?同源策略就是驰后,兩個(gè)不同源的資源相互是不能訪問(wèn)對(duì)方的資源的肆资。同源策略主要就是限制腳本的網(wǎng)絡(luò)訪問(wèn)。
比如我們打開(kāi)了一個(gè)頁(yè)面 http://example.com
灶芝,這個(gè)頁(yè)面有兩段腳本郑原,一個(gè)段使用的內(nèi)聯(lián)的方式稱為 A,它主要就是在用戶點(diǎn)擊了按鈕之后顯示一段文字夜涕,告訴用戶點(diǎn)擊了按鈕犯犁;另一段作為外部資源進(jìn)行加載稱為 B,B 是 A 的基礎(chǔ)代碼女器,比如 B 是 jQuery酸役,它被放在了 http://cdn.jquery.com
上。首先我們知道,這兩段代碼如果按照同源的定義涣澡,肯定是不同源的贱呐。也就是說(shuō)我們?cè)?http://example.com
的頁(yè)面上是不能加載 http://cdn.jquery.com
上的資源的。
好像與現(xiàn)實(shí)情況有點(diǎn)矛盾入桂。之所以現(xiàn)在可以奄薇,是因?yàn)闉g覽器為了適應(yīng)實(shí)際的生產(chǎn)情況,放寬了對(duì)同源策略的檢查抗愁,因?yàn)槲覀冎滥俚伲豢赡軐⑺械馁Y源都放在同一臺(tái)機(jī)器上。那么在頁(yè)面完全加載好之后蜘腌,頁(yè)面中的腳本(內(nèi)聯(lián)的和外部引入)的都被瀏覽器歸納到了和當(dāng)前頁(yè)面相同的源沫屡,都屬于 http://example.com
了。這么做的意思就是撮珠,腳本無(wú)法訪問(wèn)與之不同源的資源谁鳍,也就是此時(shí)的腳本(內(nèi)聯(lián)的和外部引入的)無(wú)法訪問(wèn)資源 https://example.com/user-info
。
繞過(guò)同源策略
有時(shí)比如上面的例子劫瞳,我們確實(shí)需要在腳本中加載和當(dāng)前頁(yè)面不同源的資源,比如在 http://example.com
頁(yè)面中使用腳本加載 https://example.com/user-info
中的內(nèi)容绷柒。那么如何繞過(guò)瀏覽器的同源策略呢志于?
我們知道直接在頁(yè)面中載入不同源的外部資源是可以的,那么我們就可以動(dòng)態(tài)的載入一段外部的腳本废睦。
首先伺绽,我們的 http://example.com
中有這么一段腳本:
(function () {
window['showNickname'] = function (json) {
alert(json['nickname']);
};
var userInfoServiceUrl = 'https://example.com/user-info';
var doCrossSiteRequest = function (url, callback) {
var script = document.createElement('script');
script.src = url + '?callback=' + callback;
var head = document.getElementsByTagName('head');
if (head[0]) {
head.append(script);
}
};
document.querySelector('#btnShowNickName').addEventListener('click', function () {
doCrossSiteRequest(userInfoServiceUrl, 'showNickname');
});
})();
而 https://example.com/user-info
的服務(wù)端內(nèi)容為:
<?php
$callback = isset($_GET['callback']) ? $_GET['callback'] : null;
if ($callback === null) die('invalid request');
$userInfo = [
'nickname' => 'net-user'
];
$json = json_encode($userInfo);
echo "{$callback}({$json});";
那么在瀏覽器加載了 https://example.com/user-info
的腳本為,得到的是:
showNickname({"nickname":"net-user"});
這就和我們最先在 http://example.com
留下的 window['showNickname']
對(duì)接上了嗜湃。