Skip to content

Commit

Permalink
feat: SSR support useServerInsertedHTML (#12247)
Browse files Browse the repository at this point in the history
* feat: SSR support useServerInsertedHTML

* feat: ssr insert html

* fix: string template

* chore: update lock
  • Loading branch information
MadCcc authored Apr 1, 2024
1 parent 9d2e626 commit ae9dc25
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 48 deletions.
2 changes: 2 additions & 0 deletions examples/ssr-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"start:prod": "node ./production-server.js"
},
"dependencies": {
"@ant-design/cssinjs": "^1.18.5",
"antd": "^5",
"express": "4.18.2",
"umi": "workspace:*"
}
Expand Down
23 changes: 23 additions & 0 deletions examples/ssr-demo/src/layouts/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import { useState } from 'react';
import { Outlet, useServerInsertedHTML } from 'umi';

export default function Layout() {
const [cssCache] = useState(() => createCache());

useServerInsertedHTML(() => {
const style = extractStyle(cssCache, { plain: true });
return (
<style
id="antd-cssinjs"
dangerouslySetInnerHTML={{ __html: style }}
></style>
);
});

return (
<StyleProvider cache={cssCache}>
<Outlet />
</StyleProvider>
);
}
14 changes: 13 additions & 1 deletion examples/ssr-demo/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Input } from 'antd';
import { useId } from 'react';
import {
Link,
MetadataLoader,
Expand All @@ -22,16 +24,26 @@ export default function HomePage() {
const serverLoaderData = useServerLoaderData<typeof serverLoader>();

useServerInsertedHTML(() => {
return <div>inserted html</div>;
return (
<style
dangerouslySetInnerHTML={{
__html: `.server_inserted_style { color: #1677ff }`,
}}
></style>
);
});

const id = useId();

return (
<div>
<h1 className="title">Hello~</h1>
<p className="server_inserted_style">id: {id}</p>
<p className={styles.blue}>This is index.tsx</p>
<p className={cssStyle.title}>I should be pink</p>
<p className={cssStyle.blue}>I should be cyan</p>
<Button />
<Input placeholder="这个样式不应该闪烁" />
<img src={bigImage} alt="" />
<img src={umiLogo} alt="umi" />
<Link to="/users/user">/users/user</Link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export default (api: IApi) => {
},
];
}

return [];
},
stage: Infinity,
Expand Down
106 changes: 75 additions & 31 deletions packages/server/src/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,9 @@ interface IExecMetaLoaderOpts extends IExecLoaderOpts {
serverLoaderData?: any;
}

