Skip to content

Commit

Permalink
fix: hydrate logic for ssr (#12255)
Browse files Browse the repository at this point in the history
* feat: ssr支持head body 配置

* feat: support ssr

* fix: 回退metaloader执行逻辑判断

* fix: ts lint

* feat: 优化部分ssr代码

* feat: add client metadata hydrate data

* docs: hydtateFromRoot doc 修正

* fix: delete merge.with deps

* fix: delete merge.with deps

* fix: change hydrateFromRoot root to renderFromRoot

* fix: NormalizeMeta component for render root

* fix: NormalizeMeta component for render root

* fix: hydrate 遗留问题处理

* fix: ts-ignore window.__

* fix: 空格

* fix: lint

---------

Co-authored-by: xiaoxiao <[email protected]>
Co-authored-by: Jinbao1001 <[email protected]>
  • Loading branch information
3 people authored Apr 2, 2024
1 parent c9530cf commit 67f7e90
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 40 deletions.
8 changes: 7 additions & 1 deletion packages/preset-umi/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getMarkup } from '@umijs/server';
import { chalk, fsExtra, logger, rimraf, semver } from '@umijs/utils';
import { omit } from '@umijs/utils/compiled/lodash';
import { writeFileSync } from 'fs';
import { dirname, join, resolve } from 'path';
import type { IApi, IOnGenerateFiles } from '../types';
Expand Down Expand Up @@ -171,7 +172,12 @@ umi build --clean
publicPath: api.config.publicPath,
});
const { vite } = api.args;
const markupArgs = await getMarkupArgs({ api });
const args = await getMarkupArgs({ api });

// renderFromRoot = true, 将 html 中的 title, metas 标签逻辑全部交给 metadataLoader 合并逻辑处理
const markupArgs = api.config?.ssr?.renderFromRoot
? omit(args, ['title', 'metas'])
: args;
const finalMarkUpArgs = {
...markupArgs,
styles: markupArgs.styles.concat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ function createRouteMiddleware(opts: { api: IApi }) {
stats,
publicPath: opts.api.config.publicPath!,
});

const requestHandler = await createRequestHandler({
...markupArgs,
styles: markupArgs.styles.concat(
Expand Down
3 changes: 2 additions & 1 deletion packages/preset-umi/src/features/tmpFiles/tmpFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ if (process.env.NODE_ENV === 'development') {
join(api.paths.absOutputPath, 'build-manifest.json'),
),
env: JSON.stringify(api.env),
metadata: JSON.stringify({
tplOpts: JSON.stringify({
headScripts,
styles,
title,
Expand All @@ -526,6 +526,7 @@ if (process.env.NODE_ENV === 'development') {
scripts: scripts || [],
}),
renderFromRoot: api.config.ssr?.renderFromRoot ?? false,
mountElementId: api.config.mountElementId,
},
});
}
Expand Down
5 changes: 3 additions & 2 deletions packages/preset-umi/templates/server.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ const createOpts = {
helmetContext,
createHistory,
ServerInsertedHTMLContext,
metadata: {{{metadata}}},
renderFromRoot: {{{renderFromRoot}}}
tplOpts: {{{tplOpts}}},
renderFromRoot: {{{renderFromRoot}}},
mountElementId: '{{{mountElementId}}}'

};
const requestHandler = createRequestHandler(createOpts);
Expand Down
1 change: 1 addition & 0 deletions packages/preset-umi/templates/umi.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ async function render() {
routes,
routeComponents,
pluginManager,
mountElementId: '{{{mountElementId}}}',
rootElement: contextOpts.rootElement || document.getElementById('{{{ mountElementId }}}'),
{{#loadingComponent}}
loadingComponent: Loading,
Expand Down
9 changes: 7 additions & 2 deletions packages/renderer-react/src/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export type RenderClientOpts = {
* @doc https://umijs.org/docs/api/config#runtimepublicpath
*/
runtimePublicPath?: boolean;
/**
* react dom 渲染的的目标节点 id
* @doc 一般不需要改,微前端的时候会变化
*/
mountElementId?: string;
/**
* react dom 渲染的的目标 dom
* @doc 一般不需要改,微前端的时候会变化
Expand Down Expand Up @@ -347,8 +352,8 @@ export function renderClient(opts: RenderClientOpts) {
const metadata = window.__UMI_METADATA_LOADER_DATA__ || {};

ReactDOM.hydrateRoot(
document,
<Html {...{ metadata, loaderData }}>
opts.renderFromRoot ? rootElement : document,
<Html {...{ metadata, loaderData, mountElementId: opts.mountElementId }}>
<Browser />
</Html>,
);
Expand Down
61 changes: 37 additions & 24 deletions packages/renderer-react/src/html.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,36 +36,36 @@ function generatorStyle(style: string) {
: { type: 'style', content: style };
}

const NormalizeMetadata = (props: IHtmlProps) => {
const { metadata } = props;
const HydrateMetadata = (props: IHtmlProps) => {
const { tplOpts } = props;
return (
<>
{metadata?.title && <title>{metadata.title}</title>}
{metadata?.favicons?.map((favicon: string, key: number) => {
{tplOpts?.title && <title>{tplOpts.title}</title>}
{tplOpts?.favicons?.map((favicon: string, key: number) => {
return <link key={key} rel="shortcut icon" href={favicon} />;
})}
{metadata?.description && (
<meta name="description" content={metadata.description} />
{tplOpts?.description && (
<meta name="description" content={tplOpts.description} />
)}
{metadata?.keywords?.length && (
<meta name="keywords" content={metadata.keywords.join(',')} />
{tplOpts?.keywords?.length && (
<meta name="keywords" content={tplOpts.keywords.join(',')} />
)}
{metadata?.metas?.map((em: any) => (
{tplOpts?.metas?.map((em: any) => (
<meta key={em.name} name={em.name} content={em.content} />
))}

{metadata?.links?.map((link: Record<string, string>, key: number) => {
{tplOpts?.links?.map((link: Record<string, string>, key: number) => {
return <link key={key} {...link} />;
})}
{metadata?.styles?.map((style: string, key: number) => {
{tplOpts?.styles?.map((style: string, key: number) => {
const { type, href, content } = generatorStyle(style);
if (type === 'link') {
return <link key={key} rel="stylesheet" href={href} />;
} else if (type === 'style') {
return <style key={key}>{content}</style>;
}
})}
{metadata?.headScripts?.map((script: IScript, key: number) => {
{tplOpts?.headScripts?.map((script: IScript, key: number) => {
const { content, ...rest } = normalizeScripts(script);
return (
<script key={key} {...(rest as any)}>
Expand All @@ -81,29 +81,39 @@ export function Html({
children,
loaderData,
manifest,
metadata,
tplOpts,
renderFromRoot,
mountElementId,
}: React.PropsWithChildren<IHtmlProps>) {
// TODO: 处理 head 标签,比如 favicon.ico 的一致性
// TODO: root 支持配置

if (renderFromRoot) {
return (
<>
<NormalizeMetadata metadata={metadata} />
<div id="root">{children}</div>
<HydrateMetadata tplOpts={tplOpts} />
<div id={mountElementId}>{children}</div>
</>
);
}
// @ts-ignore
const serverBuildManifest =
typeof window === 'undefined'
? manifest
: // @ts-ignore
window.__UMI_BUILD_MANIFEST_DATA__;
return (
<html lang={metadata?.lang || 'en'}>
<html suppressHydrationWarning lang={tplOpts?.lang || 'en'}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{manifest?.assets['umi.css'] && (
<link rel="stylesheet" href={manifest?.assets['umi.css']} />
{serverBuildManifest?.assets['umi.css'] && (
<link
suppressHydrationWarning
rel="stylesheet"
href={manifest?.assets['umi.css']}
/>
)}
<NormalizeMetadata metadata={metadata} />
<HydrateMetadata tplOpts={tplOpts} />
</head>
<body>
<noscript
Expand All @@ -112,18 +122,21 @@ export function Html({
}}
/>

<div id="root">{children}</div>
<div id={mountElementId}>{children}</div>
<script
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `window.__UMI_LOADER_DATA__ = ${JSON.stringify(
loaderData || {},
)}; window.__UMI_METADATA_LOADER_DATA__ = ${JSON.stringify(
metadata,
)}`,
tplOpts || {},
)}; window.__UMI_BUILD_MANIFEST_DATA__ = ${
JSON.stringify(manifest) || {}
}`,
}}
/>

{metadata?.scripts?.map((script: IScript, key: number) => {
{tplOpts?.scripts?.map((script: IScript, key: number) => {
const { content, ...rest } = normalizeScripts(script);
return (
<script key={key} {...(rest as any)}>
Expand Down
8 changes: 5 additions & 3 deletions packages/renderer-react/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IMetadata } from '@umijs/server/dist/types';
import type { ITplOpts } from '@umijs/server/dist/types';
import type { RouteMatch, RouteObject } from 'react-router-dom';

export interface IRouteSSRProps {
Expand Down Expand Up @@ -50,15 +50,17 @@ export interface IRootComponentOptions {
location: string;
loaderData: { [routeKey: string]: any };
manifest: any;
metadata?: IMetadata;
tplOpts?: ITplOpts;
renderFromRoot: boolean;
mountElementId: string;
}

export interface IHtmlProps {
children?: React.ReactNode;
loaderData?: { [routeKey: string]: any };
manifest?: any;
metadata?: IMetadata;
tplOpts?: ITplOpts;
mountElementId?: string;
renderFromRoot?: boolean;
}

Expand Down
25 changes: 19 additions & 6 deletions packages/server/src/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import * as ReactDomServer from 'react-dom/server';
import { matchRoutes } from 'react-router-dom';
import { Writable } from 'stream';
import type {
IMetadata,
IRoutesById,
IServerLoaderArgs,
ITplOpts,
MetadataLoader,
ServerLoader,
UmiRequest,
Expand Down Expand Up @@ -39,8 +39,9 @@ interface CreateRequestHandlerOptions extends CreateRequestServerlessOptions {
createHistory: (opts: any) => any;
helmetContext?: any;
ServerInsertedHTMLContext: React.Context<ServerInsertedHTMLHook | null>;
metadata: IMetadata;
tplOpts: ITplOpts;
renderFromRoot: boolean;
mountElementId: string;
}

interface IExecLoaderOpts {
Expand Down Expand Up @@ -133,9 +134,9 @@ function createJSXGenerator(opts: CreateRequestHandlerOptions) {
});
Object.entries(metadataLoaderData).forEach(([k, v]) => {
if (Array.isArray(v)) {
opts.metadata[k] = (opts.metadata[k] || []).concat(v);
opts.tplOpts[k] = (opts.tplOpts[k] || []).concat(v);
} else {
opts.metadata[k] = v;
opts.tplOpts[k] = v;
}
});
}
Expand All @@ -155,8 +156,9 @@ function createJSXGenerator(opts: CreateRequestHandlerOptions) {
location: url,
manifest,
loaderData,
metadata: opts.metadata,
tplOpts: opts.tplOpts,
renderFromRoot: opts.renderFromRoot,
mountElementId: opts.mountElementId,
};

const element = (await opts.getClientRootComponent(
Expand Down Expand Up @@ -576,5 +578,16 @@ async function executeMetadataLoader(params: IExecMetaLoaderOpts) {
if (!mod.serverLoader || typeof mod.serverLoader !== 'function') {
return;
}
return (mod.metadataLoader satisfies MetadataLoader)(serverLoaderData);
const result = (mod.metadataLoader satisfies MetadataLoader)(
serverLoaderData,
);
// types IMetadata
return ['title', 'description', 'keywords', 'lang', 'metas'].reduce(
(acc, key) => {
if (Object.prototype.hasOwnProperty.call(result, key))
acc[key] = result[key];
return acc;
},
{} as any,
);
}
13 changes: 13 additions & 0 deletions packages/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ export interface IMetadata {
scripts?: (Record<string, string> | string)[];
[key: string]: any;
}

export interface ITplOpts {
title?: string;
description?: string;
keywords?: string[];
metas?: IMetaTag[];
headScripts?: (Record<string, string> | string)[];
links?: Record<string, string>[];
styles?: string[];
favicons?: string[];
scripts?: (Record<string, string> | string)[];
[key: string]: any;
}
export type MetadataLoader<T = any> = (
serverLoaderData: T,
req?: IServerLoaderArgs,
Expand Down

0 comments on commit 67f7e90

Please sign in to comment.