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: provider useDemoData which is given by getDemoById #1876

Merged
merged 15 commits into from
Sep 11, 2023
10 changes: 0 additions & 10 deletions examples/normal/.dumi/pages/loader-test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,2 @@
import * as demo from '../../docs/index.md?type=demo';
import * as demoIndex from '../../docs/index.md?type=demo-index';
import * as frontmatter from '../../docs/index.md?type=frontmatter';
import * as text from '../../docs/index.md?type=text';

console.log('frontmatter', frontmatter);
console.log('demo', demo);
console.log('demoIndex', demoIndex);
console.log('text', text);

// Customize Page for dumi test
export default () => 'Customize Dumi Test Page';
1 change: 1 addition & 0 deletions examples/normal/src/Foo/demo/work.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'External Demo Block';
4 changes: 4 additions & 0 deletions examples/normal/src/Foo/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
```jsx
export default () => 'Hello Foo!';
```

你好,Foo!

<code src="./demo/work.tsx">示例框</code>
12 changes: 9 additions & 3 deletions src/client/pages/Demo/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { useParams, useSiteData } from 'dumi';
import { useDemoData, useParams } from 'dumi';
import { createElement, type FC } from 'react';
import './index.less';

const DemoRenderPage: FC = () => {
const { id } = useParams();
const { demos } = useSiteData();
const { component } = demos[id!] || {};

const demoData = useDemoData(id!);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

替换成了单独加载的 useDemoData hook

if (!demoData) {
return `Demo '${id}' not found!`;
zombieJ marked this conversation as resolved.
Show resolved Hide resolved
}

const { component } = demoData;

return component && createElement(component);
};
Expand Down
82 changes: 0 additions & 82 deletions src/client/theme-api/DumiDemo.tsx

This file was deleted.

27 changes: 27 additions & 0 deletions src/client/theme-api/DumiDemo/DemoErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Container from 'dumi/theme/builtins/Container';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/client/theme-api/DumiDemo.tsx 拆成了 2 份文件。

import React, { type FC, type ReactNode } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

const DemoErrorBoundary: FC<{ children: ReactNode }> = (props) => (
<ErrorBoundary
fallbackRender={({ error }: any) => (
<Container type="error">
<p>
<strong>{error.message || 'This demo has been crashed.'}</strong>
</p>
{error.stack && (
<p>
<details open>
<summary>Error stack</summary>
<pre>{error.stack}</pre>
</details>
</p>
)}
</Container>
)}
>
{props.children}
</ErrorBoundary>
);

export default DemoErrorBoundary;
70 changes: 70 additions & 0 deletions src/client/theme-api/DumiDemo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { SP_ROUTE_PREFIX } from '@/constants';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/client/theme-api/DumiDemo.tsx 拆成了 2 份文件

import { useAppData, useDemoData, useSiteData } from 'dumi';
import React, { createElement, type FC } from 'react';
import type { IPreviewerProps } from '../types';

import Previewer from 'dumi/theme/builtins/Previewer';
import DemoErrorBoundary from './DemoErrorBoundary';

export interface IDumiDemoProps {
demo: {
id: string;
inline?: boolean;
};
previewerProps: Omit<IPreviewerProps, 'asset' | 'children'>;
}

