feat: route data

This commit is contained in:
shuilan.cj 2022-03-17 15:21:32 +08:00 committed by ClarkXia
parent ecd64cd762
commit d4cbe9182b
13 changed files with 251 additions and 19 deletions

View File

@ -3,7 +3,6 @@ import type { AppConfig } from 'ice';
const appConfig: AppConfig = {
app: {
getInitialData: async (ctx) => {
console.log(ctx);
return {
auth: {
admin: true,

View File

@ -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'],
};

View File

@ -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`;
}, '');
}

View File

@ -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} />;

View File

@ -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);

View File

@ -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>;
}

View File

@ -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,
};

View File

@ -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,
}));
}

View File

@ -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();
}
}

View File

@ -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 = {};

View File

@ -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;
}

View File

@ -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);

View File

@ -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 {