實現本地跨域存儲

什麼是跨域?

先看一下 URL 有哪些部分組成,以下:html

https://github.com:80/gauseen/blog?issues=1#note
\___/  \________/ \_/ \_________/ \______/ \___/
  |         |      |       |          |      |
protocol   host   port  pathname    search  hash

protocol(協議)、host(域名)、port(端口)有一個地方不一樣都會產生跨域現象,也被稱爲客戶端同源策略前端

本地存儲受同源策略限制

客戶端(瀏覽器)出於安全性考慮,不管是 localStorage 仍是 sessionStorage 都會受到同源策略限制。git

那麼如何實現跨域存儲呢?github

window.postMessage()

想要實現跨域存儲,先找到一種可跨域通訊的機制,沒錯,就是 postMessage,它能夠安全的實現跨域通訊,不受同源策略限制。npm

語法:api

otherWindow.postMessage('message', targetOrigin, [transfer])
  • otherWindow 窗口的一個引用,如:iframecontentWindow 屬性,當前 window 對象,window.open 返回的窗口對象等
  • message 將要發送到 otherWindow 的數據
  • targetOrigin 經過窗口的 targetOrigin 屬性來指定哪些窗口能接收到消息事件,其值能夠是字符串 "*"(表示無限制)

實現思路

postMessage 可跨域特性,來實現跨域存儲。由於多個不一樣域下的頁面沒法共享本地存儲數據,咱們須要找個「中轉頁面」來統一處理其它頁面的存儲數據。爲了方便理解,畫了張時序圖,以下:跨域

跨域存儲時序圖

場景模擬

需求:瀏覽器

