在瀏覽器腳本的概念沒有出現以前,全部的網頁都是靜態的。咱們知道瀏覽器的工做模式是:php
看起來就像下面這樣:jquery
Client Request
+-------------+ +--------+
+------+ | User Agent | +--------------------------------> | |
| User +------> | | Server |
+--^---+ | (Browser) | <--------------------------------+ | |
| +-------+-----+ +--------+
| | Server Response
| |
| |
| +---------v--------+
| | Close Connection |
| +---------+--------+
| |
| |
| +--------v--------+
^---------+ Render response |
+-----------------+
複製代碼
咱們看到,一旦用戶代理(瀏覽器)關閉了和服務器之間的連接以後,客戶端和服務器之間將不能繼續通訊。編程
爲了讓頁面能夠給用戶帶來更多的交互,瀏覽器開發廠商們製造出了名爲瀏覽器腳本的東西。好比你在瀏覽一個頁面的時候,你以爲頁面的字體過小了。在靜態頁面的時候,頁面製做者在右上角給你提供了名爲 「放大字體」 的按鈕,你點擊那個按鈕,而後開啓一輪新的請求,顯著的說就是說你感受到瀏覽器刷新了。這實際上是瀏覽器從新從服務器加載頁面的資源,只不過這一次的資源是用於顯示字體放大後的頁面。(儘管這個例子如今看來有些奇怪,由於若是僅僅是改變頁面字體大小的話,彷佛直接操做 element.style.fontSize
就能夠了,不過請求另一個包含更大字體的內聯 CSS 頁面在最終效果上也是說得通的,因此這裏還請多多包含了。)json
瀏覽器腳本就是一小段由瀏覽器執行的代碼,頁面製做者將這一小段代碼,和網頁面的內容(好比一篇優美的散文,和它右上角的 「放大字體」 按鈕)一塊兒返回給瀏覽器。瀏覽器接收到頁面資源後,首先就是先將散文和 「放大字體」 按鈕顯示出來。注意到返回的內容實際上還有一段由瀏覽器執行的代碼,頁面製做者在這段帶代碼中告訴瀏覽器:若是用戶點擊了 「放大字體」 按鈕,那麼你就將頁面的字體放大。因而,當你點擊 「放大字體」 按鈕以後,瀏覽器嚴格執行頁面製做者在腳本中撰寫的內容 - 將頁面的字體放大。瀏覽器
注意在靜態頁面中瀏覽器和服務器之間的通訊過程。瀏覽器在向服務器發起了對頁面的請求以後,在服務器沒有將頁面的內容返回以前,頁面是沒法被顯示出來的,最顯著的特徵就是咱們在點擊了瀏覽器的 「刷新」 按鈕以後,頁面會 「白屏」 一小段時間。bash
起初瀏覽器腳本是沒有網絡通訊的功能的,只能作一些頁面的特效,好比「點擊按鈕放大了字體」。不過瀏覽器廠商發現,若是給腳本賦予網絡通訊的功能,將使得頁面製做者能夠給用戶提供更好的頁面交互體驗。因而在早期的 IE 瀏覽器中,首先賦予了瀏覽器腳本的通訊功能。服務器
瀏覽器腳本能夠和服務器進行網絡通訊以後,頁面製做者能夠作出具備更好體驗的頁面。好比你如今須要搜索商品,假設是要買一本編程的書,你在網頁的搜索框中輸入了 「編程的數」,很明顯是輸錯了,你將 「書」 錯輸成了 「數」。在你點擊了 「搜索」 按鈕以後,進太短暫的白屏以後,頁面中顯示了:網絡
找不到關於 「編程的數」 的產品,你是否是要找 「編程的書」app
很不錯,網站給了咱們一個提示,這樣咱們就能夠發現本身的輸入錯誤。不過這個體驗仍是有待提升的,由於每一次的搜索都會有一個短暫的 「白屏」,在白屏期間用戶只能等待。在瀏覽器腳本能夠通訊以後,搜索就能夠以一個異步的方式進行:異步
這樣的話,用戶沒必要在搜索時面對頁面的刷新時的 「白屏」 了,有一個提示框告訴用戶稍等片刻。
爲了定位網絡上的資源,咱們採用了統一資源定位符 URL,就像是一個門牌號同樣, URL 標識出資源在網絡上的位置。咱們瀏覽的網頁,其中的內容可能會來自不一樣的提供者,好比散文來自一位做家,而其中的配圖來自一位美術家。散文的 URL 是 http://writer.com/new-world
,配圖的 URL 是 http://artist.com/new-world
。
咱們須要有一種方式將網絡上的資源(好比散文和圖畫)標識出來,區別它們是來自於不一樣的做者。若是咱們將顆粒度定位到每個獨立的資源,理論上是可行的,可是咱們知道做家不可能只有一篇散文,而美術家也不會只有一幅畫。因而咱們選擇了使用:通訊協議,完整的域名,以及端口號去描述一個源,只有三者都相同,才標識兩個資源是同源的。
下面的幾個資源是同源的:
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/
複製代碼
如今知道了同源,那麼同源策略是什麼意思呢?同源策略就是,兩個不一樣源的資源相互是不能訪問對方的資源的。同源策略主要就是限制腳本的網絡訪問。
好比咱們打開了一個頁面 http://example.com
,這個頁面有兩段腳本,一個段使用的內聯的方式稱爲 A,它主要就是在用戶點擊了按鈕以後顯示一段文字,告訴用戶點擊了按鈕;另外一段做爲外部資源進行加載稱爲 B,B 是 A 的基礎代碼,好比 B 是 jQuery,它被放在了 http://cdn.jquery.com
上。首先咱們知道,這兩段代碼若是按照同源的定義,確定是不一樣源的。也就是說咱們在 http://example.com
的頁面上是不能加載 http://cdn.jquery.com
上的資源的。
好像與現實狀況有點矛盾。之因此如今能夠,是由於瀏覽器爲了適應實際的生產狀況,放寬了對同源策略的檢查,由於咱們知道,不可能將全部的資源都放在同一臺機器上。那麼在頁面徹底加載好以後,頁面中的腳本(內聯的和外部引入)的都被瀏覽器概括到了和當前頁面相同的源,都屬於 http://example.com
了。這麼作的意思就是,腳本沒法訪問與之不一樣源的資源,也就是此時的腳本(內聯的和外部引入的)沒法訪問資源 https://example.com/user-info
。
有時好比上面的例子,咱們確實須要在腳本中加載和當前頁面不一樣源的資源,好比在 http://example.com
頁面中使用腳本加載 https://example.com/user-info
中的內容。那麼如何繞過瀏覽器的同源策略呢?
咱們知道直接在頁面中載入不一樣源的外部資源是能夠的,那麼咱們就能夠動態的載入一段外部的腳本。
首先,咱們的 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
的服務端內容爲:
<?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']
對接上了。