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 = {
|
const appConfig: AppConfig = {
|
||||||
app: {
|
app: {
|
||||||
getInitialData: async (ctx) => {
|
getInitialData: async (ctx) => {
|
||||||
console.log(ctx);
|
|
||||||
return {
|
return {
|
||||||
auth: {
|
auth: {
|
||||||
admin: true,
|
admin: true,
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,17 @@ import './index.css';
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const appContext = useAppContext();
|
const appContext = useAppContext();
|
||||||
|
|
||||||
console.log('Home Page: appContext', appContext);
|
|
||||||
|
|
||||||
return <><h2>Home Page</h2><Link to="/about">about</Link></>;
|
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 = {
|
Home.pageConfig = {
|
||||||
auth: ['admin'],
|
auth: ['admin'],
|
||||||
};
|
};
|
||||||
|
|
@ -18,7 +18,7 @@ function generateComponentsImportStr(routeManifest: RouteManifest) {
|
||||||
let { file, componentName } = routeManifest[id];
|
let { file, componentName } = routeManifest[id];
|
||||||
const fileExtname = path.extname(file);
|
const fileExtname = path.extname(file);
|
||||||
file = file.replace(new RegExp(`${fileExtname}$`), '');
|
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;
|
let element;
|
||||||
if (routes.length === 1 && !routes[0].children) {
|
if (routes.length === 1 && !routes[0].children) {
|
||||||
const Page = routes[0].component;
|
const Page = routes[0].component.default;
|
||||||
element = <Page />;
|
element = <Page />;
|
||||||
} else {
|
} else {
|
||||||
element = <AppRouter PageWrappers={PageWrappers} />;
|
element = <AppRouter PageWrappers={PageWrappers} />;
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,12 @@ function Routes({ routes }: RoutesProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRouteElement(routes: RouteItem[], PageWrappers?: PageWrapper<any>[]) {
|
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;
|
let element;
|
||||||
|
|
||||||
if (PageComponent) {
|
if (pageComponent) {
|
||||||
element = (
|
element = (
|
||||||
<RouteWrapper PageComponent={PageComponent} PageWrappers={PageWrappers} />
|
<RouteWrapper PageComponent={pageComponent.default} PageWrappers={PageWrappers} />
|
||||||
);
|
);
|
||||||
} else if (load) {
|
} else if (load) {
|
||||||
const LazyComponent = React.lazy(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) {
|
async function loadRouteChunks(matchedRoutes) {
|
||||||
for (let i = 0, n = matchedRoutes.length; i < n; i++) {
|
for (let i = 0, n = matchedRoutes.length; i < n; i++) {
|
||||||
const { route } = matchedRoutes[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__;
|
appContext.initialData = (window as any).__ICE_APP_DATA__;
|
||||||
// context.pageInitialProps = (window as any).__ICE_PAGE_PROPS__;
|
// context.pageInitialProps = (window as any).__ICE_PAGE_PROPS__;
|
||||||
} else if (appConfig?.app?.getInitialData) {
|
} else if (appConfig?.app?.getInitialData) {
|
||||||
const { href, origin, pathname, search } = window.location;
|
const { href, origin, pathname } = window.location;
|
||||||
const path = href.replace(origin, '');
|
const path = href.replace(origin, '');
|
||||||
// const query = queryString.parse(search);
|
// const query = queryString.parse(search);
|
||||||
const query = {};
|
const query = {};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import Runtime from './runtime.js';
|
import Runtime from './runtime.js';
|
||||||
import serverRender from './serverRender.js';
|
import serverRender from './serverRender.js';
|
||||||
import type { AppContext, AppConfig } from './types';
|
import type { AppContext, AppConfig } from './types';
|
||||||
|
import matchRoutes from './matchRoutes.js';
|
||||||
|
|
||||||
export default async function runServerApp(
|
export default async function runServerApp(
|
||||||
requestContext,
|
requestContext,
|
||||||
|
|
@ -10,6 +11,7 @@ export default async function runServerApp(
|
||||||
Document,
|
Document,
|
||||||
documentOnly: boolean,
|
documentOnly: boolean,
|
||||||
) {
|
) {
|
||||||
|
// TODO: move this to defineAppConfig
|
||||||
const appConfig: AppConfig = {
|
const appConfig: AppConfig = {
|
||||||
...config,
|
...config,
|
||||||
app: {
|
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 = {
|
const appContext: AppContext = {
|
||||||
|
matches,
|
||||||
|
routeData,
|
||||||
routes,
|
routes,
|
||||||
appConfig,
|
appConfig,
|
||||||
initialData: null,
|
initialData: null,
|
||||||
document: Document,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (appConfig?.app?.getInitialData) {
|
if (appConfig?.app?.getInitialData) {
|
||||||
|
|
@ -40,4 +48,43 @@ export default async function runServerApp(
|
||||||
});
|
});
|
||||||
|
|
||||||
return serverRender(runtime, requestContext, Document, documentOnly);
|
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,
|
Document,
|
||||||
documentOnly: boolean,
|
documentOnly: boolean,
|
||||||
) {
|
) {
|
||||||
const documentHtml = ReactDOMServer.renderToString(<Document />);
|
const appContext = runtime.getAppContext();
|
||||||
|
const { routeData, matches } = appContext;
|
||||||
|
|
||||||
if (documentOnly) {
|
let html = '';
|
||||||
return documentHtml;
|
|
||||||
|
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();
|
let AppRouter = runtime.getAppRouter();
|
||||||
|
|
||||||
if (!AppRouter) {
|
if (!AppRouter) {
|
||||||
const { req } = requestContext;
|
const { req } = requestContext;
|
||||||
AppRouter = (props: AppRouterProps) => (
|
AppRouter = (props: AppRouterProps) => (
|
||||||
|
|
@ -30,9 +50,7 @@ export default async function serverRender(
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageHtml = ReactDOMServer.renderToString(
|
const pageHtml = ReactDOMServer.renderToString(
|
||||||
<App
|
<App runtime={runtime} />,
|
||||||
runtime={runtime}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const html = documentHtml.replace('<!--app-html-->', pageHtml);
|
const html = documentHtml.replace('<!--app-html-->', pageHtml);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { ComponentType, ReactNode } from 'react';
|
import type { ComponentType, ReactNode } from 'react';
|
||||||
import type { Renderer } from 'react-dom';
|
import type { Renderer } from 'react-dom';
|
||||||
|
import type { Params } from 'react-router-dom';
|
||||||
|
|
||||||
type VoidFunction = () => void;
|
type VoidFunction = () => void;
|
||||||
type AppLifecycle = 'onShow' | 'onHide' | 'onPageNotFound' | 'onShareAppMessage' | 'onUnhandledRejection' | 'onLaunch' | 'onError' | 'onTabItemClick';
|
type AppLifecycle = 'onShow' | 'onHide' | 'onPageNotFound' | 'onShareAppMessage' | 'onUnhandledRejection' | 'onLaunch' | 'onError' | 'onTabItemClick';
|
||||||
|
|
@ -10,6 +11,12 @@ type App = Partial<{
|
||||||
getInitialData?: (ctx?: any) => Promise<any>;
|
getInitialData?: (ctx?: any) => Promise<any>;
|
||||||
} & Record<AppLifecycle, VoidFunction>>;
|
} & Record<AppLifecycle, VoidFunction>>;
|
||||||
|
|
||||||
|
interface Page {
|
||||||
|
default: ComponentType<any>;
|
||||||
|
getPageConfig?: Function;
|
||||||
|
getInitialData?: (ctx?: any) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppConfig extends Record<string, any> {
|
export interface AppConfig extends Record<string, any> {
|
||||||
app?: App;
|
app?: App;
|
||||||
router?: {
|
router?: {
|
||||||
|
|
@ -24,7 +31,7 @@ export {
|
||||||
|
|
||||||
export interface RouteItem {
|
export interface RouteItem {
|
||||||
path: string;
|
path: string;
|
||||||
component: ComponentType;
|
component: Page;
|
||||||
componentName: string;
|
componentName: string;
|
||||||
index?: false;
|
index?: false;
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
|
|
@ -32,6 +39,12 @@ export interface RouteItem {
|
||||||
children?: RouteItem[];
|
children?: RouteItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RouteMatch<RouteItem> {
|
||||||
|
params: Params;
|
||||||
|
pathname: string;
|
||||||
|
route: RouteItem;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PageConfig {
|
export interface PageConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
auth?: string[];
|
auth?: string[];
|
||||||
|
|
@ -51,13 +64,18 @@ export interface InitialContext {
|
||||||
ssrError?: any;
|
ssrError?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RouteData {
|
||||||
|
[componentName: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppContext {
|
export interface AppContext {
|
||||||
// todo: 这是啥
|
// todo: 这是啥
|
||||||
appManifest?: Record<string, any>;
|
appManifest?: Record<string, any>;
|
||||||
routes?: RouteItem[];
|
routes?: RouteItem[];
|
||||||
initialData?: any;
|
initialData?: any;
|
||||||
appConfig: AppConfig;
|
appConfig: AppConfig;
|
||||||
document?: ComponentType;
|
routeData?: RouteData;
|
||||||
|
matches?: RouteMatch<RouteItem>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeAPI {
|
export interface RuntimeAPI {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue