瀏覽器原理之跨域?跨站?你真的不懂我!

  跨域這個東西,額……抱歉,跨域不是個東西。大家一定都接觸過,甚至解決過因跨域引起的訪問問題,無非就是本地代理,服務器開白名單。但是,但是!你真的知道跨域背後的原理麼?嗯……不就是同源策略麼?我知道啊。但是你知道爲什麼要有同源策略麼?同源策略限制了哪些內容?又有哪些內容不受同源策略的限制呢?那麼,這篇文章,帶你搞透、搞懂跨域。

  其實很多東西本質上來說,沒有難與不難的標籤,只不過,看你是否願意花心思,時間,精力去總結整理。嗯……我知道你或許沒時間,想休息,那麼我來幫你。

  花點時間,看完這篇史上最完整的關於跨域的講解。超過了一萬字,而且你要跟着寫代碼的話,會花更多的時間,除非你做好準備了,否則,隨你吧~

第一部分 理論

  這一部分,我們先來看理論,不涉及任何代碼。主要是講清楚什麼是跨域,同源策略的定義和產生的原因,以及什麼是站點,站點與域又有啥區別?當我們理解了基本的概念之後,我會帶大家梳理瀏覽器允許HTML加載、引用哪些資源,以及哪些資源會導致跨域,哪些不會。

  開始吧~又要開始長篇~大論了。

一、URL到底是什麼?

  嗯?你不是要講跨域麼?你說URL幹啥?嗯……因爲後面的理解離不開URL。所以我們花點時間,先來理解下前置知識。

  URL,統一資源定位符,Uniform Resource Locator,它是URI的一種子分類,在URI的下面還有一種更罕見的資源使用方式,叫做URN。OK,我們單純的聊URL又牽扯出來了額外的知識概念,我們一起梳理下。

  URI(Uniform Resource Identifier),叫做統一資源標誌符,在電腦術語中是用於標誌某一互聯網資源名稱的字符串。該種標誌允許用戶對網絡中(一般指萬維網)的資源通過特定的協議進行交互操作。URI的最常見的形式是統一資源定位符(URL),經常指定爲非正式的網址。更罕見的用法是統一資源名稱(URN),其目的是通過提供一種途徑,用在特定的名字空間資源的標誌,以補充網址。

  URN我們很少使用,最常用的一種URI的形式就是URL,所以我們着重分析下什麼是URL。通常URI的格式如下:

[協議名]://[用戶名]:[密碼]@[主機名]:[端口]/[路徑]?[查詢參數]#[片段ID]

  舉個例子:

                    hierarchical part
        ┌───────────────────┴─────────────────────┐
                    authority               path
        ┌───────────────┴───────────────┐┌───┴────┐
  abc://username:[email protected]:123/path/data?key=value&key2=value2#fragid1
  └┬┘   └───────┬───────┘ └────┬────┘ └┬┘           └─────────┬─────────┘ └──┬──┘
scheme  user information     host     port                  query         fragment

  urn:example:mammal:monotreme:echidna
  └┬┘ └──────────────┬───────────────┘
scheme              path

   嗯~我們可以看到URL代表的可不僅僅是http或者https這樣的應用層協議,它是一種統一資源的定位方式,並不僅僅侷限於超文本傳輸協議。更加詳細的內容可以查看文末鏈接。

二、域名到底是什麼?

  域名,是由一串用點分隔的字符組成的互聯網上某一臺計算機或計算機組的名稱,用於在數據傳輸時標識計算機的電子方位。域名可以說是一個IP地址的代稱,目的是爲了便於記憶後者。當我們使用域名的時候,會通過DNS去查找對應的ip,從而找到對應的計算機電子方位。

  域名有一套複雜的定義規則,我們簡單瞭解下。以www.baidu.com爲例,其中.com就是頂級域名,.baidu則是二級域名,www是主機名。

  額~主機名是啥?如果你在服務器手動放置靜態HTML資源的時候,會不會發現一般都是放在www文件夾下?這就是主機名,它一般被附在域名系統(DNS)的域名之後,形成完整域名。當然,主機名也不一定非得是www,你可以隨便定義。

  頂級域名的分類有很多,我們要理解它的一些區別:

  1. TLD:即Top-Level Domain,頂級域名,它是一個因特網域名的最後部分,也就是任何域名的最後一個點後面的字母組成的部分。比如:.com、.net、.edu等。
  2. gTLD:即Generic top-level domain,通用頂級域名,是供一些特定組織使用的頂級域,以其代表組織英文名稱的頭幾個英文字母代表,如.com代表商業機構。
  3. ccTLD:即Country Code Top Level Domain,國家頂級域名,嗯,只供國家使用的,比如.cn。
  4. eTLD:即Effective Top-Level Domain,有效頂級域名。

  瞭解這些關於頂級域名的區別,目前來說足夠了。

  我們得詳細解釋下什麼是有效頂級域名,這是說清楚站點的重點。頂級域名也就是TLD一般是指域名中的最後一個"."後面的內容,TLD會記錄在一個叫做Root Zone Database的列表中,它記錄了所有的頂級域名,頂級域名並不一定只有一級,也不一定都是短單詞。

  有效頂級域名eTLD,存儲在Public Suffix List中,因爲頂級域名並不一定可以被所有需要註冊域名的用戶所使用,所以用戶可以根據頂級域名註冊自己想要的二級域名,比如example.com這樣。所以有效頂級域名的存在根本的原因是讓域名的控制權在使用者手中。比如.com.cn或者.github.io就是一個eTLD。而eTLD+1則表示eTLD再加一級域名,也就是a.github.io或者baidu.com.cn。

  爲什麼要這樣搞呢?爲了區分用戶,隔離數據,這裏的用戶並不是指域名的註冊者,而是指eTLD的用戶,比如每一個github用戶都會有一個自己的域名,比如xiaoba.github.io,並不需要用戶去申請域名,只是用戶註冊,github會根據你的信息爲你註冊一個eTLD+1的域名。

  eTLD的主要作用就是爲了避免寫入太高權限的cookie。

  我覺得上面的內容基本上解釋清楚了我們後面所要涉及到核心概念,如果有些不清楚的地方,請去文末鏈接自行深入瞭解了。

三、跨域(cross-origin)與跨站(cross-site)

  前兩個小節,我們理解了URL,以及域名的概念,無論是跨域還是跨站,其實都是基於這兩部分內容展開的。我們本小節就來了解下這倆玩意到底有啥不同。

