Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: optimize async route meta solution #1974

Merged
merged 4 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading