mirror of https://github.com/alibaba/ice.git
				
				
				
			feat: route data
This commit is contained in:
		
							parent
							
								
									ecd64cd762
								
							
						
					
					
						commit
						d4cbe9182b
					
				|  | @ -3,7 +3,6 @@ import type { AppConfig } from 'ice'; | |||
| const appConfig: AppConfig = { | ||||
|   app: { | ||||
|     getInitialData: async (ctx) => { | ||||
|       console.log(ctx); | ||||
|       return { | ||||
|         auth: { | ||||
|           admin: true, | ||||
|  |  | |||
|  | @ -5,11 +5,17 @@ import './index.css'; | |||
| export default function Home() { | ||||
|   const appContext = useAppContext(); | ||||
| 
 | ||||
|   console.log('Home Page: appContext', appContext); | ||||
| 
 | ||||
|   return <><h2>Home Page</h2><Link to="/about">about</Link></>; | ||||
| } | ||||
| 
 | ||||
| export function getPageConfig() { | ||||
|   return { | ||||
|     scripts: [ | ||||
|       { src: 'https://g.alicdn.com/alilog/mlog/aplus_v2.js', block: true }, | ||||
|     ], | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| Home.pageConfig = { | ||||
|   auth: ['admin'], | ||||
| }; | ||||
|  | @ -18,7 +18,7 @@ function generateComponentsImportStr(routeManifest: RouteManifest) { | |||
|       let { file, componentName } = routeManifest[id]; | ||||
|       const fileExtname = path.extname(file); | ||||
|       file = file.replace(new RegExp(`${fileExtname}$`), ''); | ||||
|       return `${prev}const ${componentName} = React.lazy(() => import(/* webpackChunkName: "${componentName}" */ '@/${file}'))\n`; | ||||
|       return `${prev}import * as ${componentName} from '@/${file}'; \n`; | ||||
|   }, ''); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ export default function App(props: Props) { | |||
| 
 | ||||
|   let element; | ||||
|   if (routes.length === 1 && !routes[0].children) { | ||||
|     const Page = routes[0].component; | ||||
|     const Page = routes[0].component.default; | ||||
|     element = <Page />; | ||||
|   } else { | ||||
|     element = <AppRouter PageWrappers={PageWrappers} />; | ||||
|  |  | |||
|  | @ -32,12 +32,12 @@ function Routes({ routes }: RoutesProps) { | |||
| } | ||||
| 
 | ||||
| function updateRouteElement(routes: RouteItem[], PageWrappers?: PageWrapper<any>[]) { | ||||
|   return routes.map(({ path, component: PageComponent, children, index, load }: RouteItem) => { | ||||
|   return routes.map(({ path, component: pageComponent, children, index, load }: RouteItem) => { | ||||
|     let element; | ||||
| 
 | ||||
|     if (PageComponent) { | ||||
|     if (pageComponent) { | ||||
|       element = ( | ||||
|         <RouteWrapper PageComponent={PageComponent} PageWrappers={PageWrappers} /> | ||||
|         <RouteWrapper PageComponent={pageComponent.default} PageWrappers={PageWrappers} /> | ||||
|       ); | ||||
|     } else if (load) { | ||||
|       const LazyComponent = React.lazy(load); | ||||
|  |  | |||
|  | @ -0,0 +1,102 @@ | |||
| import * as React from 'react'; | ||||
| import { useDocumentContext } from './DocumentContext.js'; | ||||
| 
 | ||||
| export function Meta() { | ||||
|   const { matches, routeData } = useDocumentContext(); | ||||
|   let metas = []; | ||||
| 
 | ||||
|   matches.forEach(match => { | ||||
|     const { route } = match; | ||||
|     const { componentName } = route; | ||||
| 
 | ||||
|     const customMetas = routeData?.[componentName]?.pageConfig?.metas; | ||||
| 
 | ||||
|     // custom scripts
 | ||||
|     if (customMetas) { | ||||
|       metas = metas.concat(customMetas); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {metas && metas.map(meta => <meta {...meta} />)} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function Links() { | ||||
|   const { matches, routeData } = useDocumentContext(); | ||||
|   const links = []; | ||||
| 
 | ||||
|   matches.forEach(match => { | ||||
|     const { route } = match; | ||||
|     const { componentName } = route; | ||||
|     const customLinks = routeData?.[componentName]?.pageConfig?.links; | ||||
| 
 | ||||
|     // custom scripts
 | ||||
|     if (customLinks) { | ||||
|       customLinks.forEach((link) => { | ||||
|         const { block, ...props } = link; | ||||
|         if (block) { | ||||
|           links.push(props); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     // pages bundles
 | ||||
|     links.push({ | ||||
|       // TODO: get from build manifest
 | ||||
|       src: `./${componentName}.css`, | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {links && links.map(link => <link key={link.href} {...link} />)} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function Scripts() { | ||||
|   const { matches, routeData } = useDocumentContext(); | ||||
|   const scripts = []; | ||||
| 
 | ||||
|   matches.forEach(match => { | ||||
|     const { route } = match; | ||||
|     const { componentName } = route; | ||||
|     const customScripts = routeData?.[componentName]?.pageConfig?.scripts; | ||||
| 
 | ||||
|     // custom scripts
 | ||||
|     if (customScripts) { | ||||
|       customScripts.forEach((script) => { | ||||
|         const { block, ...props } = script; | ||||
|         if (block) { | ||||
|           scripts.push(props); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     // pages bundles
 | ||||
|     scripts.push({ | ||||
|       // TODO: get from build manifest
 | ||||
|       src: `./${componentName}.js`, | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       { | ||||
|         scripts.map(script => <script key={script.src} {...script} />) | ||||
|       } | ||||
|       {/* main entry */} | ||||
|       <script src="./main.js" /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function Root() { | ||||
|   const { html } = useDocumentContext(); | ||||
| 
 | ||||
|   // eslint-disable-next-line react/self-closing-comp
 | ||||
|   return <div id="root" dangerouslySetInnerHTML={{ __html: html || '' }}></div>; | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| import * as React from 'react'; | ||||
| import type { RouteItem, RouteMatch, RouteData } from './types'; | ||||
| 
 | ||||
| interface DocumentContext { | ||||
|   html?: string; | ||||
|   matches?: RouteMatch<RouteItem>[]; | ||||
|   routeData?: RouteData; | ||||
| } | ||||
| 
 | ||||
| const Context = React.createContext<DocumentContext>(null); | ||||
| 
 | ||||
| Context.displayName = 'DocumentContext'; | ||||
| 
 | ||||
| const useDocumentContext = () => { | ||||
|   const value = React.useContext(Context); | ||||
|   return value; | ||||
| }; | ||||
| 
 | ||||
| const DocumentContextProvider = Context.Provider; | ||||
| 
 | ||||
| export { | ||||
|   useDocumentContext, | ||||
|   DocumentContextProvider, | ||||
| }; | ||||
|  | @ -0,0 +1,18 @@ | |||
| import type { RouteObject } from 'react-router-dom'; | ||||
| import { matchRoutes } from 'react-router-dom'; | ||||
| 
 | ||||
| import type { RouteItem, RouteMatch } from './types'; | ||||
| 
 | ||||
| export default function matchRouteItems( | ||||
|   routes: RouteItem[], | ||||
|   pathname: string, | ||||
| ): RouteMatch<RouteItem>[] { | ||||
|   let matches = matchRoutes(routes as unknown as RouteObject[], pathname); | ||||
|   if (!matches) return []; | ||||
| 
 | ||||
|   return matches.map(match => ({ | ||||
|     params: match.params, | ||||
|     pathname: match.pathname, | ||||
|     route: match.route as unknown as RouteItem, | ||||
|   })); | ||||
| } | ||||
|  | @ -45,6 +45,6 @@ function getAppMountNode(rootId: string): HTMLElement { | |||
| async function loadRouteChunks(matchedRoutes) { | ||||
|   for (let i = 0, n = matchedRoutes.length; i < n; i++) { | ||||
|     const { route } = matchedRoutes[i]; | ||||
|     route.component = (await route.load()).default; | ||||
|     route.component = await route.load(); | ||||
|   } | ||||
| } | ||||
|  | @ -27,7 +27,7 @@ export default async function runApp(config: AppConfig, runtimeModules, routes) | |||
|     appContext.initialData = (window as any).__ICE_APP_DATA__; | ||||
|     // context.pageInitialProps = (window as any).__ICE_PAGE_PROPS__;
 | ||||
|   } else if (appConfig?.app?.getInitialData) { | ||||
|     const { href, origin, pathname, search } = window.location; | ||||
|     const { href, origin, pathname } = window.location; | ||||
|     const path = href.replace(origin, ''); | ||||
|     // const query = queryString.parse(search);
 | ||||
|     const query = {}; | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import Runtime from './runtime.js'; | ||||
| import serverRender from './serverRender.js'; | ||||
| import type { AppContext, AppConfig } from './types'; | ||||
| import matchRoutes from './matchRoutes.js'; | ||||
| 
 | ||||
| export default async function runServerApp( | ||||
|     requestContext, | ||||
|  | @ -10,6 +11,7 @@ export default async function runServerApp( | |||
|     Document, | ||||
|     documentOnly: boolean, | ||||
|   ) { | ||||
|   // TODO: move this to defineAppConfig
 | ||||
|   const appConfig: AppConfig = { | ||||
|     ...config, | ||||
|     app: { | ||||
|  | @ -23,11 +25,17 @@ export default async function runServerApp( | |||
|     }, | ||||
|   }; | ||||
| 
 | ||||
|   const { req } = requestContext; | ||||
|   const { path } = req; | ||||
|   const matches = matchRoutes(routes, path); | ||||
|   const routeData = await getRouteData(requestContext, matches); | ||||
| 
 | ||||
|   const appContext: AppContext = { | ||||
|     matches, | ||||
|     routeData, | ||||
|     routes, | ||||
|     appConfig, | ||||
|     initialData: null, | ||||
|     document: Document, | ||||
|   }; | ||||
| 
 | ||||
|   if (appConfig?.app?.getInitialData) { | ||||
|  | @ -40,4 +48,43 @@ export default async function runServerApp( | |||
|   }); | ||||
| 
 | ||||
|   return serverRender(runtime, requestContext, Document, documentOnly); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * prepare data for matched routes | ||||
|  * @param requestContext | ||||
|  * @param matches | ||||
|  * @returns | ||||
|  */ | ||||
| async function getRouteData(requestContext, matches) { | ||||
|   const routeData = {}; | ||||
| 
 | ||||
|   const matchedCount = matches.length; | ||||
| 
 | ||||
|   for (let i = 0; i < matchedCount; i++) { | ||||
|     const match = matches[i]; | ||||
|     const { route } = match; | ||||
|     const { component, componentName } = route; | ||||
| 
 | ||||
|     const { getInitialData, getPageConfig } = component; | ||||
|     let initialData; | ||||
|     let pageConfig; | ||||
| 
 | ||||
|     if (getInitialData) { | ||||
|       initialData = await getInitialData(requestContext); | ||||
|     } | ||||
| 
 | ||||
|     if (getPageConfig) { | ||||
|       pageConfig = getPageConfig({ | ||||
|         initialData, | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     routeData[componentName] = { | ||||
|       initialData, | ||||
|       pageConfig, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   return routeData; | ||||
| } | ||||
|  | @ -12,13 +12,33 @@ export default async function serverRender( | |||
|   Document, | ||||
|   documentOnly: boolean, | ||||
| ) { | ||||
|   const documentHtml = ReactDOMServer.renderToString(<Document />); | ||||
|   const appContext = runtime.getAppContext(); | ||||
|   const { routeData, matches } = appContext; | ||||
| 
 | ||||
|   if (documentOnly) { | ||||
|     return documentHtml; | ||||
|   let html = ''; | ||||
| 
 | ||||
|   if (!documentOnly) { | ||||
|     html = renderApp(requestContext, runtime); | ||||
|   } | ||||
| 
 | ||||
|   const documentContext = { | ||||
|     matches, | ||||
|     routeData, | ||||
|     html, | ||||
|   }; | ||||
| 
 | ||||
|   const result = ReactDOMServer.renderToString( | ||||
|     <DocumentContextProvider value={documentContext}> | ||||
|       <Document /> | ||||
|     </DocumentContextProvider>, | ||||
|   ); | ||||
| 
 | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| function renderApp(requestContext, runtime) { | ||||
|   let AppRouter = runtime.getAppRouter(); | ||||
| 
 | ||||
|   if (!AppRouter) { | ||||
|     const { req } = requestContext; | ||||
|     AppRouter = (props: AppRouterProps) => ( | ||||
|  | @ -30,9 +50,7 @@ export default async function serverRender( | |||
|   } | ||||
| 
 | ||||
|   const pageHtml = ReactDOMServer.renderToString( | ||||
|     <App | ||||
|       runtime={runtime} | ||||
|     />, | ||||
|     <App runtime={runtime} />, | ||||
|   ); | ||||
| 
 | ||||
|   const html = documentHtml.replace('<!--app-html-->', pageHtml); | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import type { ComponentType, ReactNode } from 'react'; | ||||
| import type { Renderer } from 'react-dom'; | ||||
| import type { Params } from 'react-router-dom'; | ||||
| 
 | ||||
| type VoidFunction = () => void; | ||||
| type AppLifecycle = 'onShow' | 'onHide' | 'onPageNotFound' | 'onShareAppMessage' | 'onUnhandledRejection' | 'onLaunch' | 'onError' | 'onTabItemClick'; | ||||
|  | @ -10,6 +11,12 @@ type App = Partial<{ | |||
|   getInitialData?: (ctx?: any) => Promise<any>; | ||||
| } & Record<AppLifecycle, VoidFunction>>; | ||||
| 
 | ||||
| interface Page { | ||||
|   default: ComponentType<any>; | ||||
|   getPageConfig?: Function; | ||||
|   getInitialData?: (ctx?: any) => Promise<any>; | ||||
| } | ||||
| 
 | ||||
| export interface AppConfig extends Record<string, any> { | ||||
|   app?: App; | ||||
|   router?: { | ||||
|  | @ -24,7 +31,7 @@ export { | |||
| 
 | ||||
| export interface RouteItem { | ||||
|   path: string; | ||||
|   component: ComponentType; | ||||
|   component: Page; | ||||
|   componentName: string; | ||||
|   index?: false; | ||||
|   exact?: boolean; | ||||
|  | @ -32,6 +39,12 @@ export interface RouteItem { | |||
|   children?: RouteItem[]; | ||||
| } | ||||
| 
 | ||||
| export interface RouteMatch<RouteItem> { | ||||
|   params: Params; | ||||
|   pathname: string; | ||||
|   route: RouteItem; | ||||
| } | ||||
| 
 | ||||
| export interface PageConfig { | ||||
|   title?: string; | ||||
|   auth?: string[]; | ||||
|  | @ -51,13 +64,18 @@ export interface InitialContext { | |||
|   ssrError?: any; | ||||
| } | ||||
| 
 | ||||
| export interface RouteData { | ||||
|   [componentName: string]: any; | ||||
| } | ||||
| 
 | ||||
| export interface AppContext { | ||||
|   // todo: 这是啥
 | ||||
|   appManifest?: Record<string, any>; | ||||
|   routes?: RouteItem[]; | ||||
|   initialData?: any; | ||||
|   appConfig: AppConfig; | ||||
|   document?: ComponentType; | ||||
|   routeData?: RouteData; | ||||
|   matches?: RouteMatch<RouteItem>[]; | ||||
| } | ||||
| 
 | ||||
| export interface RuntimeAPI { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue