背景
最近,在开发过程中遇到了一个 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
,那么是不是可以间接的传递?是的,解决方案就是先新开一个标签页,但不是直接打开目标页面,而是打开一个中转页面。
在这个中转页面,可以通过一些方式获取到要跳转页面的 path
和 state
状态,然后再转到最终的目标页面。
获取 path
和 state
有多种方式,一般传递数据量不大的情况,可通过 query string
方式传递至中转页,对于数据量大的情况,则可采用 ()
的方式。
对于 query string
方式, path
和 state
需要先进行百分号编码以及整体 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
传参就不太适合了。