const createJSXProvider = (
Provider: any,
serverInsertedHTMLCallbacks: Set<() => React.ReactNode>,
) => {
const createJSXProvider = (Provider: any) => {
const serverInsertedHTMLCallbacks: Set<() => React.ReactNode> = new Set();

const JSXProvider = (props: any) => {
const addInsertedHtml = React.useCallback(
(handler: () => React.ReactNode) => {
Expand All @@ -67,7 +66,7 @@ const createJSXProvider = (
value: addInsertedHtml,
});
};
return JSXProvider;
return [JSXProvider, serverInsertedHTMLCallbacks] as const;
};

function createJSXGenerator(opts: CreateRequestHandlerOptions) {
Expand Down Expand Up @@ -163,16 +162,27 @@ function createJSXGenerator(opts: CreateRequestHandlerOptions) {
};
}

const SERVER_INSERTED_HTML = 'umi-server-inserted-html';
const getGenerateStaticHTML = (
serverInsertedHTMLCallbacks?: Set<() => React.ReactNode>,
serverInsertedHTMLCallbacks: Set<() => React.ReactNode>,
opts?: {
wrapper?: boolean;
},
) => {
const children = React.createElement(React.Fragment, {
children: Array.from(serverInsertedHTMLCallbacks || []).map((callback) =>
callback(),
),
});
return (
ReactDomServer.renderToString(
React.createElement(React.Fragment, {
children: Array.from(serverInsertedHTMLCallbacks || []).map(
(callback) => callback(),
),
}),
opts?.wrapper
? React.createElement(
'div',
{ id: SERVER_INSERTED_HTML, hidden: true },
children,
)
: children,
) || ''
);
};
Expand All @@ -184,11 +194,8 @@ export function createMarkupGenerator(opts: CreateRequestHandlerOptions) {
const jsx = await jsxGeneratorDeferrer(url);
if (jsx) {
return new Promise(async (resolve, reject) => {
const serverInsertedHTMLCallbacks: Set<() => React.ReactNode> =
new Set();
const JSXProvider = createJSXProvider(
const [JSXProvider, serverInsertedHTMLCallbacks] = createJSXProvider(
opts.ServerInsertedHTMLContext.Provider,
serverInsertedHTMLCallbacks,
);

let chunks: Buffer[] = [];
Expand All @@ -200,7 +207,10 @@ export function createMarkupGenerator(opts: CreateRequestHandlerOptions) {
};
writable.on('finish', async () => {
let html = Buffer.concat(chunks).toString('utf8');
html += await getGenerateStaticHTML(serverInsertedHTMLCallbacks);
const serverHTML = getGenerateStaticHTML(serverInsertedHTMLCallbacks);
if (serverHTML) {
html = html.replace(/<\/head>/, `${serverHTML}</head>`);
}
// append helmet tags to head
if (opts.helmetContext) {
html = html.replace(
Expand Down Expand Up @@ -266,9 +276,11 @@ export default function createRequestHandler(
otherwise(): Promise<void> | void;
};

const replaceServerHTMLScript = `<script>!function(){var e=document.getElementById("${SERVER_INSERTED_HTML}");e&&(Array.from(e.children).forEach(e=>{document.head.appendChild(e)}),e.remove())}();</script>`;

if (typeof FetchEvent !== 'undefined' && args[0] instanceof FetchEvent) {
// worker mode
const [ev, opts] = args as IWorkerRequestHandlerArgs;
const [ev, workerOpts] = args as IWorkerRequestHandlerArgs;
const { pathname, searchParams } = new URL(ev.request.url);

ret = {
Expand All @@ -290,23 +302,42 @@ export default function createRequestHandler(
});

// allow modify response
if (opts?.modifyResponse) {
res = await opts.modifyResponse(res);
if (workerOpts?.modifyResponse) {
res = await workerOpts.modifyResponse(res);
}

ev.respondWith(res);
},
async sendPage(jsx) {
const [JSXProvider, serverInsertedHTMLCallbacks] = createJSXProvider(
opts.ServerInsertedHTMLContext.Provider,
);
// handle route path request
const stream = await ReactDomServer.renderToReadableStream(
jsx.element,
React.createElement(JSXProvider, undefined, jsx.element),
{
bootstrapScripts: [jsx.manifest.assets['umi.js'] || '/umi.js'],
onError(x: any) {
console.error(x);
},
},
);

const transformStream = new TransformStream({
flush(controller) {
if (serverInsertedHTMLCallbacks.size) {
const serverHTML = getGenerateStaticHTML(
serverInsertedHTMLCallbacks,
{ wrapper: true },
);
controller.enqueue(serverHTML);
controller.enqueue(replaceServerHTMLScript);
}
},
});

stream.pipeThrough(transformStream);

let res = new Response(stream, {
headers: {
'content-type': 'text/html; charset=utf-8',
Expand All @@ -315,8 +346,8 @@ export default function createRequestHandler(
});

// allow modify response
if (opts?.modifyResponse) {
res = await opts.modifyResponse(res);
if (workerOpts?.modifyResponse) {
res = await workerOpts.modifyResponse(res);
}

ev.respondWith(res);
Expand All @@ -343,6 +374,9 @@ export default function createRequestHandler(
res.status(200).json(data);
},
async sendPage(jsx) {
const [JSXProvider, serverInsertedHTMLCallbacks] = createJSXProvider(
opts.ServerInsertedHTMLContext.Provider,
);
const writable = new Writable();

res.type('html');
Expand All @@ -353,19 +387,29 @@ export default function createRequestHandler(
};

writable.on('finish', async () => {
res.write(getGenerateStaticHTML());
if (serverInsertedHTMLCallbacks.size) {
res.write(
getGenerateStaticHTML(serverInsertedHTMLCallbacks, {
wrapper: true,
}),
);
res.write(replaceServerHTMLScript);
}
res.end();
});

const stream = ReactDomServer.renderToPipeableStream(jsx.element, {
bootstrapScripts: [jsx.manifest.assets['umi.js'] || '/umi.js'],
onShellReady() {
stream.pipe(writable);
},
onError(x: any) {
console.error(x);
const stream = ReactDomServer.renderToPipeableStream(
React.createElement(JSXProvider, undefined, jsx.element),
{
bootstrapScripts: [jsx.manifest.assets['umi.js'] || '/umi.js'],
onShellReady() {
stream.pipe(writable);
},
onError(x: any) {
console.error(x);
},
},
});
);
},
otherwise: next,
};
Expand Down
38 changes: 23 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ae9dc25

Please sign in to comment.