1、跨域

  我們瞭解了域名是啥,域是啥。那如果要問你跨域的原因是啥?我相信你,肯定知道,哎呀,不就是同源策略麼?嗯……沒錯,就是同源策略,但是,你知道爲什麼要有同源策略?爲什麼同源策略是協議+域名+端口號?我只是域名不行麼?我只有協議和域名不行麼?爲啥偏偏就是這三個加在一起相同纔行?這一小小節,我們就來剖析到底什麼是跨域,爲什麼要有同源策略。

  從跨域的字面意思上來講,再結合我們上一小小節所理解的對與域的定義,可以這樣來解釋跨域:不同域名之間的訪問。但是實際上來說,卻遠遠不止如此。我們注意,同源策略是導致跨域的原因,但是隻有不同源的URL纔會導致跨域。

  OK,注意我上面這句話加進來的新的概念,首先,我們要確定的是,跨域的定義,並不是指域名不同,或者域不同,而是不同源。其次,同源的定義則是需要協議、域名、端口號三者都相同的URL纔行

  所以你看,雖然跨域叫做跨“域”,但是真正的“域”在跨域中只是一部分,雖然這部分很重要,但也只是一部分。

  那麼我們到現在爲止,終於知道了什麼是跨域,並且跨域到底跨了啥。跨域的根本原因就是同源策略,但是爲什麼要有同源策略呢?搞的這麼麻煩,我靠~嗯……都是爲了安全。

  舉個栗子你想一下噢,假設陌生人可以隨便進你家,拿你的東西,還打你的孩子,當然,你也可以進別人家,拿別人家的東西,打別人家的孩子,順便還在別人家的牆上亂塗亂畫,還裝修。這他媽不亂套了。嗯,在網絡世界,你就可以把你訪問的URL下的內容當作某一個人的家,如果主人不允許,你只能在房子外面看看,如果主人允許,那你就可以在別人的房子裏裝修,拿東西還有打孩子。注意一個重點,要主人的允許。

  總結一下下,瀏覽器默認兩個相同的源之間是可以相互訪問資源和操作 DOM 的。兩個不同的源之間若想要相互訪問資源或者操作 DOM,那麼會有一套基礎的安全策略的制約,我們把這稱爲同源策略(same-origin policy)。

  那麼問題來了,這套基礎的安全策略,也既同源策略,到底限制了兩個源之間的那些資源?哪些資源不會限制?嗯……後面會說~

  最後,最後,我好像漏了點東西,就是爲什麼同源的源必須是協議、域名、端口號的組合纔算作是源呢?嗯,因爲規範這麼定義的。哎呀!別打我~

  額……那我換種方式解釋,這個解釋純屬我個人的理解。因爲互聯網中有各種各樣的協議,你必須要有統一的協議才能互相通信,這是最大的前提,比如你說英語我說漢語,彼此又都不理解對方說的話,那你倆咋溝通呢?然後,域名,那麼有了通信的規則,還需要有通信的人,也就是計算機,域名就是用來確定是哪兩臺機器要建立通信的。最後,由於一臺計算機上可能有很多的軟件,或者應用。爲了隔離應用之間的權限,那你A應用可以訪問B應用的數據,我相信B應用肯定很不開心,我的數據全泄漏了,所以,就有了端口號。

  我們分析後發現,同源策略中的源,是最小可以確定彼此的一種定義

2)跨站

  不知道站點這個概念大家是否有過接觸,相比於域,在我們的工作中接觸到站點這個名詞其實並不多。但是由於我們要聊跨域,就不得不帶一點站點,讓大家搞清楚什麼是站點,以及跨站點又是怎麼跨的。

  有了前兩個小節的基礎,同站點的概念實際上要比同源的概念更好理解一些,因爲站點的定義並不涉及到協議和端口號,只要eTLD+1是相同的就視爲同站點。我們已經解釋過什麼是eTLD+1了哦。

  大家理解了什麼是eTLD+1,那麼也就理解了什麼是站點。個人理解,站點是最小化隔離用戶的方案。

  理解了站點,實際上跨站點也很好理解了。就是不同的eTLD+1。

  你看,這個小節,好像很簡單。哈哈哈哈,那是因爲我們做足了準備。

四、不僅僅是同源策略

  前面啊,我們鋪墊了很多,臥槽?我讀了這麼久才都是鋪墊~~嗯……好像是的。這一小節,我們要基於前三個小節的內容,聊一聊在一個網頁,或者說一個頁面通信的限制和策略。嗯,這一小節只講限制或者策略,不講解決方案,解決方案我們放到實踐裏。

  所有的限制和策略,都是爲了在絕對的安全和相對的自由中做權衡,我既不能讓你沒有規則,又不能限制你的自由。這就是一切的前提。

一、同源策略到底限制了什麼?

  我們先想思考一個問題,一個HTML頁面,可以引入或者使用哪些資源?嗯~大概有script的src,link的href,a標籤的href等等關於具備引入外部鏈接能力的DOM,再有就是XMLHttpRequest請求,嗯……最後還有cookie、localStorage、sessionStorage。大概可以分爲這三種場景或者說類型。

  我們可以把這三種情況做下分類:

  • DOM層面,同源的頁面可以互相操作DOM。
  • 數據層面,同源策略限制了不同源的站點讀取當前站點的 Cookie、IndexDB、LocalStorage 等數據。
  • 網絡層面,同源策略限制了通過 XMLHttpRequest 等方式將站點的數據發送給不同源的站點。

  誒?你上面提到的可以引入外部鏈接的DOM你沒說啊?嗯……同源策略對於外部引用的鏈接開了一個口子,讓其可以任意引用外部資源。這就導致了一個問題,早期的瀏覽器可以隨意飲用外部鏈接,於是引入的內容就很可能存在不安全的腳本。於是瀏覽器引入了內容安全策略,即CSP,CSP 的核心思想是讓服務器決定瀏覽器能夠加載哪些資源,讓服務器決定瀏覽器是否能夠執行內聯 JavaScript 代碼。

  另外,在很多場景下或者服務器設計上,都可能需要跨域發起ajax請求,如果不能跨域請求,將大大限制網站的設計能力,所以,又有了跨域資源共享(CORS),使用該機制可以進行跨域訪問控制,從而使跨域數據傳輸得以安全進行。

  最後,我們在實際應用中,不同源的兩個頁面,互相操作DOM的需求也並不是不常見,於是,瀏覽器中又引入了跨文檔消息機制,我們可以通過 window.postMessage 的 JavaScript 接口來和不同源的 DOM 進行通信。

第二部分 實踐

  本質上來說,跨域問題前端是解決不了的,或者說能解決的範圍很小,只有因域名不同所引起的跨域纔可以通過前端的能力去解決,協議和端口號引起的跨域只有前端是不行的。因爲我們前面其實說過,很多資源,要由服務器來決定,你一個瀏覽器還要什麼自行車。

  根據前面所說的同源策略限制的三種情況,我們也依次給出各情況的各種解決方案。

一、我們先來說說XMLHttpRequest

  ajax請求的跨域,是我們最常見,也最需要去理解的。所以,我們先來看看如何解決ajax的跨域。也就是網絡層面的解決方案。

1、JSONP

只支持Get請求

需要服務器和客戶端

算是一種在特定場景下的解決方案,僅供學習,現代架構方案已無太大的實際意義。

  我相信這個解決方案大家可以隨口說出來,利用script標籤可以訪問外部鏈接的機制,再結合服務器與客戶端的配合,就可以訪問一個跨域的資源,通過javascript腳本獲取數據。

  那爲啥偏偏是script標籤,img、video、audio啥的標籤都可以引用外部資源,這些標籤不行麼?能問出這個問題非常好,說明你在思考,但是問題是隻有script標籤可以執行腳本啊,你要是有其它HTML支持的可以執行腳本的標籤,那不用script也行。

  那麼,我們來看下實現代碼吧,我們需要服務器和客戶端兩個部分。我們先來安裝下依賴,需要express框架,嗯,這部分我就不多說了,直接貼上對應部分的代碼,具體的demo大家可以在參考資料中我的github中查看。

  先來看下客戶端的代碼:

let fs = require("fs");
let path = require("path");
const express = require("express");
const app = express();
const port = 4000;
app.get("/", (req, res) => {
  let sourceCode = fs.readFileSync(
    path.resolve(__dirname, "static/index.html"),
    "utf8"
  );

  res.send(sourceCode);
});

app.listen(port);

  很簡單哈,就是讀取static目錄下的文件並返回,監聽4000端口,於是,我們的客戶端url就是這樣的:http://localhost:4000/。那麼我們還要看下index.html的代碼是什麼樣的:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
  <script>
    function jsonp({ url, params, callback }) {
      return new Promise((resolve, reject) => {
        let script = document.createElement("script");
        window[callback] = function (data) {
          resolve(data);
          document.body.removeChild(script);
        };
        params = { ...params, callback };
        let arrs = [];
        for (let key in params) {
          arrs.push(`${key}=${params[key]}`);
        }
        script.src = `${url}?${arrs.join("&")}`;
        document.body.appendChild(script);
      });
    }

    jsonp({
      url: "http://localhost:3000/api",
      params: { wd: "我是參數" },
      callback: "cb",
    }).then((data) => {
      console.log(data);
    });
  </script>
</html>

  完整的代碼如上(代碼是我抄的),我們要解釋下這段代碼,jsonp方法返回了一個Promise,這個不多說,我們首先會生成一個script標籤,然後給window上綁定一個事件,這個事件名就是我們傳入的callback的名字,等待服務器傳回執行該方法的字符串。然後我們獲取傳入的參數,拼接成url的query,作爲script標籤的src參數,最後在body中插入標籤。

  我們看下服務端的代碼如何處理:

const express = require("express");
const app = express();
const port = 3000;
app.get("/api", function (req, res) {
  console.log(req.query);
  let { wd, callback } = req.query;
  res.end(`${callback}(${JSON.stringify({ wd: wd })})`);
});

app.listen(port);

  這個服務端的代碼也十分簡單,就是從路由中獲取到query信息,拼接成一個函數執行並且傳入參數的形式,在客戶端調用的時候返回,也就是在把帶拼接後的url的script標籤插入到body中的時候,就會返回這個函數執行的字符串。於是就調用了綁定在window上的那個函數。

  其實一點都不復雜,並且,我們接口的地址是:http://localhost:3000。客戶端地址我們之前說過了,是http://localhost:4000/,很明顯這跨域了。我們實現了通過jsonp的方法來跨域訪問的能力。雖然jsonp是通過script的url沒有限制訪問的方式,實現了跨域的get請求,但是在一些不大的項目場景下,get請求其實完全足夠了。

  那麼我還有個問題,get請求可以傳遞數組和對象麼?答案請在上文代碼中查找,哈哈哈。

  別急,還沒完,我還有個問題,那既然,我通過jsonp的方法實現了跨域的ajax請求,那是不是意味着我可以操作DOM,訪問cookie?那這一點安全性都沒有了啊。嗯,首先我想說的是,其實這屬於安全性問題了,但是其實跨域也是在安全策略的範疇內,所以我覺得也還是要說說。嗯,我不在這裏說。先挖個坑,等後面再填。

2、CORS

支持各種請求

僅服務器

現代項目的跨域解決方案

  幾乎現代所有項目的跨域解決方案都在應用CORS了,也就是跨域資源共享,CORS的本質哈,是新增了一組 HTTP 首部字段,允許服務器聲明哪些源站通過瀏覽器有權限訪問哪些資源。

  另外,規範要求,對那些可能對服務器數據產生副作用的 HTTP 請求方法(特別是 GET 以外的 HTTP 請求,或者搭配某些 MIME 類型 的 POST 請求),瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求(preflight request),從而獲知服務端是否允許該跨源請求。服務器確認允許之後,才發起實際的 HTTP 請求。在預檢請求的返回中,服務器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 和 HTTP 認證 相關數據)。

  那麼現在我們知道,CORS本質是一組HTTP首部字段,CORS分爲簡單請求和預檢請求。

  詳細的有關於簡單請求和預檢請求的可以查看這裏:若干訪問場景控制。我在本篇就簡單解釋下,滿足後續的實驗性代碼需求即可。

  簡單請求允許特定的HTTP Methods如:GET、HEAD、POST三個方法,並且允許認爲設置的頭字段爲:Accept、Accept-Language、Content-Language以及有條件的Content-Type。這裏條件是指,Content-Type只能是以下三者:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

  預檢請求,就是超出上述場景的請求,會預先發起一個options請求來確定服務器是否允許客戶端這樣這樣,那樣那樣的操作。那麼我們來看個具體的例子吧,逼逼賴賴,show me the code。

一、CORS簡單請求示例

  client的代碼我們不用修改,就按照之前的那樣就行,然後我們修改下index.html的代碼:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
  <script>
    const xhr = new XMLHttpRequest();
    const url = "http://localhost:3000/api";

    xhr.open("GET", url);
    xhr.onreadystatechange = function () {
      if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        console.log(xhr);
        console.log(xhr.responseText);
      }
    };
    xhr.send();
  </script>
</html>

  很簡單哈,就是一個XMLHttpRequest,打印了一下結果。繼續我們看下server端的代碼,也只需要稍微修改一下:

const express = require("express");
const app = express();
const port = 3000;
app.get("/api", function (req, res) {
  res.end(
    JSON.stringify({
      type: "cors",
      message: "ok",
      code: 1,
      body: { content: [{ a: 1, b: 2 }], page: 1, total: 10 },
    })
  );
});

app.listen(port);

  也沒啥哈,就是返回個JSON,然後,我們修改下package.json的script:

