跨域我知道,存储我知道,那跨域存储呢?

关注  高级前端进阶 ,回复“ 加群

加入我们一起学习,天天进步

作者:gauseen

来源:https://github.com/gauseen/blog

什么是跨域?

先看一下 URL 有哪些部分组成,如下:

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

protocol(协议)、host(域名)、port(端口)有一个地方不同都会产生跨域现象,也被称为客户端同源策略

本地存储受同源策略限制

客户端(浏览器)出于安全性考虑,无论是 localStorage 还是  sessionStorage 都会受到同源策略限制。

那么如何实现 跨域存储 呢?

window.postMessage()

想要实现 跨域存储 ,先找到一种可跨域通信的机制,没错,就是  postMessage ,它可以安全的实现跨域通信,不受同源策略限制。

语法:

otherWindow.postMessage('message', targetOrigin, [transfer])
  • otherWindow 窗口的一个引用,如:
    iframe 的 
    contentWindow 属性,当前 
    window 对象,
    window.open 返回的窗口对象等
  • message 将要发送到 
    otherWindow 的数据
  • targetOrigin 通过窗口的 
    targetOrigin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串 
    "*" (表示无限制)

实现思路

postMessage 可跨域特性,来实现跨域存储。因为多个不同域下的页面无法共享本地存储数据,我们需要找个“中转页面”来统一处理其它页面的存储数据。为了方便理解,画了张时序图,如下:

跨域存储时序图

场景模拟

需求:

有两个不同的域名( http://localhost:6001 和  http://localhost:6002 )想共用本地存储中的同一个  token 作为统一登录凭证:

假设:

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 页面代码

  
  
  
  

  
    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, '*')
    }
  

hub.html 中转页面代码

  
    // 映射关系
    let map = {
      setItem: (key, value) => window.localStorage['setItem'](key, value "'setItem'"),
      getItem: (key) => window.localStorage['getItem'](key "'getItem'"),
    }

    // “中转页面”监听 ifameWin.postMessage() 事件
    window.addEventListener('message', function (e) {
      let { method, key, value } = e.data
      // 处理对应的存储方法
      let result = map[method](key, value "method")
      // 返回给当前 client 的数据
      let response = {
        result,
      }
      // 把获取的数据,传递给 client 窗口
      window.parent.postMessage(response, '*')
    })
  

client2.html 页面代码

  
  
  
  

  
    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)
    })
  

浏览器打开如下地址:

  • http://localhost:6001/client1.html

  • http://localhost:6002/client2.html

具体效果如下

改进版本

共拆分成 2 个 js 文件,一个是客户端页面使用 client.js ,另一个是中转页面使用  hub.js,具体代码如下:

// client.js

class Client {
  constructor (hubUrl) {
    this.hubUrl = hubUrl
    // 每个请求的 id 值,作为唯一标识(累加)
    this.id = 0
    // 所有请求消息数据映射(如:getItem、setItem)
    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 "'setItem'"),
      getItem: (key) => window.localStorage['getItem'](key "'getItem'"),
    }
  }
  // 监听 client ifameWin.postMessage() 事件
  _initListener () {
    window.addEventListener('message', (e) => {
      let { method, key, value, id } = e.data
      // 处理对应的存储方法
      let result = this.map[method](key, value "method")
      // 返回给当前 client 的数据
      let response = {
        id,
        result,
      }
      // 把获取的数据,发送给 client 窗口
      window.parent.postMessage(response, '*')
    })
  }
}

页面使用:




  
  

  
  
    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 完成本地存储')
      })
    }
  




  
  
    const hub = new Hub()
  




  
  

  
  
    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 完成本地存储')
      })
    }
  

总结

以上就实现了跨域存储,也是 cross-storage [2] 开源库的核心原理。通过  window.postMessage() api 跨域特性,再配合一个 “中转页面”,来完成所谓的“跨域存储”,实际上并没有真正的在浏览器端实现跨域存储,这是浏览器的限制,我们无法打破,只能用“曲线救国”的方式,变向来共享存储数据。

所有源码在这里: 跨域存储源码 [3]

参考资料