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

fix(ssr): render same root in server #12002

Closed
wants to merge 15 commits into from
5 changes: 4 additions & 1 deletion examples/ssr-demo/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import './index.less';
// @ts-ignore
import styles from './index.less';
// @ts-ignore
import { useId } from 'react';
import umiLogo from './umi.png';

export default function HomePage() {
const clientLoaderData = useClientLoaderData();
const serverLoaderData = useServerLoaderData<typeof serverLoader>();
const id = useId();

useServerInsertedHTML(() => {
return <div>inserted html</div>;
Expand All @@ -28,6 +30,7 @@ export default function HomePage() {
return (
<div>
<h1 className="title">Hello~</h1>
<h2 id={id}>{id}</h2>
<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>
Expand All @@ -53,7 +56,7 @@ export async function clientLoader() {
}

export const serverLoader: ServerLoader = async (req) => {
const url = req!.request.url;
const url = req?.request.url;
await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
return { message: `data from server loader of index.tsx, url: ${url}` };
};
Expand Down
8 changes: 7 additions & 1 deletion packages/renderer-react/src/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { matchRoutes, Router, useRoutes } from 'react-router-dom';
import { AppContext, useAppData } from './appContext';
import { fetchServerLoader } from './dataFetcher';
import { createClientRoutes } from './routes';
import { Html } from './server';
import { ILoaderData, IRouteComponents, IRoutesById } from './types';

let root: ReactDOM.Root | null = null;
Expand Down Expand Up @@ -336,7 +337,12 @@ export function renderClient(opts: RenderClientOpts) {
if (opts.components) return Browser;

if (opts.hydrate) {
ReactDOM.hydrateRoot(rootElement, <Browser />);
ReactDOM.hydrateRoot(
document,
<Html {...opts}>
<Browser />
</Html>,
);
return;
}

Expand Down
31 changes: 20 additions & 11 deletions packages/renderer-react/src/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Routes } from './browser';
import { createClientRoutes } from './routes';
import { IRouteComponents, IRoutesById } from './types';

interface IHtmlProps {
interface IRootComponentOptions {
routes: IRoutesById;
routeComponents: IRouteComponents;
pluginManager: any;
Expand All @@ -17,7 +17,7 @@ interface IHtmlProps {
}

// Get the root React component for ReactDOMServer.renderToString
export async function getClientRootComponent(opts: IHtmlProps) {
export async function getClientRootComponent(opts: IRootComponentOptions) {
const basename = '/';
const components = { ...opts.routeComponents };
const clientRoutes = createClientRoutes({
Expand Down Expand Up @@ -63,7 +63,14 @@ export async function getClientRootComponent(opts: IHtmlProps) {
return <Html {...opts}>{app}</Html>;
}

function Html({
interface IHtmlProps {
children: React.ReactNode;
loaderData?: { [routeKey: string]: any };
manifest?: any;
metadata?: IMetadata;
}

export function Html({
children,
loaderData,
manifest,
Expand All @@ -87,7 +94,7 @@ function Html({
{metadata?.metas?.map((em) => (
<meta key={em.name} name={em.name} content={em.content} />
))}
{manifest.assets['umi.css'] && (
{manifest?.assets['umi.css'] && (
<link rel="stylesheet" href={manifest.assets['umi.css']} />
)}
</head>
Expand All @@ -99,13 +106,15 @@ function Html({
/>

<div id="root">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `window.__UMI_LOADER_DATA__ = ${JSON.stringify(
loaderData,
)}`,
}}
/>
{loaderData && (
Copy link
Contributor

Choose a reason for hiding this comment

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

感觉这里有问题吧:

  1. 不应该在 browser.tsx 里导入 server.tsx 的东西,会造成 browser 运行时和 node 运行时混在一起,如果要复用应该单独抽取文件出来。

  2. 最重要的是这里服务端有 loaderData 的 html 发送给用户,而用户的 js 代码里 hydrate 的是一个没有 loaderData 的 html 树,这不一致吧。

Copy link
Contributor Author

Choose a reason for hiding this comment

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

这里虽然看上去是不一致的但是不会导致注水失败。这段脚本如果只是修改 window 参数的话可以考虑放到 head 里

Copy link
Contributor Author

@MadCcc MadCcc Jan 16, 2024

Choose a reason for hiding this comment

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

注水的具体机制我还没有深挖过,目前做的尝试发现不是所有情况下的 html 不一致都会打断注水,比如 head 里的标签数量等,会保留服务端的 html

Copy link
Contributor

Choose a reason for hiding this comment

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

无论是否会注水正确,我认为都不应该从 html 开始 hydrate ,因为不管是社区共识还是 react 官方文档,都统一从 react root 节点开始 hydrate 保持一致,应该保持这个基本原则。

<script
dangerouslySetInnerHTML={{
__html: `window.__UMI_LOADER_DATA__ = ${JSON.stringify(
loaderData,
)}`,
}}
/>
)}
</body>
</html>
);
Expand Down
17 changes: 8 additions & 9 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,21 +140,20 @@ export async function getMarkup(
`<!DOCTYPE html>
<html>
<head>`,
metas.join('\n'),
favicons.join('\n'),
metas.join(''),
favicons.join(''),
title,
links.join('\n'),
styles.join('\n'),
headScripts.join('\n'),
`</head>
<body>`,
links.join(''),
styles.join(''),
headScripts.join(''),
`</head><body>`,
markup,
scripts.join('\n'),
scripts.join(''),
`</body>
</html>`,
]
.filter(Boolean)
.join('\n');
.join('');
if (opts.modifyHTML) {
markup = await opts.modifyHTML(markup, { path: opts.path });
}
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export function createMarkupGenerator(opts: CreateRequestHandlerOptions) {
serverInsertedHTMLCallbacks,
);

let chunks: Buffer[] = [];
const chunks: Buffer[] = [];
const writable = new Writable();

writable._write = (chunk, _encoding, next) => {
Expand Down
Loading