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

2022-10-02 18:00:18

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

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

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

第一部分 理論

  這一部分,我們先來看理論,不涉及任何程式碼。主要是講清楚什麼是跨域,同源策略的定義和產生的原因,以及什麼是站點,站點與域又有啥區別?當我們理解了基本的概念之後,我會帶大家梳理瀏覽器允許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)"