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

feat: optimize the error prompts and editing experience of vue demo #2065

Merged
merged 3 commits into from
Apr 25, 2024
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
16 changes: 11 additions & 5 deletions src/client/pages/Demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@ import { useRenderer } from '../../theme-api/useRenderer';
import './index.less';

const DemoRenderPage: FC = () => {
const { id } = useParams();
const demo = useDemo(id!);
const params = useParams();
const id = params.id!;

const canvasRef = useRenderer(demo!);
const demo = useDemo(id)!;
const { canvasRef } = useRenderer({
id,
component: demo.component,
renderOpts: demo.renderOpts,
});

const { component, renderOpts } = demo || {};

const {
node: liveDemoNode,
setSource,
error: liveDemoError,
loading,
} = useLiveDemo(id!);

const finalNode =
Expand Down Expand Up @@ -43,13 +49,13 @@ const DemoRenderPage: FC = () => {

// notify parent window that compile done
useEffect(() => {
if (liveDemoNode || liveDemoError) {
if (!loading && (liveDemoError || liveDemoNode)) {
window.postMessage({
type: 'dumi.liveDemo.compileDone',
value: { err: liveDemoError },
});
}
}, [liveDemoNode, liveDemoError]);
}, [liveDemoError, liveDemoNode, loading]);

return finalNode;
};
Expand Down
6 changes: 5 additions & 1 deletion src/client/theme-api/DumiDemo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ const InternalDumiDemo = (props: IDumiDemoProps) => {
const demo = useDemo(id)!;
const { component, asset, renderOpts } = demo;

const canvasRef = useRenderer(Object.assign(demo, { id }));
const { canvasRef } = useRenderer({
id,
renderOpts: demo.renderOpts,
component: demo.component,
});

// hide debug demo in production
if (process.env.NODE_ENV === 'production' && props.previewerProps.debug)
Expand Down
12 changes: 12 additions & 0 deletions src/client/theme-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ export type IDemoCancelableFn = (
component: AgnosticComponentModule,
) => (() => void) | Promise<() => void>;

export type IDemoPreflightFn = (
component: AgnosticComponentModule,
) => Promise<void>;

export type IDemoData = {
component: ReactComponentType | AgnosticComponentType;
asset: IPreviewerProps['asset'];
Expand All @@ -267,6 +271,14 @@ export type IDemoData = {
* provide a runtime compile function for compile demo code for live preview
*/
compile?: IDemoCompileFn;
/**
* Component rendering function, used to manage the creation and unmount of components
*/
renderer?: IDemoCancelableFn;
/**
* Used to detect initialization errors of components in advance
* (if there is an error, the component will not be mounted)
*/
preflight?: IDemoPreflightFn;
};
};
41 changes: 21 additions & 20 deletions src/client/theme-api/useLiveDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import DemoErrorBoundary from './DumiDemo/DemoErrorBoundary';
import type { AgnosticComponentType } from './types';
import { useRenderer } from './useRenderer';
import { getAgnosticComponentModule } from './utils';

const THROTTLE_WAIT = 500;

Expand Down Expand Up @@ -39,21 +40,23 @@ export const useLiveDemo = (
const loadingTimer = useRef<number>();
const taskToken = useRef<number>();

function resetLoadingStatus() {
clearTimeout(loadingTimer.current);
setLoading(false);
}

const { context = {}, asset, renderOpts } = demo;
const [component, setComponent] = useState<AgnosticComponentType>();
const ref = useRenderer(
component
? {
id,
...demo,
component,
}
: Object.assign(demo, { id }),
);
const [error, setError] = useState<Error | null>(null);

const [demoNode, setDemoNode] = useState<ReactNode>();
const { canvasRef: ref, setComponent } = useRenderer({
id,
renderOpts: demo.renderOpts,
onResolved: () => {
resetLoadingStatus();
},
});

const [error, setError] = useState<Error | null>(null);
const [demoNode, setDemoNode] = useState<ReactNode>();
const setSource = useCallback(
throttle(
async (source: Record<string, string>) => {
Expand All @@ -66,11 +69,6 @@ export const useLiveDemo = (
THROTTLE_WAIT - 1,
);

function resetLoadingStatus() {
clearTimeout(loadingTimer.current);
setLoading(false);
}

if (opts?.iframe && opts?.containerRef?.current) {
const iframeWindow =
opts.containerRef.current.querySelector('iframe')!.contentWindow!;
Expand All @@ -88,7 +86,6 @@ export const useLiveDemo = (
resolve();
}
};

iframeWindow.addEventListener('message', handler);
iframeWindow.postMessage({
type: 'dumi.liveDemo.setSource',
Expand Down Expand Up @@ -128,13 +125,17 @@ export const useLiveDemo = (
module,
require,
});
setComponent(exports);
const component = await getAgnosticComponentModule(exports);
Copy link
Member

Choose a reason for hiding this comment

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

我们只支持 export default 就够了吧,适配 cjs 导出会增加不少复杂度,react 技术栈目前也是直接读 exports.default

Copy link
Collaborator Author

@jeffwcx jeffwcx Apr 25, 2024

Choose a reason for hiding this comment

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

这里其实无所谓, getAgnosticComponentModule 主要是 await 一下,默认还是获取module.default

if (renderOpts.preflight) {
await renderOpts.preflight(component);
}
setComponent(component);
setDemoNode(createElement('div', { ref }));
setError(null);
} catch (err: any) {
setError(err);
resetLoadingStatus();
}
resetLoadingStatus();
return;
}

Expand Down
96 changes: 68 additions & 28 deletions src/client/theme-api/useRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,92 @@
import { useEffect, useRef } from 'react';
import type { AgnosticComponentModule, IDemoData } from './types';
import { useEffect, useRef, useState } from 'react';
import type { AgnosticComponentType, IDemoData } from './types';
import { getAgnosticComponentModule } from './utils';

// maintain all the mounted instance
const map = new Map<string, any>();
const map = new Map<
string,
{ teardown?: () => void; hostElement?: HTMLElement }
>();

export interface UseRendererOptions {
id: string;
component?: AgnosticComponentType;
renderOpts: IDemoData['renderOpts'];
onResolved?: () => void;
}

export const useRenderer = ({
id,
component,
renderOpts,
}: IDemoData & { id: string }) => {
onResolved,
}: UseRendererOptions) => {
const [deferedComponent, setComponent] =
useState<AgnosticComponentType | null>(
component ? getAgnosticComponentModule(component) : null,
);

const canvasRef = useRef<HTMLDivElement>(null);

const teardownRef = useRef(() => {});

const prevComponent = useRef(component);
const prevCanvas = useRef(canvasRef.current);

// forcibly destroyed
if (prevComponent.current !== component) {
const teardown = map.get(id);
teardown?.();
prevComponent.current = component;
const resolving = useRef(false);

if (prevCanvas.current !== canvasRef.current) {
if (prevCanvas.current === null) {
// When first render, component maintained by the parent component userRenderer may be removed.
// The hosted element should be added back
const handler = map.get(id);
if (handler?.teardown && handler?.hostElement && canvasRef.current) {
canvasRef.current.appendChild(handler.hostElement);
teardownRef.current = handler.teardown;
}
}
prevCanvas.current = canvasRef.current;
}

const renderer = renderOpts?.renderer;

useEffect(() => {
async function resolveRender() {
if (!canvasRef.current || !renderer || !component) return;
if (map.get(id)) return;

map.set(id, () => {});
let module: AgnosticComponentModule =
component instanceof Promise ? await component : component;
module = module.default ?? module;

const teardown = await renderer(canvasRef.current, module);

// remove instance when react component is unmounted
teardownRef.current = function () {
teardown();
map.delete(id);
};
map.set(id, teardownRef.current);
if (!canvasRef.current || !renderer || !deferedComponent) return;
const legacyHandler = map.get(id);
if (resolving.current) return;
resolving.current = true;

const legacyTeardown = legacyHandler?.teardown;
const comp = await deferedComponent;
const hostElement = document.createElement('div');
try {
canvasRef.current.appendChild(hostElement);
const teardown = await renderer(hostElement, comp);
legacyTeardown?.();
legacyHandler?.hostElement?.remove();
// remove instance when react component is unmounted
teardownRef.current = function () {
teardown();
hostElement.remove();
map.delete(id);
};
map.set(id, {
teardown: teardownRef.current,
hostElement,
});
} catch (error) {
hostElement.remove();
throw error;
} finally {
resolving.current = false;
onResolved?.();
}
}

resolveRender();
}, [canvasRef.current, component, renderer]);
}, [canvasRef.current, deferedComponent, renderer]);

useEffect(() => () => teardownRef.current(), []);

return canvasRef;
return { canvasRef, setComponent };
};
10 changes: 10 additions & 0 deletions src/client/theme-api/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PluginManager, useAppData, useIntl, useSiteData } from 'dumi';
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
import type {
AgnosticComponentModule,
IDemoData,
ILocale,
INav,
INavItem,
Expand Down Expand Up @@ -146,3 +148,11 @@ export const pickRouteSortMeta = (
export function getLocaleNav(nav: IUserNavValue | INav, locale: ILocale) {
return Array.isArray(nav) ? nav : nav[locale.id];
}

export async function getAgnosticComponentModule(
component: IDemoData['component'],
): Promise<AgnosticComponentModule> {
const mod: AgnosticComponentModule =
component instanceof Promise ? await component : component;
return mod.default ?? mod;
}
7 changes: 7 additions & 0 deletions src/loaders/markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@ export const demos = {
)}')).default,`);
}

if (renderOpts.preflightPath) {
propertyArray.push(`
preflight: (await import('${winPath(
renderOpts.preflightPath,
)}')).default,`);
}

if (propertyArray.length === 0) return 'undefined';

return `{
Expand Down
2 changes: 2 additions & 0 deletions src/loaders/markdown/transformer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ declare module 'vfile' {
renderOpts: {
type?: string;
rendererPath?: string;
preflightPath?: string;
compilePath?: string;
};
}
Expand All @@ -55,6 +56,7 @@ declare module 'vfile' {
renderOpts: {
type?: string;
rendererPath?: string;
preflightPath?: string;
compilePath?: string; // only for fix type
};
}
Expand Down
2 changes: 2 additions & 0 deletions src/loaders/markdown/transformer/rehypeDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ export default function rehypeDemo(
component,
renderOpts: {
rendererPath: runtimeOpts?.rendererPath,
preflightPath: runtimeOpts?.preflightPath,
},
};
}
Expand Down Expand Up @@ -450,6 +451,7 @@ export default function rehypeDemo(
renderOpts: {
rendererPath: runtimeOpts?.rendererPath,
compilePath: runtimeOpts?.compilePath,
preflightPath: runtimeOpts?.preflightPath,
},
};
},
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export type IDumiTechStackOnBlockLoadArgs = OnLoadArgs & {
};

export interface IDumiTechStackRuntimeOpts {
/**
* Component detection function path,
* used to detect errors that occur from application creation to component mounting.
*/
preflightPath?: string;
/**
* path of the cancelable{@link IDemoCancelableFn} function
* that manipulate(mount/unmount) third-party framework component
Expand Down
55 changes: 27 additions & 28 deletions suites/preset-vue/lib/compiler.mjs

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions suites/preset-vue/lib/preflight.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createApp, h, nextTick } from 'vue';

function u(d){return new Promise((i,t)=>{let n=document.createElement("div");n.style.display="none",n.style.overflow="hidden",document.body.appendChild(n);let e;function r(){nextTick(()=>{e.config.errorHandler=void 0,e.unmount(),n.remove();});}e=createApp({mounted(){i(),r();},render(){return h(d)}}),e.config.warnHandler=o=>{r(),t(new Error(o));},e.config.errorHandler=o=>{r(),t(o);},e.mount(n);})}

export { u as default };
4 changes: 2 additions & 2 deletions suites/preset-vue/lib/renderer.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createApp } from 'vue';

var _=function(s,e){e.__css__&&setTimeout(()=>{document.querySelectorAll(`style[css-${e.__id__}]`).forEach(t=>t.remove()),document.head.insertAdjacentHTML("beforeend",`<style css-${e.__id__}>${e.__css__}</style>`);},1);let r=createApp(e);return r.config.errorHandler=t=>console.error(t),r.mount(s),()=>{r.unmount();}},l=_;
var _=async function(s,e){e.__css__&&setTimeout(()=>{document.querySelectorAll(`style[css-${e.__id__}]`).forEach(r=>r.remove()),document.head.insertAdjacentHTML("beforeend",`<style css-${e.__id__}>${e.__css__}</style>`);},1);let t=createApp(e);return t.config.errorHandler=function(r){throw r},t.mount(s),()=>{t.unmount();}},o=_;

export { l as default };
export { o as default };
2 changes: 1 addition & 1 deletion suites/preset-vue/src/compiler/browser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as Babel from '@babel/standalone';
import type * as Babel from '@babel/standalone';
import jsx from '@vue/babel-plugin-jsx';
import hashId from 'hash-sum';
import { COMP_IDENTIFIER, createCompiler, resolveFilename } from './index';
Expand Down
Loading
Loading