瀏覽器異步加載和同源策略

靜態(tài)頁(yè)面

在瀏覽器腳本的概念沒有出現(xiàn)之前,所有的網(wǎng)頁(yè)都是靜態(tài)的。我們知道瀏覽器的工作模式是:

  1. 瀏覽器向網(wǎng)站服務(wù)器發(fā)起請(qǐng)求
  2. 網(wǎng)站接受瀏覽器的請(qǐng)求,返回一些字符串(比如一些組成頁(yè)面的 HTML 字符串)
  3. 瀏覽器接收到網(wǎng)站返回的用于組成頁(yè)面的字符串后,就可以關(guān)閉連接了
  4. 瀏覽器將組成頁(yè)面的字符串渲染到屏幕上,使得用戶可以看到一個(gè)可視化的結(jié)果

看起來就像下面這樣:

                                                                                  ?
                                       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è)面可以給用戶帶來更多的交互,瀏覽器開發(fā)廠商們制造出了名為瀏覽器腳本的東西。比如你在瀏覽一個(gè)頁(yè)面的時(shí)候,你覺得頁(yè)面的字體太小了。在靜態(tài)頁(yè)面的時(shí)候,頁(yè)面制作者在右上角給你提供了名為 “放大字體” 的按鈕,你點(diǎn)擊那個(gè)按鈕,然后開啟一輪新的請(qǐng)求,顯著的說就是說你感覺到瀏覽器刷新了。這其實(shí)是瀏覽器重新從服務(wù)器加載頁(yè)面的資源,只不過這一次的資源是用于顯示字體放大后的頁(yè)面。

瀏覽器腳本就是一小段由瀏覽器執(zhí)行的代碼,頁(yè)面制作者將這一小段代碼,和網(wǎng)頁(yè)面的內(nèi)容(比如一篇優(yōu)美的散文,和它右上角的 “放大字體” 按鈕)一起返回給瀏覽器。瀏覽器接收到頁(yè)面資源后,首先就是先將散文和 “放大字體” 按鈕顯示出來。注意到返回的內(nèi)容實(shí)際上還有一段由瀏覽器執(zhí)行的代碼,頁(yè)面制作者在這段帶代碼中告訴瀏覽器:如果用戶點(diǎn)擊了 “放大字體” 按鈕,那么你就將頁(yè)面的字體放大。于是,當(dāng)你點(diǎn)擊 “放大字體” 按鈕之后,瀏覽器嚴(yán)格執(zhí)行頁(yè)面制作者在腳本中撰寫的內(nèi)容 - 將頁(yè)面的字體放大。

異步加載

注意在靜態(tài)頁(yè)面中瀏覽器和服務(wù)器之間的通信過程。瀏覽器在向服務(wù)器發(fā)起了對(duì)頁(yè)面的請(qǐng)求之后,在服務(wù)器沒有將頁(yè)面的內(nèi)容返回之前,頁(yè)面是無法被顯示出來的,最顯著的特征就是我們?cè)邳c(diǎn)擊了瀏覽器的 “刷新” 按鈕之后,頁(yè)面會(huì) “白屏” 一小段時(shí)間。

起初瀏覽器腳本是沒有網(wǎng)絡(luò)通信的功能的,只能做一些頁(yè)面的特效,比如“點(diǎn)擊按鈕放大了字體”。不過瀏覽器廠商發(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)過短暫的白屏之后,頁(yè)面中顯示了:

找不到關(guān)于 “編程的數(shù)” 的產(chǎn)品,你是不是要找 “編程的書”

很不錯(cuò),網(wǎng)站給了我們一個(gè)提示,這樣我們就可以發(fā)現(xiàn)自己的輸入錯(cuò)誤。不過這個(gè)體驗(yàn)還是有待提高的,因?yàn)槊恳淮蔚乃阉鞫紩?huì)有一個(gè)短暫的 “白屏”,在白屏期間用戶只能等待。在瀏覽器腳本可以通信之后,搜索就可以以一個(gè)異步的方式進(jìn)行:

  1. 用戶在瀏覽器中輸入搜索頁(yè)面的地址 “http://search.shop.com
  2. 瀏覽器會(huì)向網(wǎng)站請(qǐng)求搜索頁(yè)面的內(nèi)容,用于顯示這個(gè)頁(yè)面
  3. 網(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ì)來自不同的提供者,比如散文來自一位作家,而其中的配圖來自一位美術(shù)家。散文的 URL 是 http://writer.com/new-world,配圖的 URL 是 http://artist.com/new-world。

我們需要有一種方式將網(wǎng)絡(luò)上的資源(比如散文和圖畫)標(biāo)識(shí)出來,區(qū)別它們是來自于不同的作者。如果我們將顆粒度定位到每一個(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è)不同源的資源相互是不能訪問對(duì)方的資源的。同源策略主要就是限制腳本的網(wǎng)絡(luò)訪問。

比如我們打開了一個(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 上。首先我們知道,這兩段代碼如果按照同源的定義,肯定是不同源的。也就是說我們?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)槲覀冎?,不可能將所有的資源都放在同一臺(tái)機(jī)器上。那么在頁(yè)面完全加載好之后,頁(yè)面中的腳本(內(nèi)聯(lián)的和外部引入)的都被瀏覽器歸納到了和當(dāng)前頁(yè)面相同的源,都屬于 http://example.com 了。這么做的意思就是,腳本無法訪問與之不同源的資源,也就是此時(shí)的腳本(內(nèi)聯(lián)的和外部引入的)無法訪問資源 https://example.com/user-info

繞過同源策略

有時(shí)比如上面的例子,我們確實(shí)需要在腳本中加載和當(dāng)前頁(yè)面不同源的資源,比如在 http://example.com 頁(yè)面中使用腳本加載 https://example.com/user-info 中的內(nèi)容。那么如何繞過瀏覽器的同源策略呢?

我們知道直接在頁(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ì)接上了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容