有兩個不一樣的域名(http://localhost:6001http://localhost:6002)想共用本地存儲中的同一個 token安全

假設:session

http://localhost:6001 對應 client1.html 頁面
http://localhost:6002 對應 client2.html 頁面
http://localhost:6003 對應 hub.html 中轉頁面

啓動服務:

使用 http-server 啓動 3 個本地服務

npm -g install http-server

# 啓動 3 個不一樣端口的服務,模擬跨域現象
http-server -p 6001
http-server -p 6002
http-server -p 6003

簡單實現版本

client1.html 頁面代碼

<body>
  <!-- 開始存儲事件 -->
  <button onclick="handleSetItem()">client1-setItem</button>
  <!-- iframe 嵌套「中轉頁面」 hub.html -->
  <iframe src="http://localhost:6003/hub.html" frameborder="0" id="hub"></iframe>

  <script>
    const $ = id => document.querySelector(id)
    // 獲取 iframe window 對象
    const ifameWin = $('#hub').contentWindow

    let count = 0
    function handleSetItem () {
      let request = {
        // 存儲的方法
        method: 'setItem',
        // 存儲的 key
        key: 'someKey',
        // 須要存儲的數據值
        value: `來自 client-1 消息:${count++}`,
      }
      // 向 iframe 「中轉頁面」發送消息
      ifameWin.postMessage(request, '*')
    }
  </script>
</body>

hub.html 中轉頁面代碼

<body>
  <script>
    // 映射關係
    let map = {
      setItem: (key, value) => window.localStorage['setItem'](key, value),
      getItem: (key) => window.localStorage['getItem'](key),
    }

    // 「中轉頁面」監聽 ifameWin.postMessage() 事件
    window.addEventListener('message', function (e) {
      let { method, key, value } = e.data
      // 處理對應的存儲方法
      let result = map[method](key, value)
      // 返回給當前 client 的數據
      let response = {
        result,
      }
      // 把獲取的數據,傳遞給 client 窗口
      window.parent.postMessage(response, '*')
    })
  </script>
</body>

client2.html 頁面代碼

<body>
  <!-- 獲取本地存儲數據 -->
  <button onclick="handleGetItem()">client2-getItem</button>
  <!-- iframe 嵌套「中轉頁面」 hub.html -->
  <iframe src="http://localhost:6003/hub.html" frameborder="0" id="hub"></iframe>

  <script>
    const $ = id => document.querySelector(id)
    // 獲取 iframe window 對象
    const ifameWin = $('#hub').contentWindow

    function handleGetItem () {
      let request = {
        // 存儲的方法(獲取)
        method: 'getItem',
        // 獲取的 key
        key: 'someKey',
      }
      // 向 iframe 「中轉頁面」發送消息
      ifameWin.postMessage(request, '*')
    }

    // 監聽 iframe 「中轉頁面」返回的消息
    window.addEventListener('message', function (e) {
      console.log('client 2 獲取到數據啦:', e.data)
    })
  </script>
</body>

瀏覽器打開以下地址:

具體效果以下:

跨域存儲 Demo 效果演示

改進版本

分紅 2 個 js 文件,一個是客戶端頁面使用 client.js,另外一個是中轉頁面使用 hub.js

// client.js

class Client {
  constructor (hubUrl) {
    this.hubUrl = hubUrl
    // 全部請求的 id 值(累加)
    this.id = 0
    // 全部請求消息映射
    this._requests = {}
    // 獲取 iframe window 對象
    this._iframeWin = this._createIframe(this.hubUrl).contentWindow
    this._initListener()
  }
  //
  getItem (key, callback) {
    this._requestFn('getItem', {
      key,
      callback,
    })
  }
  setItem (key, value, callback) {
    this._requestFn('setItem', {
      key,
      value,
      callback,
    })
  }
  _requestFn (method, { key, value, callback }) {
    // 發消息時,請求對象格式
    let req = {
      id: this.id++,
      method,
      key,
      value,
    }
    // 請求 id 和回調函數的映射
    this._requests[req.id] = callback
    // 向 iframe 「中轉頁面」發送消息
    this._iframeWin.postMessage(req, '*')
  }
  // 初始化監聽函數
  _initListener () {
    // 監聽 iframe 「中轉頁面」返回的消息
    window.addEventListener('message', (e) => {
      let { id, result } = e.data
      // 找到「中轉頁面」的消息對應的回調函數
      let currentCallback = this._requests[id]
      if (!currentCallback) return
      // 調用並返回數據
      currentCallback(result)
    })
  }
  // 建立 iframe 標籤
  _createIframe (hubUrl) {
    const iframe = document.createElement('iframe')
    iframe.src = hubUrl
    iframe.style = 'display: none;'
    window.document.body.appendChild(iframe)
    return iframe
  }
}
// hub.js

class Hub {
  constructor () {
    this._initListener()
    this.map = {
      setItem: (key, value) => window.localStorage['setItem'](key, value),
      getItem: (key) => window.localStorage['getItem'](key),
    }
  }
  // 監聽 client ifameWin.postMessage() 事件
  _initListener () {
    window.addEventListener('message', (e) => {
      let { method, key, value, id } = e.data
      // 處理對應的存儲方法
      let result = this.map[method](key, value)
      // 返回給當前 client 的數據
      let response = {
        id,
        result,
      }
      // 把獲取的數據,發送給 client 窗口
      window.parent.postMessage(response, '*')
    })
  }
}

頁面使用:

<!-- client1 頁面代碼 -->

<body>
  <button onclick="handleGetItem()">client1-GetItem</button>
  <button onclick="handleSetItem()">client1-SetItem</button>

  <script src="./lib/client.js"></script>
  <script>
    const crossStorage = new Client('http://localhost:6003/hub.html')
    // 在 client1 中,獲取 client2 存儲的數據
    function handleGetItem () {
      crossStorage.getItem('client2Key', (result) => {
        console.log('client-1 getItem result: ', result)
      })
    }

    // client1 本地存儲
    function handleSetItem () {
      crossStorage.setItem('client1Key', 'client-1 value', (result) => {
        console.log('client-1 完成本地存儲')
      })
    }
  </script>
</body>
<!-- hub 頁面代碼 -->

<body>
  <script src="./lib/hub.js"></script>
  <script>
    const hub = new Hub()
  </script>
</body>
<!-- client2 頁面代碼 -->

<body>
  <button onclick="handleGetItem()">client2-GetItem</button>
  <button onclick="handleSetItem()">client2-SetItem</button>

  <script src="./lib/client.js"></script>
  <script>
    const crossStorage = new Client('http://localhost:6003/hub.html')
    // 在 client2 中,獲取 client1 存儲的數據
    function handleGetItem () {
      crossStorage.getItem('client1Key', (result) => {
       console.log('client-2 getItem result: ', result)
      })
    }
    // client2 本地存儲
    function handleSetItem () {
      crossStorage.setItem('client2Key', 'client-2 value', (result) => {
        console.log('client-2 完成本地存儲')
      })
    }
  </script>
</body>

總結

以上就實現了跨域存儲,也是 cross-storage 開源庫的原理。
經過 window.postMessage() api 跨域特性,再配合一個 「中轉頁面」,來完成所謂的「跨域存儲」,實際上並無真正的在瀏覽器端實現跨域存儲,
這是瀏覽器的限制,咱們沒法打破,只能用「曲線救國」的方式,變向來共享存儲數據。

全部源碼在這裏:跨域存儲源碼

歡迎關注無廣告文章、無廣告文章、無廣告文章公衆號:學前端

你的關注、點贊、star 是我最大的動力!謝謝!

參考

相關文章
相關標籤/搜索