背景: 在前端的项目中,可能会遇到需要在一个项目中内嵌另一个完整项目。今天我们就来讨论讨论利用 iframe 内嵌项目的路由处理。
首先内嵌项目和被内嵌项目都有各自的路由,并均配有路由跳转。假定我们现有两个 umi 项目 A 和 B 并且均已部署发布,url 分别为https://www.myProjectA.com
和https://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 内嵌场景中如何实现父子页面的路由同步,也是踩了很多坑实现的,希望对你有帮助~