Skip to content

Commit

Permalink
refactor: optimize async route meta solution (#1974)
Browse files Browse the repository at this point in the history
* refactor: optimize async route meta solution

* refactor: simplify markdown loader rules

* refactor: correct async tab meta load logic

* refactor: page suspense add fallback
  • Loading branch information
PeachScript committed Dec 14, 2023
1 parent b962cf9 commit 5ffeaa4
Show file tree
Hide file tree
Showing 33 changed files with 486 additions and 553 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
module.exports = {
extends: require.resolve('@umijs/lint/dist/config/eslint'),
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ ignoreRestSiblings: true },
],
},
};
5 changes: 1 addition & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from '@@/dumi/exports';
export { Root as HastRoot } from 'hast';
export * from 'umi';
export {
Expand All @@ -7,8 +8,4 @@ export {
export * from './dist';
// override umi exported defineConfig
export { defineConfig } from './dist';
export * from './dist/client/theme-api';
export { IApi } from './dist/types';
export function getRouteMetaById(id: string): Promise<any>;
/** @private Internal usage. Safe to remove */
export function loadFilesMeta(): Promise<any>;
7 changes: 2 additions & 5 deletions src/client/pages/Demo/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { useDemoData, useParams } from 'dumi';
import { useDemo, useParams } from 'dumi';
import { createElement, type FC } from 'react';
import './index.less';

const DemoRenderPage: FC = () => {
const { id } = useParams();

const demoInfo = useDemoData(id!);

const { component } = demoInfo!;
const { component } = useDemo(id!) || {};

return component && createElement(component);
};
Expand Down
6 changes: 2 additions & 4 deletions src/client/theme-api/DumiDemo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SP_ROUTE_PREFIX } from '@/constants';
import { useAppData, useDemoData, useSiteData } from 'dumi';
import { useAppData, useDemo, useSiteData } from 'dumi';
import React, { createElement, type FC } from 'react';
import type { IPreviewerProps } from '../types';

Expand All @@ -17,14 +17,12 @@ export interface IDumiDemoProps {
const InternalDumiDemo = (props: IDumiDemoProps) => {
const { historyType } = useSiteData();
const { basename } = useAppData();
const demoInfo = useDemoData(props.demo.id)!;
const { component, asset } = useDemo(props.demo.id)!;

// hide debug demo in production
if (process.env.NODE_ENV === 'production' && props.previewerProps.debug)
return null;

const { component, asset } = demoInfo;

const demoNode = (
<DemoErrorBoundary>{createElement(component)}</DemoErrorBoundary>
);
Expand Down
38 changes: 38 additions & 0 deletions src/client/theme-api/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { PICKED_PKG_FIELDS } from '@/constants';
import type { AtomComponentAsset } from 'dumi-assets-types';
import { createContext, useContext } from 'react';
import type { IDemoData, ILocalesConfig, IThemeConfig } from './types';

interface ISiteContext {
pkg: Partial<Record<keyof typeof PICKED_PKG_FIELDS, any>>;
historyType: 'browser' | 'hash' | 'memory';
entryExports: Record<string, any>;
demos: Record<string, IDemoData>;
components: Record<string, AtomComponentAsset>;
locales: ILocalesConfig;
themeConfig: IThemeConfig;
hostname?: string;
loading: boolean;
setLoading: (status: boolean) => void;
/**
* private field, do not use it in your code
*/
_2_level_nav_available: boolean;
}

export const SiteContext = createContext<ISiteContext>({
pkg: {},
historyType: 'browser',
entryExports: {},
demos: {},
components: {},
locales: [],
themeConfig: {} as IThemeConfig,
loading: false,
setLoading: () => {},
_2_level_nav_available: true,
});

export const useSiteData = () => {
return useContext(SiteContext);
};
71 changes: 0 additions & 71 deletions src/client/theme-api/context/index.ts

This file was deleted.

33 changes: 0 additions & 33 deletions src/client/theme-api/context/use.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/client/theme-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export { AtomRenderer } from './AtomRenderer';
export { DumiDemo } from './DumiDemo';
export { DumiDemoGrid } from './DumiDemoGrid';
export { DumiPage } from './DumiPage';
export { useDemoData, useSiteData } from './context';
export { useSiteData } from './context';
export { evalCode } from './evalCode';
export { LiveContext, LiveProvider, isLiveEnabled } from './live';
export { openCodeSandbox } from './openCodeSandbox';
Expand Down
2 changes: 1 addition & 1 deletion src/client/theme-api/live/useDemoScopes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getDemoScopesById } from 'dumi';
import use from '../context/use';
import { use } from '../utils';

const cache = new Map<string, any>();

Expand Down
6 changes: 6 additions & 0 deletions src/client/theme-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,9 @@ export type IRoutesById = Record<
[key: string]: any;
}
>;

export type IDemoData = {
component: ComponentType;
asset: IPreviewerProps['asset'];
routeId: string;
};
100 changes: 73 additions & 27 deletions src/client/theme-api/useRouteMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,71 @@ import {
useLocation,
useRouteData,
} from 'dumi';
import { useMemo } from 'react';
import use from './context/use';
import type { IRouteMeta } from './types';
import { useCallback, useState } from 'react';
import type { IRouteMeta, IRoutesById } from './types';
import { use, useIsomorphicLayoutEffect } from './utils';

const cache = new Map<string, any>();

const useAsyncRouteMeta = (id: string) => {
if (!cache.has(id)) {
cache.set(id, getRouteMetaById(id));
}

return use<any>(cache.get(id)!);
};

const emptyMeta = {
const cache = new Map<string, Promise<IRouteMeta | undefined>>();
const EMPTY_META = {
frontmatter: {},
toc: [],
texts: [],
};
} as any;
const ASYNC_META_PROPS = ['texts'];

function getCachedRouteMeta(route: IRoutesById[string]) {
const cacheKey = route.id;
const pendingCacheKey = `${cacheKey}:pending`;

if (!cache.get(cacheKey)) {
const merge = (meta: IRouteMeta = EMPTY_META) => {
if (route.meta) {
Object.keys(route.meta).forEach((key) => {
(meta as any)[key] ??= (route.meta as any)[key];
});
}

return meta;
};
const meta = merge(getRouteMetaById(route.id, { syncOnly: true }));
const ret: Parameters<typeof use<IRouteMeta>>[0] = Promise.resolve(meta);
const proxyGetter = (target: any, prop: string) => {
if (ASYNC_META_PROPS.includes(prop)) {
if (!cache.get(pendingCacheKey)) {
// load async meta then replace cache
cache.set(
pendingCacheKey,
getRouteMetaById(route.id).then((full) =>
cache.set(cacheKey, Promise.resolve(merge(full))).get(cacheKey),
),
);
}

// throw promise to trigger suspense
throw cache.get(pendingCacheKey);
}

return target[prop];
};

// return sync meta by default
ret.status = 'fulfilled';

// load async meta if property accessed
meta.tabs?.forEach((tab) => {
tab.meta = new Proxy(tab.meta, {
get: proxyGetter,
});
});
ret.value = new Proxy(meta, {
get: proxyGetter,
});

cache.set(cacheKey, ret);
}

return cache.get(cacheKey)!;
}

/**
* hook for get matched route meta
Expand All @@ -32,26 +78,26 @@ export const useRouteMeta = () => {
const { route } = useRouteData();
const { pathname } = useLocation();
const { clientRoutes } = useAppData();
const getter = useCallback(() => {
let ret: IRoutesById[string];

const curRoute = useMemo(() => {
if (route.path === pathname && !('isLayout' in route)) {
// use `useRouteData` result if matched, for performance
return route;
ret = route as any;
} else {
// match manually for dynamic route & layout component
const matched = matchRoutes(clientRoutes, pathname)?.pop();
return matched?.route;
ret = matched?.route as any;
}
}, [clientRoutes.length, pathname]);

const meta: IRouteMeta =
useAsyncRouteMeta((curRoute as any)?.id) || emptyMeta;
return ret;
}, [clientRoutes.length, pathname]);
const [matchedRoute, setMatchedRoute] = useState(getter);
const meta = use(getCachedRouteMeta(matchedRoute));

if (curRoute && 'meta' in curRoute && typeof curRoute.meta === 'object') {
Object.keys(curRoute.meta as IRouteMeta).forEach((key) => {
(meta as any)[key] ??= (curRoute as any).meta[key];
});
}
useIsomorphicLayoutEffect(() => {
setMatchedRoute(getter);
}, [clientRoutes.length, pathname]);

return meta;
return meta!;
};
Loading

0 comments on commit 5ffeaa4

Please sign in to comment.