Skip to content

Commit

Permalink
feat: optimize the error prompts and editing experience of vue demo (#…
Browse files Browse the repository at this point in the history
…2065)

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

* refactor: use preflight option

* refactor: optimize userRenderer
  • Loading branch information
jeffwcx committed Apr 25, 2024
1 parent 0df859e commit 4da1489
Show file tree
Hide file tree
Showing 19 changed files with 239 additions and 89 deletions.
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 @@ -258,6 +258,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 @@ -268,6 +272,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);
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 @@ -210,6 +210,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 @@ -403,6 +403,7 @@ export default function rehypeDemo(
component,
renderOpts: {
rendererPath: runtimeOpts?.rendererPath,
preflightPath: runtimeOpts?.preflightPath,
},
};
}
Expand Down Expand Up @@ -463,6 +464,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

0 comments on commit 4da1489

Please sign in to comment.