const InternalDumiDemo = (props: IDumiDemoProps) => {
const { historyType } = useSiteData();
const { basename } = useAppData();
const demoInfo = useDemoData(props.demo.id);
zombieJ marked this conversation as resolved.
Show resolved Hide resolved

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

if (!demoInfo) {
return `Demo '${props.demo.id}' not found!`;
}

const { component, asset } = demoInfo;

const demoNode = (
<DemoErrorBoundary>{createElement(component)}</DemoErrorBoundary>
);

if (props.demo.inline) {
return demoNode;
}

const isHashRoute = historyType === 'hash';

return (
<Previewer
asset={asset}
demoUrl={
// allow user override demoUrl by frontmatter
props.previewerProps.demoUrl ||
// when use hash route, browser can automatically handle relative paths starting with #
`${isHashRoute ? `#` : ''}${basename}${SP_ROUTE_PREFIX}demos/${
props.demo.id
}`
}
{...props.previewerProps}
>
{props.previewerProps.iframe ? null : demoNode}
</Previewer>
);
};

export const DumiDemo: FC<IDumiDemoProps> = React.memo(
InternalDumiDemo,
(prev, next) => {
// compare length for performance
return JSON.stringify(prev).length === JSON.stringify(next).length;
},
);

if (process.env.NODE_ENV !== 'production') {
InternalDumiDemo.displayName = 'DumiDemo';
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { PICKED_PKG_FIELDS } from '@/constants';
import type { AtomComponentAsset } from 'dumi-assets-types';
import { createContext, useContext, type ComponentType } from 'react';
import type { ILocalesConfig, IPreviewerProps, IThemeConfig } from './types';
import type { ILocalesConfig, IPreviewerProps, IThemeConfig } from '../types';
import use from './use';

export type DemoInfo = {
component: ComponentType;
asset: IPreviewerProps['asset'];
routeId: string;
};

interface ISiteContext {
pkg: Partial<Record<keyof typeof PICKED_PKG_FIELDS, any>>;
historyType: 'browser' | 'hash' | 'memory';
entryExports: Record<string, any>;
demos: Record<
string,
{
component: ComponentType;
asset: IPreviewerProps['asset'];
routeId: string;
}
>;
demos: Record<string, DemoInfo>;
components: Record<string, AtomComponentAsset>;
locales: ILocalesConfig;
themeConfig: IThemeConfig;
Expand All @@ -25,6 +25,11 @@ interface ISiteContext {
* private field, do not use it in your code
*/
_2_level_nav_available: boolean;

// =================== Demo Map ===================
// Demo map is root providing the id => route mapping info.
// Which is generated by `meta-demos` template
getDemoById: (id: string) => Promise<DemoInfo | null>;
Copy link
Contributor Author

@zombieJ zombieJ Sep 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

提供新的 getDemoById 方法从独立的 chunk 里加载数据

}

export const SiteContext = createContext<ISiteContext>({
Expand All @@ -38,8 +43,22 @@ export const SiteContext = createContext<ISiteContext>({
loading: false,
setLoading: () => {},
_2_level_nav_available: true,
getDemoById: async () => null,
});

export const useSiteData = () => {
return useContext(SiteContext);
};

const cache = new Map<string, Promise<DemoInfo | null>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache 是不是做在 getDemoById 里更合适

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是 React 的 Suspense data 一套的,放 getDemoById 里就和 React 实现耦合了。


// Async load demo data
export function useDemoData(demoId: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有 hook 的话要么 getDemoById 就作为内部方法不通过 dumi 和 context 暴露了?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getDemoById 必须要通过 context 传递才能供 hooks 消费。不暴露的话指的是额外搞个 context 传这个? 

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我的想法是 useDemoData 和 getDemoById 的实现都写在这里:

path: 'dumi/meta/index.ts',

然后在这里相对路径 re-export useDemoData 实现导出:

content: `export * from '../exports';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get 了,那我建议这个晚点搞。
React 的 Suspense 异步数据现在 React 还没有提供一个很好的封装。往上挪的话,template 里会有大量的混合代码。这其实没有特别大的必要。而且我们本身也没有在 exports 里透露出来,只是通过 context 传递而已。

const { getDemoById } = useSiteData();

if (!cache.has(demoId)) {
cache.set(demoId, getDemoById(demoId));
}

return use(cache.get(demoId)!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 use 的作用是什么呢

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

见上,React Suspense 的实现。官方现在没有提供封装,所以直接抄的文档代码

}
30 changes: 30 additions & 0 deletions src/client/theme-api/context/use.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copy from React official demo.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react.dev 中,抄的 use hook

// This will be replace if React release new version of use hooks
type ReactPromise<T> = Promise<T> & {
status?: 'pending' | 'fulfilled' | 'rejected';
value?: T;
reason?: any;
};

export default function use<T>(promise: ReactPromise<T>): T {
if (promise.status === 'fulfilled') {
return promise.value!;
} else if (promise.status === 'rejected') {
throw promise.reason;
} else if (promise.status === 'pending') {
throw promise;
} else {
promise.status = 'pending';
promise.then(
(result) => {
promise.status = 'fulfilled';
promise.value = result;
},
(reason) => {
promise.status = 'rejected';
promise.reason = reason;
},
);
throw promise;
}
}
8 changes: 4 additions & 4 deletions src/client/theme-api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export {
createIntlCache,
defineMessages,
FormattedDate,
FormattedDateParts,
FormattedDisplayName,
Expand All @@ -12,17 +10,19 @@ export {
FormattedRelativeTime,
FormattedTime,
FormattedTimeParts,
injectIntl,
IntlContext,
IntlProvider,
RawIntlProvider,
createIntlCache,
defineMessages,
injectIntl,
useIntl,
} from 'react-intl';
export { AtomRenderer } from './AtomRenderer';
export { useSiteData } from './context';
export { DumiDemo } from './DumiDemo';
export { DumiDemoGrid } from './DumiDemoGrid';
export { DumiPage } from './DumiPage';
export { useDemoData, useSiteData } from './context';
export { openCodeSandbox } from './openCodeSandbox';
export { openStackBlitz } from './openStackBlitz';
export type { IPreviewerProps } from './types';
Expand Down
12 changes: 12 additions & 0 deletions src/features/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ export default (api: IApi) => {
},
});

// generate meta lazy entry
api.writeTmpFile({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新的 meta/demo 入口,使用 demo-index chunk 异步加载数据

noPluginDir: true,
path: 'dumi/meta/demos.ts',
tplPath: winPath(join(TEMPLATES_DIR, 'meta-demos.ts.tpl')),
context: {
metaFiles: parsedMetaFiles.filter((metaFile) =>
metaFile.file.endsWith('.md'),
),
},
});

// generate runtime plugin, to append page meta to route object
api.writeTmpFile({
noPluginDir: true,
Expand Down
2 changes: 2 additions & 0 deletions src/templates/ContextWrapper.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { useOutlet, history } from 'dumi';
import { SiteContext } from '{{{contextPath}}}';
import { demos, components } from '../meta';
import { getDemoById } from '../meta/demos';
import { locales } from '../locales/config';
{{{defaultExport}}}
{{{namedExport}}}
Expand Down Expand Up @@ -43,6 +44,7 @@ export default function DumiContextWrapper() {
hostname: {{{hostname}}},
themeConfig: {{{themeConfig}}},
_2_level_nav_available: {{{_2_level_nav_available}}},
getDemoById,
};

return (
Expand Down
Loading
Loading