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 1 commit
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
25 changes: 22 additions & 3 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(Object.assign(demo, { id }), {
onInitError: (error) => {
jeffwcx marked this conversation as resolved.
Show resolved Hide resolved
throw error;
},
});

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

const {
node: liveDemoNode,
setSource,
error: liveDemoError,
rendered,
jeffwcx marked this conversation as resolved.
Show resolved Hide resolved
} = useLiveDemo(id!);

const finalNode =
Expand All @@ -41,8 +47,21 @@ const DemoRenderPage: FC = () => {
return () => window.removeEventListener('message', handler);
}, [setSource]);

// The error of the demo with renderer is asynchronous
useEffect(() => {
jeffwcx marked this conversation as resolved.
Show resolved Hide resolved
if (rendered && (liveDemoError || liveDemoNode)) {
window.postMessage({
type: 'dumi.liveDemo.compileDone',
value: { err: liveDemoError },
});
}
}, [liveDemoError, liveDemoNode, rendered]);

// notify parent window that compile done
useEffect(() => {
if (renderOpts?.renderer) {
return;
}
if (liveDemoNode || liveDemoError) {
window.postMessage({
type: 'dumi.liveDemo.compileDone',
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(Object.assign(demo, { id }), {
onInitError: (err) => {
throw err;
},
});

// hide debug demo in production
if (process.env.NODE_ENV === 'production' && props.previewerProps.debug)
Expand Down
13 changes: 13 additions & 0 deletions src/client/theme-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,19 @@ export type IDemoCompileFn = (
export type IDemoCancelableFn = (
canvas: HTMLElement,
component: AgnosticComponentModule,
options?: {
/**
* If the renderer of a third-party application invokes this function,
* it will be processed by the react application in ErrorBoundary
*/
onRuntimeError?: (error: Error) => void;
PeachScript marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

这里之前讨论不是用 throw error 替代么

/**
* Error message will only be given below the Demo
*
* Usually this is an error during demo initialization.
*/
onInitError?: (error: Error) => void;
},
) => (() => void) | Promise<() => void>;

export type IDemoData = {
Expand Down
24 changes: 18 additions & 6 deletions src/client/theme-api/useLiveDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export const useLiveDemo = (

const { context = {}, asset, renderOpts } = demo;
const [component, setComponent] = useState<AgnosticComponentType>();
const [error, setError] = useState<Error | null>(null);
const [rendered, setRendered] = useState<boolean>(false);

const ref = useRenderer(
component
? {
Expand All @@ -49,11 +52,20 @@ export const useLiveDemo = (
component,
}
: Object.assign(demo, { id }),
{
onInitError: (err) => {
setError(err);
},
onRuntimeError: (err) => {
throw err;
},
onResolved: () => {
setRendered(true);
},
},
);

const [demoNode, setDemoNode] = useState<ReactNode>();

const [error, setError] = useState<Error | null>(null);
const setSource = useCallback(
throttle(
async (source: Record<string, string>) => {
Expand All @@ -70,11 +82,10 @@ export const useLiveDemo = (
clearTimeout(loadingTimer.current);
setLoading(false);
}

setRendered(false);
if (opts?.iframe && opts?.containerRef?.current) {
const iframeWindow =
opts.containerRef.current.querySelector('iframe')!.contentWindow!;

jeffwcx marked this conversation as resolved.
Show resolved Hide resolved
await new Promise<void>((resolve) => {
const handler = (
ev: MessageEvent<{
Expand All @@ -88,7 +99,6 @@ export const useLiveDemo = (
resolve();
}
};

iframeWindow.addEventListener('message', handler);
iframeWindow.postMessage({
type: 'dumi.liveDemo.setSource',
Expand All @@ -115,6 +125,7 @@ export const useLiveDemo = (
} catch (error: any) {
setError(error);
resetLoadingStatus();
setRendered(true);
return;
}
}
Expand All @@ -133,6 +144,7 @@ export const useLiveDemo = (
setError(null);
} catch (err: any) {
setError(err);
setRendered(true);
}
resetLoadingStatus();
return;
Expand Down Expand Up @@ -188,5 +200,5 @@ export const useLiveDemo = (
[context, asset, renderOpts],
);

return { node: demoNode, loading, error, setSource };
return { node: demoNode, loading, error, setSource, rendered };
};
83 changes: 59 additions & 24 deletions src/client/theme-api/useRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,81 @@
import { useEffect, useRef } from 'react';
import type { AgnosticComponentModule, IDemoData } from './types';
import type {
AgnosticComponentModule,
IDemoCancelableFn,
IDemoData,
} from './types';

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

export const useRenderer = ({
id,
component,
renderOpts,
}: IDemoData & { id: string }) => {
export type UseRendererOptions = Parameters<IDemoCancelableFn>[2] & {
onResolved?: () => void;
};

export const useRenderer = (
{ id, component, renderOpts }: IDemoData & { id: string },
options?: UseRendererOptions,
) => {
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;
const legacyHandler = map.get(id);
if (resolving.current) return;
resolving.current = true;

const legacyTeardown = legacyHandler?.teardown;

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);
const hostElement = document.createElement('div');
try {
canvasRef.current.appendChild(hostElement);
const teardown = await renderer(hostElement, module, options);
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();
options?.onInitError?.(error as Error);
} finally {
resolving.current = false;
options?.onResolved?.();
}
}

resolveRender();
Expand Down
55 changes: 27 additions & 28 deletions suites/preset-vue/lib/compiler.mjs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 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';
import { createApp, h, nextTick } 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=_;
function c(i){return new Promise((t,n)=>{let e=document.createElement("div");e.style.width="0",e.style.height="0",e.style.visibility="hidden",document.body.appendChild(e);let r;function o(){nextTick(()=>{r.config.errorHandler=void 0,r.unmount(),e.remove();});}r=createApp({mounted(){t(),o();},render(){return h(i)}}),r.config.warnHandler=d=>{o(),n(d);},r.config.errorHandler=d=>{o(),n(d);},r.mount(e);})}var u=async function(i,t,n){t.__css__&&setTimeout(()=>{document.querySelectorAll(`style[css-${t.__id__}]`).forEach(r=>r.remove()),document.head.insertAdjacentHTML("beforeend",`<style css-${t.__id__}>${t.__css__}</style>`);},1),await c(t);let e=createApp(t);return e.config.errorHandler=function(r){n?.onRuntimeError?.(r);},e.mount(i),()=>{e.unmount();}},_=u;

export { l as default };
export { _ 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
50 changes: 47 additions & 3 deletions suites/preset-vue/src/vue/runtime/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,47 @@
import type { IDemoCancelableFn } from 'dumi/dist/client/theme-api';
import { createApp } from 'vue';
import { createApp, h, nextTick, type App } from 'vue';

const renderer: IDemoCancelableFn = function (canvas, component) {
function tryRender(component: any) {
return new Promise<void>((resolve, reject) => {
const el = document.createElement('div');
el.style.width = '0';
el.style.height = '0';
el.style.visibility = 'hidden';
jeffwcx marked this conversation as resolved.
Show resolved Hide resolved
document.body.appendChild(el);
let app!: App;
function destroy() {
nextTick(() => {
app.config.errorHandler = void 0;
app.unmount();
el.remove();
});
}
app = createApp({
mounted() {
resolve();
destroy();
},
render() {
return h(component);
},
});
app.config.warnHandler = (msg) => {
destroy();
reject(new Error(msg));
};
app.config.errorHandler = (error) => {
destroy();
reject(error);
};
app.mount(el);
});
}

const renderer: IDemoCancelableFn = async function (
canvas,
component,
options,
) {
if (component.__css__) {
setTimeout(() => {
document
Expand All @@ -13,9 +53,13 @@ const renderer: IDemoCancelableFn = function (canvas, component) {
);
}, 1);
}
// check component is able to render, to avoid show vue overlay error
jeffwcx marked this conversation as resolved.
Show resolved Hide resolved
await tryRender(component);
const app = createApp(component);

app.config.errorHandler = (e) => console.error(e);
app.config.errorHandler = function (err) {
options?.onRuntimeError?.(err as Error);
};
app.mount(canvas);
return () => {
app.unmount();
Expand Down
2 changes: 1 addition & 1 deletion suites/preset-vue/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default defineConfig([
target: 'esnext',
platform: 'browser',
noExternal: ['@vue/babel-plugin-jsx', 'hash-sum'],
external: ['vue/compiler-sfc', '@babel/standalone'],
external: ['vue/compiler-sfc'],
treeshake: true,
},
{
Expand Down
Loading