"scripts": {
  "jsonp-client": "node ./jsonp/client.js",
  "jsonp-server": "node ./jsonp/server.js",
  "jsonp": "concurrently \"yarn jsonp-client\" \"yarn jsonp-server\"",
  "cors-client": "node ./cors/client.js",
  "cors-server": "node ./cors/server.js",
  "cors": "concurrently \"yarn cors-client\" \"yarn cors-server\""
},

  嗯,就是加了cors的部分,然後我們yarn cors一下,我們看到了我們好像很熟悉的內容:

   誒?我忽然想起來一個問題,跨域的請求從瀏覽器發出,最後到達服務器了麼?瀏覽器又接收到服務器的結果了麼?答案是肯定的,實際上跨域的請求從瀏覽器發出,並被服務器接收,因爲只有到了服務器才能知道是不是跨域啊,不然咋做後續的可能的額外的邏輯呢?而且,服務器返回的信息也被瀏覽器接受到了,只是瀏覽器認爲這不安全,不給你罷了。

  額~跑題了,我們繼續。那,咋解決跨域的問題呢?Access-Control-Allow-Origin?嗯~倒是也沒錯~我們在服務器端設置一下:

const express = require("express");
const app = express();
const port = 3000;
app.get("/api", function (req, res) {
  res.set("Access-Control-Allow-Origin", "*");
  res.end(
    JSON.stringify({
      type: "cors",
      message: "ok",
      code: 1,
      body: { content: [{ a: 1, b: 2 }], page: 1, total: 10 },
    })
  );
});

app.listen(port);

  然後,我們發現,請求可以啦~~~

   並且,我們拿到了返回的數據:

   完美~誒?爲啥返回的是字符串啊,不應該是個對象麼?那是因爲你用的框架幫你處理了,比如axios。

  具體的響應頭字段變化,我們可以看到:

   沒問題吧?哈哈,我們看下一個複雜點的例子~

二、CORS預檢請求示例

  簡單請求的CORS很簡單對吧?直接一個響應頭就解決了如此讓人煩躁的跨域問題,那我怎麼試一下複雜請求,也就是預檢請求的跨域?服務器的代碼我們暫時不動,修改下客戶端代碼:

    const xhr = new XMLHttpRequest();
    const url = "http://localhost:3000/api";

    xhr.open("GET", url);
    xhr.setRequestHeader("X-NAME", "zaking");
    xhr.setRequestHeader("Content-Type", "application/xml");
    xhr.onreadystatechange = function () {
      if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        console.log(xhr);
        console.log(xhr.responseText);
      }
    };
    xhr.send();

  我們只是多加了兩個請求頭,一個是Content-Type,但是它的值卻不是我們知道的符合簡單請求要求的值,另外我們還設置了一個自定義的請求頭,不出意外的,肯定報錯了:

   但是這個報錯,我們仔細讀一下,跟之前的那個報錯還不一樣,這裏的報錯說的是:預請求的響應沒有通過檢查,因爲請求的資源沒有提供跨域允許的頭字段。我們再看下Network請求:

   有兩個請求,一個預請求,一個真正的請求,我們看下預請求的請求頭:

   沒有我們設置的請求頭字段,但是卻多了這兩個,當然,還有其他的,是瀏覽器根據你的請求默認設置的,因爲是OPTIONS請求,所以你會發現啥參數都沒帶,實際上就是客戶端與服務器的預先確認,防止無效的信息傳遞。

  那,要怎麼解決呢?我們修改下服務器代碼:

const express = require("express");
const app = express();
const port = 3000;
app.use(function (req, res, next) {
  res.set({
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "X-NAME,Content-Type",
  });
  if (req.method === "OPTIONS") {
    res.end(); // OPTIONS請求不做任何處理
  }
  next();
});
app.get("/api", function (req, res) {
  res.end(
    JSON.stringify({
      type: "cors",
      message: "ok",
      code: 1,
      body: { content: [{ a: 1, b: 2 }], page: 1, total: 10 },
    })
  );
});

app.listen(port);

  app.use接收一個函數,會在匹配路由的時候觸發,我們增加了一個跨域允許的頭字段,Access-Control-Allow-Headers,把我們需要傳遞的頭字段加進去就可以了,也包括我們自定義的頭字段,這樣就可以了~

  具體的響應大家可以自己去寫下代碼體驗下哦~

3、WebSocket

幾乎不會作爲跨域的解決方案

  WebSocket想必大家都有所瞭解,我想了又想,應不應該把WebSocket歸屬於XMLHttpRequest範疇下,但是我又想了想,不放在這裏,放在哪個分類下都不太合適,嗯~就放這吧。

  先簡單解釋下WebSocket吧,WebSocket是由HTML5規範並定義的一種全雙工通信通道,和HTTP一樣,是基於TCP/IP協議的一種應用層通信協議,相較於經常需要使用推送實時數據到客戶端甚至通過維護兩個HTTP連接來模擬全雙工連接的舊的輪詢或長輪詢(Comet)來說,這就極大的減少了不必要的網絡流量與延遲。

  通過WebSocket協議,我們可以跨域訪問,爲啥WebSocket不受同源策略的限制呢?嗯~~我沒研究過WebSocket協議,有興趣大家可以自行研究下,這裏不再展開,不過我覺得根本原因就是WebSocket不是HTTP協議,WebSocket協議允許跨域,或者說WebSocket就是允許這樣搞嘛。

  OK,那麼我們來實現下代碼吧,頁面結構都跟之前一樣哈,我們先修改下客戶端代碼:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
  <script>
    // Create WebSocket connection.
    const socket = new WebSocket("ws://localhost:3000");

    // Connection opened
    socket.addEventListener("open", function (event) {
      socket.send("Hello Server!");
    });

    // Listen for messages
    socket.addEventListener("message", function (event) {
      console.log("Message from server ", event.data);
    });
  </script>
</html>

  然後是服務器代碼,我們需要先安裝下ws模塊,嗯~就是一個node的WebSocket的模塊,可以讓我們更簡單的使用WebSocket,服務器代碼如下:

let WebSocket = require("ws"); //記得安裝ws
let wss = new WebSocket.Server({ port: 3000 });
wss.on("connection", function (ws) {
  ws.on("message", function (data) {
    console.log(data);
    ws.send("Hello Client");
  });
});

  嗯~我們根本沒用express,只要ws就可以了。

  然後,我們就可以看到結果了,由於WebSocket有一套完整的協議規則,與HTTP並不相同,這裏僅作爲HTTP跨域的一種解決方案,不多說了。

