跨页面 history state 传递

时间:2025-03-28 09:50:15

背景

最近,在开发过程中遇到了一个 history state 相关的问题。当新开标签页打开新的页面时,是无法传递 history state 的。

问题样例

比如说,下面这个跳转链接通过新开标签页打开,虽然提供了 state 属性,但是由于 history state 不能跨标签页传递,所以在新开的页面获取不到这个 state 的,设置也是白设置。

import React from 'react';
import { Link } from 'react-router-dom';

function Test() {
  return (
    <Link to={{ pathname: '/path/to', state: { a: 1, b: 2 } }} target="_blank">
      跳转链接
    </Link>
  );
}

解决方案

既然不能直接跨页面传递 history state,那么是不是可以间接的传递?是的,解决方案就是先新开一个标签页,但不是直接打开目标页面,而是打开一个中转页面。

在这个中转页面,可以通过一些方式获取到要跳转页面的 pathstate 状态,然后再转到最终的目标页面。

获取 pathstate 有多种方式,一般传递数据量不大的情况,可通过 query string 方式传递至中转页,对于数据量大的情况,则可采用 () 的方式。

对于 query string 方式, pathstate 需要先进行百分号编码以及整体 JSON 序列化,在中转页获取时需要进行相应的解码及解析。

对于 () 方式,可以直接传递对象,不用对数据进行预处理,但使用稍微麻烦点,需要先打开中转页,然后监听中转页相关事件再传递数据。

跳转至中转页后,将会调用 () 方法转至目标页面,这样目标页面就能够获取到 history state

代码实现

中转页代码实现:

/**
 * 跨页面 history state 传递中转页(解决跨页面不能传递 history state 的问题)
 * 使用方法 1(适合传递数据量少的情况):
 * ('/path/to/transfer?data={JSON字符串}')
 * 生成 JSON 字符串样例代码:encodeURIComponent(({path: '/path/to', state: {a:1, b: 'bb'}}))
 *
 * 使用方法 2(使用前提:调用页与中转页同源):
 * const win = ('/path/to/transfer');
 * if (win) {
 *   ('load', function () {
 *     win && ({ type: HISTORY_STATE_TRANSFER_MESSAGE_TYPE, payload: { path: '/path/to', state: { a: 1, b: 'bb' } } }, '*');
 *   });
 * }
 *
 * 使用方法 3(适合非同源的情况):
 * const win = ('/path/to/transfer');
 * function receiveMessage(event: MessageEvent) {
 *   const data = ;
 *   if (win &&  === HISTORY_STATE_TRANSFER_READY_MESSAGE_TYPE) {
 *     ({ type: HISTORY_STATE_TRANSFER_MESSAGE_TYPE, payload: { path: '/path/to', state: { a: 1, b: 'bb' } } }, '*');
 *     ('message', receiveMessage, false);
 *   }
 * }
 * win && ('message', receiveMessage, false);
 */
import React, { useEffect } from 'react';
import { Spin } from 'antd';
import queryString from 'query-string';
import { RouteComponentProps, useHistory } from 'react-router-dom';

// history state 传递消息类型
export const HISTORY_STATE_TRANSFER_MESSAGE_TYPE = 'CrossPageHistoryStateTransfer';
// history state 传递准备消息类型
export const HISTORY_STATE_TRANSFER_READY_MESSAGE_TYPE = 'CrossPageHistoryStateTransferReady';

interface IProps extends RouteComponentProps {}

const CrossPageHistoryStateTransfer = (props: IProps) => {
  const history = useHistory();
  const { data } = queryString.parse(props.location.search, {
    parseNumbers: true,
  });

  let parsedData: { path?: string; state?: any } | null = null;

  if (typeof data === 'string') {
    try {
      parsedData = JSON.parse(decodeURIComponent(data));
    } catch (err) {
      console.log(err);
    }
  }

  useEffect(() => {
    // URL 方式获取数据
    if (parsedData && typeof parsedData?.path === 'string') {
      // 用 replace 跳转,不用 push 是为了避免 back 后又自动 forward
      history.replace(parsedData.path, parsedData.state);
    }
    // postMessage 方式获取数据
    function receiveMessage(event: MessageEvent<{ type: string; payload: { path?: string; state?: any } }>) {
      const data = event.data;
      if (data.type === HISTORY_STATE_TRANSFER_MESSAGE_TYPE && typeof data.payload.path === 'string') {
        history.replace(data.payload.path, data.payload.state);
      }
    }
    window.addEventListener('message', receiveMessage, false);
    window.opener && window.opener.postMessage({ type: HISTORY_STATE_TRANSFER_READY_MESSAGE_TYPE }, '*');
    return () => {
      window.removeEventListener('message', receiveMessage, false);
    };
  }, []);

  return <Spin spinning={true}></Spin>;
};

export default CrossPageHistoryStateTransfer;

中转页调用代码样例:

其中 /path/to/transfer 路径指代中转页路径,/path/to/target 路径指代跳转的目标路径。

方式一(适合传递数据量少的情况):

const json = encodeURIComponent(
  JSON.stringify({
    path: '/path/to/target',
    state: { a: 1, b: 2, c: 3 },
  })
);
window.open(`/path/to/transfer?data=${json}`);

方式二(使用前提:调用页与中转页同源):

const win = window.open('/path/to/transfer');
if (win) {
  win.addEventListener('load', function () {
    win && win.postMessage({ type: HISTORY_STATE_TRANSFER_MESSAGE_TYPE, payload: { path: '/path/to/target', state: { a: 1, b: 'bb' } } }, '*');
  });
}

方式三(适合非同源的情况):

const win = window.open('/path/to/transfer');
function receiveMessage(event: MessageEvent) {
  const data = event.data;
  if (win && data.type === HISTORY_STATE_TRANSFER_READY_MESSAGE_TYPE) {
    win.postMessage({ type: HISTORY_STATE_TRANSFER_MESSAGE_TYPE, payload: { path: '/path/to/target', state: { a: 1, b: 'bb' } } }, '*');
    window.removeEventListener('message', receiveMessage, false);
  }
}
win && window.addEventListener('message', receiveMessage, false);

答疑

有同学可能会问为什么不直接用 URL 传参,非要用 history state

因为如果是简单的场景,比如说只需要一个 id 字段时,这种情况只需要用 URL 传参即可。但对于有些情况,比如说参数非常多,或者参数中包含了 URL 的保留字需要百分号编码等等,这些情况用 URL 传参就不太适合了。