业务需求

常常会遇到一些需求会要求不同域名下的各业务或者合作的三方站点进行数据交换,逻辑处理。比如在jwt鉴权的情况下实现sso需要我们把token共享给其他网站。
我们可以采用 postMessagge 和 localStorage 进行跨域共享。

API 设计

分为两种类型的组件:数据中心 和 客户端。该库分为两种类型的组件:数据中心嵌入到每个页面,并直接与 LocalStorage API 交互。然后,客户端将这数据中心通过 iframe 的方式嵌入并发布消息,以请求存储,检索和删除数据。这允许多个客户端访问和共享位于单个存储中的数据。
在初始化数据中心时应该传递一组权限对象,使其来自与不信任来源的消息及不被允许的方法被忽略。客服端 API 应当支持 Promise。
数据中心的 API

let hub = new crosData.Hub([
  {
    origin: /.*localhost:1000\d$/,
    allow: ["get", "set", "del"]
  }
]);

客户端的 API

let client = new crosData.Client("http://localhost:10002/hub.html");
client.set("token", 1234).then(res => {
  console.log(res);
});
client.get("token").then(res => {
  console.log(res);
});
client.del("token").then(res => {
  console.log(res);
});
client.clear().then(res => {
  console.log(res);
});

编码思路

客户端初始化会创建一个iframe,地址为数据中心的地址,调用方法时会向数据中心执行postMessage事件,客户端会监听message事件,收到数据中心的响应后会根据响应执行reslove或reject,返回数据。
client代码:

import { Action, Message, Res } from "./interface";
export class Client {
  parent: Window;
  origin: string;
  sendQueue: Function[];
  messageId: number;
  cbs: any;
  child: Window | null;
  constructor(iframeUrl: string) {
    this.parent = window;
    this.origin = new URL(iframeUrl).origin;
    this.sendQueue = [];
    this.messageId = 0;
    this.cbs = {};
    this.child = null;
    this.createIframe(iframeUrl);
    window.addEventListener("message", (evt: MessageEvent) => {
      if (typeof evt.data !== "string" || evt.data === "") {
        return;
      }
      const res: Res = JSON.parse(evt.data);
      if (res.status) {
        if (evt.origin === this.origin) {
          this.cbs[res.messageId].reslove(res.data);
          delete this.cbs[res.messageId];
        }
      } else {
        this.cbs[res.messageId].reject(res.error);
        delete this.cbs[res.messageId];
      }
    });
  }
  createIframe(url: string) {
    let frame = document.createElement("iframe");
    frame.style.cssText =
      "width:1px;height:1px;border:0;position:absolute;left:-9999px;top:-9999px;";
    frame.setAttribute("src", url);
    document.body.appendChild(frame);
    frame.onload = () => {
      this.child = frame.contentWindow;
      this.sendQueue.forEach(item => item());
    };
  }
  postHanle(action: Action, key?: string, val?: string) {
    const message: Message = {
      messageId: this.messageId,
      action: action,
      origin: new URL(location.href).origin,
      data: key ? { key: key, val: val } : undefined
    };
    if (this.child) {
      this.child.postMessage(JSON.stringify(message), this.origin);
    } else {
      this.sendQueue.push(() => {
        this.child!.postMessage(JSON.stringify(message), this.origin);
      });
    }
    return new Promise((reslove, reject) => {
      this.cbs[this.messageId] = {
        reslove,
        reject
      };
      this.messageId++;
    });
  }
  set(key: string, val: string) {
    return this.postHanle("set", key, val);
  }
  get(key: string, val: string) {
    return this.postHanle("get", key, val);
  }
  del(key: string, val: string) {
    return this.postHanle("del", key, val);
  }
  clear() {
    return this.postHanle("clear");
  }
}

数据中心会监听message事件,根据客户端发送的message事件调用相关方法,发送数据。
hub代码:

import { Action, Message, Res, Permission } from "./interface";

export class Hub {
  Permissions: Permission[] = [];
  constructor(Permissions: Permission[]) {
    this.Permissions = Permissions;
    window.addEventListener("message", (evt: MessageEvent) => {
      if (typeof evt.data !== "string" || evt.data === "") {
        return;
      }
      const message: Message = JSON.parse(evt.data);
      if (message.origin === evt.origin) {
        const { data, action } = message;
        if (!this.permitted(message)) {
          let res: Res = {
            messageId: message.messageId,
            data: {},
            status: false,
            error: "没有权限"
          };
          window.top.postMessage(JSON.stringify(res), evt.origin);
        } else {
          let res = this[action].apply(this, [message]);
          window.top.postMessage(JSON.stringify(res), evt.origin);
        }
      }
    });
  }
  permitted(message: Message): Boolean {
    for (let index = 0; index < this.Permissions.length; index++) {
      const item = this.Permissions[index];
      if (item.origin.test(message.origin)) {
        return item.allow.indexOf(message.action) > -1;
      }
    }
    return false;
  }
  set(message: Message): Res {
    window.localStorage.setItem(message.data!.key, message.data!.val!);
    return {
      messageId: message.messageId,
      data: {
        [message.data!.key]: message.data!.val
      },
      status: true
    };
  }
  get(message: Message): Res {
    window.localStorage.getItem(message.data!.key);
    return {
      messageId: message.messageId,
      data: {
        [message.data!.key]: message.data!.val
      },
      status: true
    };
  }
  del(message: Message): Res {
    window.localStorage.removeItem(message.data!.key);
    return {
      messageId: message.messageId,
      data: {
        [message.data!.key]: message.data!.val
      },
      status: true
    };
  }
  clear(message: Message): Res {
    window.localStorage.clear();
    return {
      messageId: message.messageId,
      status: true
    };
  }
}

改进点

  • 可以加入过期时间限制
  • 可以用 lz-string 对数据进行压缩

源码

我已经基于这个思路发了一个npm的包 cros-data,如果有需要可以直接 npm install cros-data -s 安装使用。
源码地址: cros-data
文档地址: 文档

Last modification:June 30, 2021
如果觉得我的文章对你有用,请随意赞赏