4、小小的總結

  我們稍微回顧一下上面的三種解決方案,Jsonp,CORS,WebSocket,我們發現這些解決方案都離不開服務器,換句話說,服務器纔是真正解決問題的源頭,瀏覽器能做的事情其實是十分有限的。那,爲什麼瀏覽器在真正解決跨域的方案上並不重要呢?嗯~~~你一個瀏覽器想要多大的權限?瀏覽器在理論上講,僅僅只是數據展示的形式,你想要隨隨便便就能去別人家裏拿銀行卡取錢麼?顯然是不現實的,嗯~~~這就是一切的原因。瀏覽器只能是,也必然是有限的訪問權限。

  那麼,上面的三種解決方案,都是基於應用層協議的解決方案,無論是JSONP、CORS還是WebSocket都是基於應用層協議,這是他們的一個共通點,並且,這些權限僅限於數據的獲取,也即通過應用層協議與服務器交互數據

  誒?我既然可以和服務器交互數據了,那我能不能在www.a.com/index.html獲取www.b.com的服務器的數據,然後再讓www.c.com/index.html獲取數據做修改?等等等等這樣,反正就是通過某個瀏覽器粗行口修改服務器數據再讓另一個瀏覽器窗口獲取修改後的數據從而通過javascript腳本來修改頁面DOM。額~~肯定可以,但是這好像有點脫褲子放屁?!

  當然,上面說的這種亂碼七糟的方式不是不行,但是它所涉及到的問題就與跨域無關了,我個人覺得,它算是服務器架構設計了,哇~~~這個詞好高大上,我真的不會。

二、代理(其實我應該算做“一”)

  代理想必大家很熟悉啦,我們用vue-cli生成一個項目默認就安裝了代理模塊,簡單看下文檔,配置幾個參數就能實現本地代理,從而實現本地開發環境的數據遠程訪問,嗯~~是數據,也就是HTTP,所以我在標題才說應該算是“一”嘛。

  代理的更多內容我不多說了,大家有興趣可以看我的前端運維繫列的一篇文章,那裏有很詳細的解釋。我簡單解釋下什麼是代理,代理在這裏的全稱叫做代理服務器,服務器?嗯~沒錯,代理服務器也是一種服務器,換句話說,因爲同源策略是客戶端(或者說瀏覽器)的,是爲了限制客戶端訪問權限,而服務器則壓根沒有這個什麼垃圾同源策略,所以,我們搞一個服務器,假裝與你的客戶端同源,然後代理服務器在中間幫你轉一下,這不就可以了嘛。

  就好像你想去銀行取錢,結果發現不是本人,哪怕你拿着銀行卡,銀行肯定也不給你錢啊,但是你可以假裝成本人,呢~在現實中假裝一個人可沒那麼容易,但是在互聯網絡中,或者再小一點,在同源策略下,假裝一個“人“還是挺容易的。

1、正向代理

  正向代理,就是指代理服務器爲客戶端代理,真實服務器接收到的是代理服務器發起的請求,真實服務器並不知道真實的客戶端到底是誰。

  那麼在本地環境的Node正向代理的場景下,整個請求的流轉大致是這樣的,在index.html(舉個例子)向遠程服務器發起請求的時候,Node代理服務器會攔截這個請求,並把該請求轉發給遠程服務器,當Node代理服務器接收到遠程真實服務器的響應後,再次把結果響應給本地的客戶端。

  核心在於本地Node代理服務器是如何接收和發送以及返回響應的,我們來看下代碼,基本的代碼,我們就用cors那部分作爲基礎修改就好了,還是4000端口的頁面去訪問3000端口的api,只不過之前cors的時候並沒有經過轉發。

  首先,我們先修改下端口號3000的server代碼,讓它作爲中間的代理服務器,並且把名字修改成prxoy.js:

const http = require("http");
const server = http.createServer((request, response) => {
  response.writeHead(200, {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "*",
    "Access-Control-Allow-Headers": "Content-Type",
  });
  http
    .request(
      {
        host: "127.0.0.1",
        port: 6000,
        url: "/api",
        method: request.method,
        headers: request.headers,
      },
      (serverResponse) => {
        var body = "";
        serverResponse.on("data", (chunk) => {
          body += chunk;
        });
        serverResponse.on("end", () => {
          console.log(body);
          response.end(body);
        });
      }
    )
    .end();
});
server.listen(3000);

  我們看下這段代碼,跟之前的不太一樣了,之前我們利用express,但是現在我們直接用Node的HTTP模塊,生成了一個HTTP服務,並且這個服務設置了響應頭,也就是我們之前允許跨域的那些響應頭,然後呢直接通過http模塊的request向http://localhost:6000/api,發起了請求,並把得到的響應結果拼湊和返回,並不複雜,對吧,然後我們再創建一個server.js,代碼類似:

const http = require("http");
const server = http.createServer((request, response) => {
  response.end(
    JSON.stringify({
      type: "cors",
      message: "ok",
      code: 1,
      body: { content: [{ a: 1, b: 2 }], page: 1, total: 10 },
    })
  );
});
server.listen(6000);

  代碼很簡單,就是返回個數據罷了。那麼OK,我們現在覈心的都做完了,我們在package.json中加上幾句話:

"node-proxy-client": "node ./node-proxy/client.js",
"node-proxy-proxy": "node ./node-proxy/proxy.js",
"node-proxy-server": "node ./node-proxy/server.js",
"node-proxy": "concurrently \"yarn node-proxy-client\" \"yarn node-proxy-server\" \"yarn node-proxy-proxy\""

  都知道啥意思吧,然後,我們打開兩個命令行工具,分別啓動3000的代理服務器和6000的遠程服務器,最後在打開瀏覽器,訪問本地3000端口,你看看啥效果:

 

   直接頁面中就顯示了返回的結果,注意,我們現在僅僅是服務端的交流,跟跨域沒關係的對吧?然後,我們跑一下yarn node-proxy再看下結果?

   完美~

  我額外要多說兩句,上面代理的代碼僅是例子,最小化證明我們方案的可行性,就本地代理來說,你可以使用node插件,可以使用webpack,等等等等,解決本地代理的手段和方法非常之多,但是其核心原理無非就是讓瀏覽器與服務器的通信,變成服務器與服務器之間的通信。

2、反向代理

  反向代理,簡單來說就是代理服務器代理的是真實服務器,客戶端並不知道真正的服務器是什麼。

  當我們發起請求的時候,是經過反向代理服務器來攔截過濾一遍,是否允許轉發給真實的服務器,基本上現代的服務器架構都會使用Nginx作爲代理服務器去處理網絡請求,在現在的場景下大家有所瞭解就好。

  既然我們要實現反向代理,那麼我們需要在本地安裝下nginx,至於安裝方法,大家自行百度吧(文末或許有驚喜哦,先百度再看)。

  安裝完成之後,就可以在命令工具中試一下安裝成功沒有:

naginx -v

  出現版本號,說明我們安裝OK啦。然後,理論上講,你的nginx配置文件在這裏:/usr/local/etc/nginx/nginx.conf。我們需要修改這個nginx.conf配置文件:

server {
    listen       8080;
    server_name  localhost;

    location / {
        proxy_pass  http://localhost:3000;
        add_header Access-Control-Allow-Origin http://localhost:4000;  
        root   html;
        index  index.html index.htm;
    }
}

  其他的當我們安裝的時候就存在了,重點就是這兩行代碼。一個是需要代理的服務器,一個是我們設置的響應頭。當然,注意,我們只設置了一個跨域的響應頭,所以目前只支持簡單請求。我們修改下客戶端代碼,刪除掉額外設置的請求頭,然後,我們還需要刪除之前cors的時候在服務器設置的允許跨域請求的那部分代碼,我相信你知道我說的是什麼,就不貼代碼了哦,有問題我相信你也可以自行解決了。

  然後,我們在package.json中按照慣例的再加上點腳本:

"nginx-proxy-client": "node ./nginx-proxy/client.js",
"nginx-proxy-server": "node ./nginx-proxy/server.js",
"nginx-proxy": "concurrently \"yarn nginx-proxy-client\" \"yarn nginx-proxy-server\""

  最後,我們需要啓動下:

yarn nginx-proxy

  還沒完,我們還需要啓動nginx:

sudo brew services restart nginx   

  那麼~~見證奇蹟的時刻:

  又一次~完美~  

三、DOM操作?算是吧(其實我覺得這不算是跨域操作DOM,就這樣吧)

  前面第一二章哈,都與服務器有關,沒了服務器毛都幹不了,那客戶端就啥也幹不了了?瀏覽器就這麼垃圾?嗯,那肯定不是(其實我是騙你往下看),這一大章,我們就來看看瀏覽器有哪些能力來解決跨域的問題。

  解析來的內容我們需要另外一套代碼,這回與服務器沒關係了,好像與ajax請求也沒關係了,只是跨域的頁面在瀏覽器中想要乾點什麼。

  嗯~~我們先來看代碼:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index.html
  </body>
</html>

  這是其中一個html的代碼,然後是啓動服務的client1.js:

let fs = require("fs");
let path = require("path");
const express = require("express");
const app = express();
const port = 3001;
app.get("/", (req, res) => {
  let sourceCode = fs.readFileSync(
    path.resolve(__dirname, "static/index1.html"),
    "utf8"
  );

  res.send(sourceCode);
});

app.listen(port);

  不復雜,就是我們之前的代碼,還沒完事,你要複製出來一份,index2.html,以及client2.js,不過client2.js中的端口號是3002。然後我們加上package.json的腳本:

"postmessage-client1": "node ./postmessage/client1.js",
"postmessage-client2": "node ./postmessage/client2.js",
"postmessage": "concurrently \"yarn postmessage-client1\" \"yarn postmessage-client2\""

  然後,我們啓動服務,分別訪問3001和3002,一點毛病沒有,可以看到我們的頁面內容。那麼基本的框架我們完事了哦。下面要看我們的核心內容了。

1、postMessage

特別重要

很有用

與服務器無關

  在我們之前代碼的基礎上,我們來寫一下postMessage,哦對,在寫之前我得先簡單說下什麼是postMessage。

  window.postMessage() 方法可以安全地實現跨源通信。通常,對於兩個不同頁面的腳本,只有當執行它們的頁面位於具有相同的協議(通常爲 https),端口號(443 爲 https 的默認值),以及主機 (兩個頁面的 Document.domain設置爲相同的值) 時,這兩個腳本才能相互通信。window.postMessage() 方法提供了一種受控機制來規避此限制,只要正確的使用,這種方法就很安全。也即只有同源的兩個頁面腳本纔可以互相通信。

  一個窗口可以獲得對另一個窗口的引用(比如 targetWindow = window.opener),然後在窗口上調用 targetWindow.postMessage() 方法分發一個 MessageEvent 消息。OK,我們來寫下代碼吧,在index1.html中添加點javascript腳本:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index.html
  </body>
  <script>
    window.postMessage("hello index2", "*");
    window.addEventListener("message", (e) => {
      console.log(e, "I am from index1");
    });
  </script>
</html>

  看起來不錯,然後同樣的,在給index2.html加點腳本:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index2.html
  </body>
  <script>
    window.postMessage("hello index1", "*");
    window.addEventListener("message", (e) => {
      console.log(e, "I am from index2");
    });
  </script>
</html>

  看起來也很不錯。我們啓動下,看下控制檯:

  嗯~~~嗯?好像不太對,我們在看另外一個:

   嗯~~~草,不對啊,你這不對啊,3001端口應該打印hello index1,3002應該打印hello index2。你這隻在自己和自己玩呢啊?

  嗯哼……我承認你是最強的。額……我承認你說的對,爲啥會這樣,這也沒通信成功啊。因爲我們缺少了尤爲關鍵的一點。

  一個窗口可以獲得對另一個窗口的引用(比如 targetWindow = window.opener),然後在窗口上調用 targetWindow.postMessage() 方法分發一個消息。

  必須!必須!獲得另外一個窗口引用纔可以!換句話說,兩個單獨的,獨立的,互相沒有跳轉關係的頁面,postMessage也不行!那?想一想,我有哪些方法可以解決這個引用纔可以呢?

1)通過a標籤獲取window.opener

  window.opener會返回打開當前窗口的那個窗口的引用,例如:在 window A 中打開了 window B,B.opener 返回 A。那麼,我們需要加點代碼:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index.html
    <a target="_blank" rel="opener" href="http://localhost:3002/"
      >打開index2.html</a
    >
  </body>
  <script>
    window.addEventListener("message", function (e) {
      console.log(e);
    });
  </script>
</html>

  這代碼很簡單沒啥好說的,先讓程序跑起來,等會再說重點,我們再來看一下,index2.html咋寫:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index2.html
  </body>
  <script>
    const target = window.opener;
    target.postMessage("hello I am from 3002", "http://localhost:3001/");
  </script>
</html>

  也很簡單。我們啓動下服務。打開localhost:3001。然後點擊a標籤,跳轉到3002,然後回到3001,我們可以看到打印的結果:

   那,我想要實現雙向通信咋整?嗯。。繼續加點代碼,在index1.html中,可以這樣:

window.addEventListener("message", function (e) {
  console.log(e);
  e.source.postMessage("hello index2 I am Index1", e.origin);
});

  通過返回的事件的source可以獲取到對方window得引用,然後e.origin就是對方的源地址。這樣就可以把信息再傳遞過去,然後index2.html中接收一下即可:

