iframe内嵌父子页面路由同步

背景:在前端的项目中,可能会遇到需要在一个项目中内嵌另一个完整项目。今天我们就来讨论讨论利用 iframe 内嵌项目的路由处理。

首先内嵌项目和被内嵌项目都有各自的路由,并均配有路由跳转。假定我们现有两个 umi 项目 A 和 B 并且均已部署发布,url 分别为https://www.myProjectA.comhttps://www.myProjectB.com,需要将项目 A 内嵌至项目 B 的https://www.myProjectB.com/iframe页面中。

在项目 B 的 config 文件夹中的 config.ts 文件中添加路由

1
2
3
4
5
6
7
8
9
10
11
12
export default {
/** ---其他配置--- */
...otherConfig
routes:[
/** ---其他路由地址--- */
...otherRoutes
{
path: '/iframe',
component: 'src/pages/iframe',
}
]
}

在 src/pages/iframe/index.tsx 文件中加入 iframe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';

const Iframe: React.FC = React.memo(() => {
return (
<iframe
src="https://www.myProjectA.com"
name="iframe"
allowFullScreen
id="iframe"
style={{ width: '100%', height: '100%' }}
></iframe>
);
});

export default Iframe;

此时只要 projectA 的地址能够被 projectB 内嵌,那么内嵌就成功啦~不妨试试直接内嵌掘金官网https://juejin.cn吧。

我们可以在项目 B 中访问操作内嵌的项目 A,和直接访问项目 A 几乎没有区别,但是细心的人就会发现,不管项目 A 地址怎么变,项目 B 始终只是 iframe 内嵌页的路由,即https://www.myProjectB.com/iframe。这样就会带来一个问题:我们无法直接在项目 B 中通过路由访问项目 A 的某一个页面,这也是本文探讨的核心问题。

子路由变化通知父页面

首先我们知道项目 B 中的项目 A 要想跳转是其路由发生的改变,要想项目 B 也感知并体现,那么第一步就是要将子路由的改变通知父页面并进行路由改变。iframe 的页面间通信通过 postMessage 方法可以实现。

思路:劫持子页面路由变化,通过 postMessage 通知父页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const { replaceState } = window.history;
// 劫持路由
function hijackHistoryRoute() {
Object.assign(window.history, {
pushState(data: any, unused: string, url?: string | URL) {
// 使用 replaceState
replaceState.call(this, data || { key: Date.now() }, unused, url);
top?.postMessage({ type: 'routePush', payLoad: url }, '*');
},
replaceState(data: any, unused: string, url?: string | URL) {
replaceState.call(this, data || { key: Date.now() }, unused, url);
top?.postMessage({ type: 'routeReplace', payLoad: url }, '*');
},
});
}

useEffect(() => {
// 如果是iframe内嵌页,为实现路由跳转,添加路由劫持
if (window.name === 'iframe') {
hijackHistoryRoute();
}
}, []);

上述实现了子项目共用一个路由,同时增加了路由劫持并通知父页面,笔者将这部分功能加在了全局的一个 context 中,具体引入因项目而不同。此时在父页面操作回退发现,原本可以正常回退现在回退将会退出 iframe 页面,这是因为子项目中只有一个路由,所以相当于父页面当前页也只有一个路由。

父页面监听子页面路由变化改变路由

在第一步中已经实现了子页面路由改变通知父页面,现在需要在父页面将路由的变化体现出来。

思路:将子页面的路由地址作为父页面路由的一个参数,及父页面的路由改造为https://www.myProjectB.com/iframe?url={子路由}

父页面增加监听并处理路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export enum PostMessageType {
ROUTE_PUSH = 'routePush',
ROUTE_REPLACE = 'routeReplace',
}

import { history } from 'umi';

// 路由处理
const handleRouteChange = (toRoute: string, postMessageType: string) => {
const { pathname, search } = window.location;
const current = decodeURIComponent(search.slice(5));
if (current === toRoute) return;
const route = `${pathname}?url=${encodeURIComponent(toRoute)}`;
if (postMessageType === PostMessageType.ROUTE_PUSH) {
history.push(route);
} else if (postMessageType === PostMessageType.ROUTE_REPLACE) {
history.replace(route);
}
};

useEffect(() => {
// 监听postMessage方法
window.addEventListener(
'message',
(e) => {
const postMessageType = e.data.type;
if (
postMessageType === PostMessageType.ROUTE_PUSH ||
postMessageType === PostMessageType.ROUTE_REPLACE
) {
handleRouteChange(e.data.payLoad, postMessageType);
}
},
false,
);
}, []);

路由处理可以直接是子页面的完整路由,也可以是特定路径,为了支持 umi 的 replace 跳转,我这里会将路由处理为跳转目标页的路径。

现在在内嵌页中点击其他路由,路由已经可以完成变化了,但是回退或者刷新页面,我们还是不能进入正确的指定页面。

父页面路由变化通知子页面

子页面路由变化能够同步父页面,现在我们要完成父页面路由变化同步子页面。

思路:监听父页面路由变化,通过 postMessage 告知子页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 对象转为 query string方法
export function objToQueryStr(obj: Record<string, any>) {
if (!obj) return '';
const str: string[] = [];
Object.keys(obj).forEach((key) => {
str.push(`${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`);
});

return str.join('&');
}

import { history, useLocation } from 'umi';

const { query } = useLocation() as any;

useEffect(() => {
const iframe = document.getElementById('iframe') as any;
const iw = iframe?.contentWindow;
if (!iw) return;
iw?.postMessage({ type: 'routeChange', payLoad: objToQueryStr(query).slice(4) }, '*');
}, [query]);

子项目监听 postMessage 并跳转路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const current = `${window.location.pathname}${window.location.search}`;
// 监听postMessage路由改变操作
const handleListenMessage = useCallback(
debounce(
(e) => {
const postMessageType = e.data.type;
if (postMessageType === 'routeChange') {
if (!e.data.payLoad) return;
const route = decodeURIComponent(e.data.payLoad);
if (current === route) return;
history.replace(route);
}
},
200,
{ trailing: true, leading: false },
),
[current],
);

// 如果是内嵌页,添加页面事件监听
useEffect(() => {
if (window.name === 'iframe') {
window.addEventListener('message', handleListenMessage);
}

return () => {
if (window.name === 'iframe') {
window.removeEventListener('message', handleListenMessage);
}
};
}, [current]);

完成子页面监听父页面路由变化并跳转,就实现了父子页面的路由同步,点击内嵌页其他路由,或者点击后退前进按钮路由都能正常跳转。要想刷新页面能进入正确的页面只需支持 iframe 的 src 中带参数即可,子页面识别此地址进行跳转即可。

父页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const [toUrl, setToUrl] = useState('/');

useEffect(() => {
if (query.url) {
setToUrl(query.url);
}
},[]);

return (
<iframe
src=`https://www.myProjectA.com?to={toUrl}`
name="iframe"
allowFullScreen
id="iframe"
style={{ width: '100%', height: '100%' }}
></iframe>
);

子页面

1
2
3
4
5
6
7
import useLocationQuery from '@/hooks/useLocationQuery';

const { to = '/', token = '', tenantId } = useLocationQuery();

useEffect(() => {
history.replace(to);
}, []);

总结

本文探讨实现了在 iframe 内嵌场景中如何实现父子页面的路由同步,也是踩了很多坑实现的,希望对你有帮助~