diff --git a/packages/plugin-qiankun/src/common.test.ts b/packages/plugin-qiankun/src/common.test.ts index 79813eab..c6fe7966 100644 --- a/packages/plugin-qiankun/src/common.test.ts +++ b/packages/plugin-qiankun/src/common.test.ts @@ -2,107 +2,184 @@ * @author Kuitos * @since 2019-10-22 */ +import 'jest'; +import { testPathWithPrefix, insertRoute } from './common'; -import { testPathWithPrefix } from './common'; +describe('testPathPrefix', () => { + test('testPathPrefix', () => { + // browser history + expect(testPathWithPrefix('/js', '/')).toBeFalsy(); -test('testPathPrefix', () => { - // browser history - expect(testPathWithPrefix('/js', '/')).toBeFalsy(); + expect(testPathWithPrefix('/js', '/js')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/jss')).toBeFalsy(); + expect(testPathWithPrefix('/js', '/js/')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/s')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/s/a')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/s?a=b')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js?a=b')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js?')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/?')).toBeTruthy(); + expect(testPathWithPrefix('/js', '/js/?a=b')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/jss')).toBeFalsy(); - expect(testPathWithPrefix('/js', '/js/')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/s')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/s/a')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/s?a=b')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js?a=b')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js?')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/?')).toBeTruthy(); - expect(testPathWithPrefix('/js', '/js/?a=b')).toBeTruthy(); + // hash history + expect(testPathWithPrefix('#/js', '#/js')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/jss')).toBeFalsy(); + expect(testPathWithPrefix('#/js', '#/js/')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/s')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/s/a')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/s?a=b')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js?a=b')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js?')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/?')).toBeTruthy(); + expect(testPathWithPrefix('#/js', '#/js/?a=b')).toBeTruthy(); - // hash history - expect(testPathWithPrefix('#/js', '#/js')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/jss')).toBeFalsy(); - expect(testPathWithPrefix('#/js', '#/js/')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/s')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/s/a')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/s?a=b')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js?a=b')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js?')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/?')).toBeTruthy(); - expect(testPathWithPrefix('#/js', '#/js/?a=b')).toBeTruthy(); + // browser history with slash ending + expect(testPathWithPrefix('/js/', '/js')).toBeFalsy(); + expect(testPathWithPrefix('/js/', '/jss')).toBeFalsy(); + expect(testPathWithPrefix('/js/', '/js/')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js/s')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js/s/a')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js/s?a=b')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js?a=b')).toBeFalsy(); + expect(testPathWithPrefix('/js/', '/js?')).toBeFalsy(); + expect(testPathWithPrefix('/js/', '/js/?')).toBeTruthy(); + expect(testPathWithPrefix('/js/', '/js/?a=b')).toBeTruthy(); - // browser history with slash ending - expect(testPathWithPrefix('/js/', '/js')).toBeFalsy(); - expect(testPathWithPrefix('/js/', '/jss')).toBeFalsy(); - expect(testPathWithPrefix('/js/', '/js/')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js/s')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js/s/a')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js/s?a=b')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js?a=b')).toBeFalsy(); - expect(testPathWithPrefix('/js/', '/js?')).toBeFalsy(); - expect(testPathWithPrefix('/js/', '/js/?')).toBeTruthy(); - expect(testPathWithPrefix('/js/', '/js/?a=b')).toBeTruthy(); + // hash history with slash ending + expect(testPathWithPrefix('#/js/', '#/js')).toBeFalsy(); + expect(testPathWithPrefix('#/js/', '#/jss')).toBeFalsy(); + expect(testPathWithPrefix('#/js/', '#/js/')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js/s')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js/s/a')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js/s?a=b')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js?a=b')).toBeFalsy(); + expect(testPathWithPrefix('#/js/', '#/js?')).toBeFalsy(); + expect(testPathWithPrefix('#/js/', '#/js/?')).toBeTruthy(); + expect(testPathWithPrefix('#/js/', '#/js/?a=b')).toBeTruthy(); - // hash history with slash ending - expect(testPathWithPrefix('#/js/', '#/js')).toBeFalsy(); - expect(testPathWithPrefix('#/js/', '#/jss')).toBeFalsy(); - expect(testPathWithPrefix('#/js/', '#/js/')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js/s')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js/s/a')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js/s?a=b')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js?a=b')).toBeFalsy(); - expect(testPathWithPrefix('#/js/', '#/js?')).toBeFalsy(); - expect(testPathWithPrefix('#/js/', '#/js/?')).toBeTruthy(); - expect(testPathWithPrefix('#/js/', '#/js/?a=b')).toBeTruthy(); + // browser history with dynamic route + expect(testPathWithPrefix('/:abc', '/js')).toBeTruthy(); + expect(testPathWithPrefix('/:abc', '/123')).toBeTruthy(); + expect(testPathWithPrefix('/:abc/', '/js/')).toBeTruthy(); + expect(testPathWithPrefix('/:abc/js', '/js/js')).toBeTruthy(); + expect(testPathWithPrefix('/:abc/js', '/js/123')).toBeFalsy(); + expect(testPathWithPrefix('/js/:abc', '/js/123')).toBeTruthy(); + expect(testPathWithPrefix('/js/:abc/', '/js/123')).toBeFalsy(); + expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/jsx')).toBeTruthy(); + expect( + testPathWithPrefix('/js/:abc/jsx', '/js/123/jsx/hello'), + ).toBeTruthy(); + expect(testPathWithPrefix('/js/:abc/jsx', '/js/123')).toBeFalsy(); + expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/css')).toBeFalsy(); + expect( + testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsx/kkk'), + ).toBeTruthy(); + expect(testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsk')).toBeFalsy(); + expect( + testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsx/kkk/hello'), + ).toBeTruthy(); + expect(testPathWithPrefix('/js/:abc?', '/js')).toBeTruthy(); + expect(testPathWithPrefix('/js/*', '/js/245')).toBeTruthy(); - // browser history with dynamic route - expect(testPathWithPrefix('/:abc', '/js')).toBeTruthy(); - expect(testPathWithPrefix('/:abc', '/123')).toBeTruthy(); - expect(testPathWithPrefix('/:abc/', '/js/')).toBeTruthy(); - expect(testPathWithPrefix('/:abc/js', '/js/js')).toBeTruthy(); - expect(testPathWithPrefix('/:abc/js', '/js/123')).toBeFalsy(); - expect(testPathWithPrefix('/js/:abc', '/js/123')).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc/', '/js/123')).toBeFalsy(); - expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/jsx')).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/jsx/hello')).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc/jsx', '/js/123')).toBeFalsy(); - expect(testPathWithPrefix('/js/:abc/jsx', '/js/123/css')).toBeFalsy(); - expect( - testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsx/kkk'), - ).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsk')).toBeFalsy(); - expect( - testPathWithPrefix('/js/:abc/jsx/:cde', '/js/123/jsx/kkk/hello'), - ).toBeTruthy(); - expect(testPathWithPrefix('/js/:abc?', '/js')).toBeTruthy(); - expect(testPathWithPrefix('/js/*', '/js/245')).toBeTruthy(); + // hash history with dynamic route + expect(testPathWithPrefix('#/:abc', '#/js')).toBeTruthy(); + expect(testPathWithPrefix('#/:abc', '#/123')).toBeTruthy(); + expect(testPathWithPrefix('#/:abc/', '#/js/')).toBeTruthy(); + expect(testPathWithPrefix('#/:abc/js', '#/js/js')).toBeTruthy(); + expect(testPathWithPrefix('#/:abc/js', '#/js/123')).toBeFalsy(); + expect(testPathWithPrefix('#/js/:abc', '#/js/123')).toBeTruthy(); + expect(testPathWithPrefix('#/js/:abc/', '#/js/123')).toBeFalsy(); + expect(testPathWithPrefix('#/js/:abc/', '#/js/123/jsx')).toBeTruthy(); + expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx')).toBeTruthy(); + expect( + testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx/hello'), + ).toBeTruthy(); + expect( + testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx/hello?test=1'), + ).toBeTruthy(); + expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123')).toBeFalsy(); + expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123/css')).toBeFalsy(); + expect( + testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsx/kkk'), + ).toBeTruthy(); + expect( + testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsx/kkk/hello'), + ).toBeTruthy(); + expect( + testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsk'), + ).toBeFalsy(); + expect(testPathWithPrefix('#/js/:abc?', '#/js')).toBeTruthy(); + expect(testPathWithPrefix('#/js/*', '#/js/245')).toBeTruthy(); + }); +}); + +describe('test insert route', () => { + test('insert', () => { + const mockRoutes = [{ path: '/a' }]; + insertRoute(mockRoutes, { path: '/a/b', insert: '/a' }); + expect(mockRoutes).toEqual([ + { path: '/a', exact: false, routes: [{ insert: '/a', path: '/a/b' }] }, + ]); + }); + test('insert with children routes', () => { + const mockRoutes = [{ path: '/a' }]; + insertRoute(mockRoutes, { + path: '/a/b', + insert: '/a', + routes: [{ path: '/a/b/c' }], + }); + expect(mockRoutes).toEqual([ + { + path: '/a', + exact: false, + routes: [{ insert: '/a', path: '/a/b', routes: [{ path: '/a/b/c' }] }], + }, + ]); + }); + test('insert into children routes', () => { + const mockRoutes = [{ path: '/a', routes: [{ path: '/a/b' }] }]; + insertRoute(mockRoutes, { path: '/a/b/c', insert: '/a/b' }); + expect(mockRoutes).toEqual([ + { + path: '/a', + routes: [ + { + path: '/a/b', + exact: false, + routes: [{ path: '/a/b/c', insert: '/a/b' }], + }, + ], + }, + ]); + }); + + test('insert node does not exist', () => { + const mockRoutes = [{ path: '/a' }]; + const mockInsert = { path: '/a/b', insert: '/b' }; + const errorFn = jest.fn(); + try { + insertRoute(mockRoutes, mockInsert); + } catch (e) { + errorFn(); + expect(e.message).toEqual( + `[plugin-qiankun]: path "${mockInsert.insert}" not found`, + ); + } + expect(errorFn).toBeCalled(); + }); - // hash history with dynamic route - expect(testPathWithPrefix('#/:abc', '#/js')).toBeTruthy(); - expect(testPathWithPrefix('#/:abc', '#/123')).toBeTruthy(); - expect(testPathWithPrefix('#/:abc/', '#/js/')).toBeTruthy(); - expect(testPathWithPrefix('#/:abc/js', '#/js/js')).toBeTruthy(); - expect(testPathWithPrefix('#/:abc/js', '#/js/123')).toBeFalsy(); - expect(testPathWithPrefix('#/js/:abc', '#/js/123')).toBeTruthy(); - expect(testPathWithPrefix('#/js/:abc/', '#/js/123')).toBeFalsy(); - expect(testPathWithPrefix('#/js/:abc/', '#/js/123/jsx')).toBeTruthy(); - expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx')).toBeTruthy(); - expect( - testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx/hello'), - ).toBeTruthy(); - expect( - testPathWithPrefix('#/js/:abc/jsx', '#/js/123/jsx/hello?test=1'), - ).toBeTruthy(); - expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123')).toBeFalsy(); - expect(testPathWithPrefix('#/js/:abc/jsx', '#/js/123/css')).toBeFalsy(); - expect( - testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsx/kkk'), - ).toBeTruthy(); - expect( - testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsx/kkk/hello'), - ).toBeTruthy(); - expect(testPathWithPrefix('#/js/:abc/jsx/:cde', '#/js/123/jsk')).toBeFalsy(); - expect(testPathWithPrefix('#/js/:abc?', '#/js')).toBeTruthy(); - expect(testPathWithPrefix('#/js/*', '#/js/245')).toBeTruthy(); + test('insert path does not follow hierarchy', () => { + const mockRoutes = [{ path: '/a' }]; + const mockInsert = { path: '/b', insert: '/a' }; + const errorFn = jest.fn(); + try { + insertRoute(mockRoutes, mockInsert); + } catch (e) { + errorFn(); + expect(e.message).toEqual( + `[plugin-qiankun]: path "${mockInsert.path}" need to starts with "${mockRoutes[0].path}"`, + ); + } + expect(errorFn).toBeCalled(); + }); }); diff --git a/packages/plugin-qiankun/src/common.ts b/packages/plugin-qiankun/src/common.ts index 14413857..a06d0f1a 100644 --- a/packages/plugin-qiankun/src/common.ts +++ b/packages/plugin-qiankun/src/common.ts @@ -4,6 +4,7 @@ */ import { ReactComponentElement } from 'react'; +import type { IRouteProps } from 'umi'; export const defaultMountContainerId = 'root-subapp'; @@ -88,3 +89,43 @@ export function patchMicroAppRoute( route.component = getMicroAppRouteComponent(opts); } } + +const recursiveSearch = ( + routes: IRouteProps[], + path: string, +): IRouteProps | null => { + for (let i = 0; i < routes.length; i++) { + if (routes[i].path === path) { + return routes[i]; + } + if (routes[i].routes && routes[i].routes?.length) { + const found = recursiveSearch(routes[i].routes || [], path); + if (found) { + return found; + } + } + } + return null; +}; + +export function insertRoute(routes: IRouteProps[], microAppRoute: IRouteProps) { + const found = recursiveSearch(routes, microAppRoute.insert); + if (found) { + if ( + !microAppRoute.path || + !found.path || + !microAppRoute.path.startsWith(found.path) + ) { + throw new Error( + `[plugin-qiankun]: path "${microAppRoute.path}" need to starts with "${found.path}"`, + ); + } + found.exact = false; + found.routes = found.routes || []; + found.routes.push(microAppRoute); + } else { + throw new Error( + `[plugin-qiankun]: path "${microAppRoute.insert}" not found`, + ); + } +} diff --git a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl index 16fe26be..edc0cb72 100644 --- a/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl +++ b/packages/plugin-qiankun/src/master/masterRuntimePlugin.ts.tpl @@ -7,13 +7,15 @@ import { prefetchApps, registerMicroApps, start } from 'qiankun'; // @ts-ignore import { ApplyPluginsType, getMicroAppRouteComponent, plugin } from 'umi'; -import { defaultMountContainerId, noop, patchMicroAppRoute, testPathWithPrefix, toArray } from './common'; +import { defaultMountContainerId, noop, patchMicroAppRoute, testPathWithPrefix, toArray, insertRoute } from './common'; import { defaultHistoryType } from './constants'; import { getMasterOptions, setMasterOptions } from './masterOptions'; // @ts-ignore import { deferred } from './qiankunDefer.js'; import { App, HistoryType, MasterOptions, MicroAppRoute } from './types'; +let microAppRuntimeRoutes: MicroAppRoute[]; + async function getMasterRuntime() { const config = await plugin.applyPlugins({ key: 'qiankun', @@ -25,7 +27,35 @@ async function getMasterRuntime() { return master || config; } -let microAppRuntimeRoutes: MicroAppRoute[]; +// modify route with "microApp" attribute to use real component +function patchMicroAppRouteComponent(routes: IRouteProps[]) { + const getRootRoutes = (routes: IRouteProps[]) => { + const rootRoute = routes.find(route => route.path === '/'); + if (rootRoute) { + // 如果根路由是叶子节点,则直接返回其父节点 + if (!rootRoute.routes) { + return routes; + } + + return getRootRoutes(rootRoute.routes); + } + + return routes; + }; + + const rootRoutes = getRootRoutes(routes); + if (rootRoutes) { + const { routeBindingAlias, base, masterHistoryType } = getMasterOptions() as MasterOptions; + microAppRuntimeRoutes.reverse().forEach(microAppRoute => { + patchMicroAppRoute(microAppRoute, getMicroAppRouteComponent, { base, masterHistoryType, routeBindingAlias }); + if (microAppRoute.insert) { + insertRoute(routes, microAppRoute); + } else { + rootRoutes.unshift(microAppRoute); + } + }); + } +} export async function render(oldRender: typeof noop) { const runtimeOptions = await getMasterRuntime(); @@ -80,31 +110,9 @@ export async function render(oldRender: typeof noop) { } } -export function patchRoutes(opts: { routes: IRouteProps[] }) { +export function patchRoutes({ routes }: { routes: IRouteProps[] }) { if (microAppRuntimeRoutes) { - const getRootRoutes = (routes: IRouteProps[]) => { - const rootRoute = routes.find(route => route.path === '/'); - if (rootRoute) { - // 如果根路由是叶子节点,则直接返回其父节点 - if (!rootRoute.routes) { - return routes; - } - - return getRootRoutes(rootRoute.routes); - } - - return routes; - }; - - const { routes } = opts; - const rootRoutes = getRootRoutes(routes); - if (rootRoutes) { - const { routeBindingAlias, base, masterHistoryType } = getMasterOptions() as MasterOptions; - microAppRuntimeRoutes.reverse().forEach(microAppRoute => { - patchMicroAppRoute(microAppRoute, getMicroAppRouteComponent, { base, masterHistoryType, routeBindingAlias }); - rootRoutes.unshift(microAppRoute); - }); - } + patchMicroAppRouteComponent(routes) } }