const target = window.opener;
target.postMessage("hello I am from 3002", "http://localhost:3001/");
window.addEventListener("message", (e) => {
  console.log(e);
});

  OK,我們就這樣實現了postMessage跨域的雙向通信。但是,其實在我寫實驗性的代碼中遇到了很多問題,這些問題很重要,我簡單總結下:

  1. a標籤打開另外一個窗口時,必須攜帶rel="opener"纔可以讓被打開的頁面通過window.opener獲取到父頁面的應用。我之前以爲只要HTTP的請求頭中帶了referer,那麼就可以通過window.opener獲取到,但是實驗後發現這是兩回事。
  2. 只有在雙方頁面都加載完畢後postMesaage纔會生效!
  3. 基於第二點,如果你不是新打開一個標籤頁,也就是target不是_blank的話,也不行!

  其實,我覺得現在有點跑題了,真的,很多內容其實與跨域的關係並不大了。但是沒辦法,講到這了,就得說清楚。

  那麼以上是通過a標籤打開的新窗口,下面我們看下另外一種方式,我寫這篇文章的時候真沒想到會寫這麼多東西,早知道我就分兩三篇了,算了,懶得再開一個,要是看不下去就看不下去吧。

  爲了更清晰一點,我們把當前的postmessage文件夾下的代碼放到a-tag文件夾下,並且修改下啓動腳本,不多說了。

2) 通過window.open獲取window.opener

  這部分的代碼,我們先在postmessage的文件夾下新建一個open-fun文件夾,然後,把上一小節的a-tag文件夾下的內容複製過來,然後,修改script腳本,具體的去文末的demo地址看吧,不贅述了。

  我們直接上代碼吧,首先是index1.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index.html
    <button id="btn">打開index2.html</button>
  </body>
  <script>
    const btn = document.getElementById("btn");
    btn.addEventListener("click", function () {
      window.open("http://localhost:3002/");
    });
    window.addEventListener("message", function (e) {
      console.log(e);
    });
  </script>
</html>

  嗯,然後是index2.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index2.html
  </body>
  <script>
    window.opener.postMessage("hello I am from 3002", "http://localhost:3001/");
  </script>
</html>

  這代碼沒啥好說的,但是這幾行代碼我寫了一個小時,你猜是爲什麼?通過a標籤來打開新窗口的時候,實際上,是在B頁面(被打開的頁面)率先發起的,在A頁面(打開的頁面)接收到消息後才能把數據傳回去。所以我就想,爲什麼不能在打開的時候就獲取到呢?然後,我就可以主動在A頁面傳輸數據了,不用再來一個來回。但是我試了下不行。爲什麼我試了這麼久呢,因爲我一直記得我在第一遍寫的時候是可以的。

  至於再怎麼從A頁面傳到B頁面,參考1),我歇歇~~~~。

3) 通過iframe獲取window.opener

  iframe方式的的話,其實都類似,都是要獲取到對方的window纔可以。說實話我不太想寫這個,百度一大堆,一百度就是這個,一百度就是這個。還是貼一下代碼吧,就不多說了。複製的過程略了啊。

  首先是index1.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index.html
    <iframe
      src="http://localhost:3002/"
      id="iframe"
      onload="load()"
      frameborder="0"
    ></iframe>
  </body>
  <script>
    function load() {
      const frame = document.getElementById("iframe");
      frame.contentWindow.postMessage(
        "hello index2 , I am index1",
        "http://localhost:3002/"
      );
    }
  </script>
</html>

  然後是index2:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index2.html
  </body>
  <script>
    window.addEventListener("message", function (e) {
      console.log(e);
    });
  </script>
</html>

  肯定有效果,我們發現,通過iframe的onload方法,可以獲取到加載完成後的B頁面,這樣就可以主動發起請求,而不會像之前兩個那樣,在A頁面獲取不到B頁面得加載狀態。而iframe之所以能獲取到onload的狀態(以下純屬我個人猜測,沒有任何考證)是因爲iframe算是一個元素,我在父頁面有很高的操作權限,但是你額外打開一個頁面,可能沒那麼簡單。

  所以,至此,我們可以簡單總結一下,通過postMessage通信的核心是:雙方必須都加載完畢。其次就是:要能獲取到來源頁面的referrer。沒了。

  補充,HTTP請求頭的referer是歷史原因把referrer寫錯了,又沒法改,至於現在是否出了修正我也不知道,大家可以自己查找。

  再補充,我在查資料中還看到說postMessage跨站是不能通信的,說實話我不確定,但是我個人覺得postMessage在跨站的情況下也是可以通信的,因爲跨域本身就包含跨站,另外,我們可以發現,postMessage的本質是在雙方的客戶端頁面都需要識別彼此的必要信息,這樣的前提下就意味着雙方可以確定身份,傳遞信息並不是不安全的。

2、window.name

  window.name,在使用它解決跨域之前哈,我們先了解下它是什麼。window.name其實就是指窗口的名稱,默認是一個空字符串,我們可以給window.name設置一個字符串,在跳轉後的窗口中獲取這個window.name。

  我們先看下代碼,還是之前的結構,不多說,index1.html是這樣的:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index1.html
    <a href="http://localhost:3002" onClick="window.name='zaking-1';"
      >點擊我看看目標頁面的window.name-zaking1</a
    >
    <a href="http://localhost:3002" onClick="window.name='zaking-2';"
      >點擊我看看目標頁面的window.name-zaking2</a
    >
  </body>
</html>

  然後,index2.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index2.html
    <p>window.name值是:<output id="output"></output></p>
  </body>
  <script>
    output.textContent = window.name;
  </script>
</html>

  這樣就可以了,你啓動服務後會發現,是在當前窗口替換了,如果是新打開一個窗口,比如a標籤的target設置爲_blank,則無效,因爲不是之前的窗口了。

  通過這樣的方式再加上iframe,就可以實現跨域傳遞數據。要注意,window.name是瀏覽器窗口的能力,其實與跨域無關,只不過在古老的時代,跨域解決方案太少,可以通過這種能力hack一下,實現跨域場景傳遞數據的需要罷了。現代瀏覽器已經無需如此,大家瞭解下就行了。

  額外要提的一點是,如果你要用iframe,那麼iframe和當前窗口的window並不是同一個window。至於怎麼驗證,我相信你肯定知道。所以,如果你要想用window.name + iframe做跨域通信,就需要一箇中間的iframe作爲轉接,利用其同一個iframe的window.name。

  我也說了,沒啥意義,大家自行了解吧。現代瀏覽器用這玩意,我怕你是會被主管罵。

  既然沒意義,那你寫個毛?嗯~~作爲極個別特殊場景下的極特別方案選型。雖然最後也可能被篩掉。

3、location.hash

  這個東西肯定很熟悉了,url嘛,url的一部分嘛,沒錯。我就簡單提一下吧,跟上面的name一樣,沒啥實際的生產意義。因爲你要用這個東西作爲跨域的方案,就意味着你要捨棄url本身的一些能力,比如,我傳了一個hash,在Vue-Router的hash模式下怎麼辦?能力重合,且爲了解決跨域反而覆蓋了location本身的能力,你還要爲了彌補而添加額外的不穩定且不安全的代碼。付出的代價太大。

  location.hash本身也並不是爲了跨域而存在的,它設計的目的其實就是爲了錨點定位,現代UI框架用它來作爲路由的一種處理方案。

  不多說了,例子可以自行去demo代碼中查看。

  額外多說兩句的是,hash可以傳數據,query呢?params呢?答案是都可以,前提是不要覆蓋它本身的應用場景,因爲本身就是個url跳轉,就是個get請求的url地址,肯定可以獲取到。

4、慣例:階段性總結

  前面兩大部分,實際上我並沒有寫跨域操作DOM的試驗性代碼,因爲你既然能傳遞信息,就可以根據獲取到的信息來修改DOM。而如果你想要直接修改DOM,比如targetDocument.getElementById什麼的,說實話,我也不確定哪些場景可以,但是我們來發散思維,分析一下。

  首先,第一部分的瀏覽器與服務器的HTTP通信的解決方案,與DOM無關。PASS~

  第二部分,有三個解決方案,一個是獲取opener,它可以麼?我覺得可以,因爲你已經獲取到了目標窗口的引用,那麼我猜是可以通過該引用來操作DOM的。而剩下兩種則不可以直接操作,因爲它們沒有直接的關聯關係或獲取途徑。

  當然,以上純屬我瞎猜的,有誤導的可能性,大家理性參考。有興趣可以自己試下哦~

四、跨站了

  哎呦,重點來了,比較核心且複雜的內容來了。 因爲跨域的本地模擬其實很簡單,localhost改個端口號就行了,但是跨站的模擬則要複雜很多,因爲域名不好搞。還記得我們之前用的nginx做反向代理那部分不?嗯,我們要重新修改下nginx的配置:

server {
    listen       8080;
    server_name  index1.zaking.com;
}

  還有另外一個:

server {
    listen       8080;
    server_name  index2.zaking.com;
}

  這裏僅作示例哈,還有一點,就是我們要配置一下代理的地址,具體的去demo裏看吧(其實我是故意想讓你去看demo的)。然後我們啓動下nginx:

sudo brew services restart nginx   

  windows的啓動方法,大家自己去找吧,這也不是講nginx的文章,然後,還沒完,我們還需要啓動我們複製出來的本地server文件,跟之前的結構一樣,不多說啦,嗯~~至少目前是一樣的,頁面裏面啥也沒有,就一點html就可以了,就像這樣:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index2.html
  </body>
</html>

  我想盡量少說廢話,但是又不得不說些廢話,唉~~無所謂了。然後~~然後啓動這兩個本地服務,就像之前那樣,還沒完~~~

  你需要打開你本地的hosts文件,mac的話是在/etc下面,可以在命令行直接輸入:

open /etc

  這樣就可以打開該文件夾,然後找到hosts文件,添加兩個host,就像這樣:

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost
255.255.255.255	broadcasthost
::1             localhost
127.0.0.1 index1.zaking.com
127.0.0.1 index2.zaking.com

  然後保存,可能在保存文件的時候需要你管理員的權限。嗯~~百度~~

  好啦,這樣我們的準備工作就都做完了,我們就可以在瀏覽器裏打開index1.zaking.com了。

   效果不錯吧。index2也是一樣的。那麼準備工作做完了,我們要進入我們的重點了。就是跨站。我們在最開始理論的部分花了一定的篇幅聊了聊什麼是跨站,並且有一個重點就是:跨站一定跨域,但是跨域不一定跨站。大家一定要記住,死記硬背不太好記,大概理解一下就是跨域的要求更多,且包含了跨站的部分,所以定義跨域的範圍比跨站要大。那麼既然如此,我們想象一下:

   跨站了,那麼一定是在跨域的範圍內,所以一定跨站一定跨域,但是我跨域了,可能不一定是屬於跨站的範圍。這樣是不是就很好理解了?

  我記得啊,不好意思,這篇文章是我寫的有史以來最長的又沒法停下來的一篇文章,所以開始的東西有點不記得了,我記得最開始的部分我們好像說過,跨域會影響三部分的內容,我們稍稍回憶下,會影響HTTP、DOM還有本地存儲比如cookie,localstorage啥的。我也是解釋了下爲啥不允許跨站訪問這些數據,簡單說就是爲了隔離用戶。那~~我們來實驗一下吧。

  這是index1.html的代碼:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index1.html
    <iframe
      src="http://index2.zaking.com:8080/"
      onload="load()"
      id="iframe"
      frameborder="0"
    ></iframe>
  </body>
  <script>
    function load() {
      const frame = document.getElementById("iframe");
      console.log(frame.contentWindow.name);
    }
  </script>
</html>

  然後index2.html很簡單:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    我是index2.html
  </body>
  <script>
    var name = "zaking";
  </script>
</html>

  然後我們重啓下本地服務試下:

   完美,不出意外的報錯了,那要怎麼解決呢?在兩個頁面中加上:

document.domain = "zaking.com";

  就可以了,再試下?

 

   完美~~結束了~

  講道理,我覺得到現在就算是完事了,因爲再講其他的東西就要涉及到更多的關於HTTP以及瀏覽器的特性,所以會越寫越多。所以,我糾結了10秒鐘,決定這篇文章到此結束。感謝你能看到這裏。如果你跟着我修改了本地的hosts和nginx,別忘了改回去~

  當然,更多的內容我應該會在我之後的系列博客中寫,不過啥時候我也不知道。

  最後,這篇博客寫的夠長了,但是實際上還有很多問題是存疑或者未解決的,如果後面有機會的話,再針對各解決方案的知識點整理一篇更深入的解析。本文中也或許有些東西雖然我寫出來了,但是理解方向並不正確,希望可以不吝指點。

  說實話我覺得有點虎頭蛇尾,最後跨站的部分其實我還想寫寫cookie的,但是其實重點也說的差不多了,具體例子代碼就暫時不寫了吧。

  最後的最後的最後,感謝~~

  噢噢噢,還有,最後一點,就是不重要你也不需要知道也沒啥意義的解決跨域的方式,就是修改瀏覽器對於跨域的攔截,從瀏覽器配置的層面修改,絕對不建議這麼搞!!!!無論什麼場景都不需要!!!!所以我不會告訴你怎麼改。

  最後的總結,服務器與客戶端跨域,用CORS,客戶端與客戶端的跨域,用postMessage。其他的,知道就行了。沒了~這回真沒了。

參考資料:

  1. 域名的含義
  2. 域名
  3. 統一資源標識符
  4. 極客時間《32 | 同源策略:爲什麼XMLHttpRequest不能跨域請求資源?》
  5. 什麼叫TLD、gTLD、nTLD、ccTLD、iTLD以及幾者之間的關係
  6. Public_Suffix_List
  7. 統一資源標誌符
  8. 九種跨域方式實現原理(完整版)

  9. 跨源資源共享
  10. postMessage
  11. Referrer-Policy
  12. window opener
  13. window.name
  14. 所有示例代碼地址
  15. homebrew鏡像安裝方法:
/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章