Skip to content

Commit

Permalink
fix: metadata and hydrate root mismatched between csr and ssr (#12220)
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

---------

Co-authored-by: xiaoxiao <[email protected]>
Co-authored-by: Jinbao1001 <[email protected]>
  • Loading branch information
3 people authored Apr 1, 2024
1 parent ae9dc25 commit c9530cf
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 95 deletions.
21 changes: 20 additions & 1 deletion examples/ssr-demo/.umirc.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
export default {
svgr: {},
hash: true,
mfsu: false,
routePrefetch: {},
manifest: {},
clientLoader: {},
title: '测试title',
scripts: [`https://a.com/b.js`],
ssr: {
serverBuildPath: './umi.server.js',
builder: 'webpack',
renderFromRoot: false,
},
styles: [`body { color: red; }`, `https://a.com/b.css`],

metas: [
{
name: 'test',
content: 'content',
},
],
links: [{ href: '/foo.css', rel: 'preload' }],

headScripts: [
{
src: 'https://www.baidu.com',
},
],
};
5 changes: 4 additions & 1 deletion packages/preset-umi/src/commands/dev/getBabelOpts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { IApi } from '../../types';

export async function getBabelOpts(opts: { api: IApi }) {
// TODO: 支持用户自定义
const shouldUseAutomaticRuntime = semver.gte(opts.api.appData.react.version, '16.14.0');
const shouldUseAutomaticRuntime = semver.gte(
opts.api.appData.react.version,
'16.14.0',
);
const babelPresetOpts = await opts.api.applyPlugins({
key: 'modifyBabelPresetOpts',
initialValue: {
Expand Down
1 change: 1 addition & 0 deletions packages/preset-umi/src/features/ssr/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default (api: IApi) => {
serverBuildPath: zod.string(),
platform: zod.string(),
builder: zod.enum(['esbuild', 'webpack']),
renderFromRoot: zod.boolean(),
})
.deepPartial();
},
Expand Down
14 changes: 13 additions & 1 deletion packages/preset-umi/src/features/tmpFiles/tmpFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { importLazy, lodash, winPath } from '@umijs/utils';
import { existsSync, readdirSync } from 'fs';
import { basename, dirname, join } from 'path';
import { RUNTIME_TYPE_FILE_NAME } from 'umi';
import { getMarkupArgs } from '../../commands/dev/getMarkupArgs';
import { TEMPLATES_DIR } from '../../constants';
import { IApi } from '../../types';
import { getModuleExports } from './getModuleExports';
import { importsToStr } from './importsToStr';

const routesApi: typeof import('./routes') = importLazy(
require.resolve('./routes'),
);
Expand Down Expand Up @@ -496,6 +496,8 @@ if (process.env.NODE_ENV === 'development') {
}
return memo;
}, []);
const { headScripts, scripts, styles, title, favicons, links, metas } =
await getMarkupArgs({ api });
api.writeTmpFile({
noPluginDir: true,
path: 'umi.server.ts',
Expand All @@ -514,6 +516,16 @@ if (process.env.NODE_ENV === 'development') {
join(api.paths.absOutputPath, 'build-manifest.json'),
),
env: JSON.stringify(api.env),
metadata: JSON.stringify({
headScripts,
styles,
title,
favicons,
links,
metas,
scripts: scripts || [],
}),
renderFromRoot: api.config.ssr?.renderFromRoot ?? false,
},
});
}
Expand Down
3 changes: 3 additions & 0 deletions packages/preset-umi/templates/server.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const createOpts = {
helmetContext,
createHistory,
ServerInsertedHTMLContext,
metadata: {{{metadata}}},
renderFromRoot: {{{renderFromRoot}}}

};
const requestHandler = createRequestHandler(createOpts);
/**
Expand Down
21 changes: 18 additions & 3 deletions packages/renderer-react/src/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import ReactDOM from 'react-dom/client';
import { matchRoutes, Router, useRoutes } from 'react-router-dom';
import { AppContext, useAppData } from './appContext';
import { fetchServerLoader } from './dataFetcher';
import { Html } from './html';
import { createClientRoutes } from './routes';
import { ILoaderData, IRouteComponents, IRoutesById } from './types';

let root: ReactDOM.Root | null = null;

// react 18 some scenarios need unmount such as micro app
Expand Down Expand Up @@ -96,6 +96,11 @@ export type RenderClientOpts = {
* @doc 一般不需要改,微前端的时候会变化
*/
rootElement?: HTMLElement;
/**
* ssr 是否从 app root 根节点开始 render
* @doc 默认 false, 从 app root 开始 render,为 true 时从 html 开始
*/
renderFromRoot?: boolean;
/**
* 当前的路由配置
*/
Expand Down Expand Up @@ -331,12 +336,22 @@ const getBrowser = (
*/
export function renderClient(opts: RenderClientOpts) {
const rootElement = opts.rootElement || document.getElementById('root')!;

const Browser = getBrowser(opts, <Routes />);
// 为了测试,直接返回组件
if (opts.components) return Browser;

if (opts.hydrate) {
ReactDOM.hydrateRoot(rootElement, <Browser />);
// @ts-ignore
const loaderData = window.__UMI_LOADER_DATA__ || {};
// @ts-ignore
const metadata = window.__UMI_METADATA_LOADER_DATA__ || {};

ReactDOM.hydrateRoot(
document,
<Html {...{ metadata, loaderData }}>
<Browser />
</Html>,
);
return;
}

Expand Down
137 changes: 137 additions & 0 deletions packages/renderer-react/src/html.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React from 'react';
import { IHtmlProps, IScript } from './types';

const RE_URL = /^(http:|https:)?\/\//;

function isUrl(str: string) {
return (
RE_URL.test(str) ||
(str.startsWith('/') && !str.startsWith('/*')) ||
str.startsWith('./') ||
str.startsWith('../')
);
}

function normalizeScripts(script: IScript, extraProps = {}) {
if (typeof script === 'string') {
return isUrl(script)
? {
src: script,
...extraProps,
}
: { content: script };
} else if (typeof script === 'object') {
return {
...script,
...extraProps,
};
} else {
throw new Error(`Invalid script type: ${typeof script}`);
}
}

function generatorStyle(style: string) {
return isUrl(style)
? { type: 'link', href: style }
: { type: 'style', content: style };
}

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

{metadata?.links?.map((link: Record<string, string>, key: number) => {
return <link key={key} {...link} />;
})}
{metadata?.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) => {
const { content, ...rest } = normalizeScripts(script);
return (
<script key={key} {...(rest as any)}>
{content}
</script>
);
})}
</>
);
};

export function Html({
children,
loaderData,
manifest,
metadata,
renderFromRoot,
}: React.PropsWithChildren<IHtmlProps>) {
// TODO: 处理 head 标签,比如 favicon.ico 的一致性
// TODO: root 支持配置

if (renderFromRoot) {
return (
<>
<NormalizeMetadata metadata={metadata} />
<div id="root">{children}</div>
</>
);
}
return (
<html lang={metadata?.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']} />
)}
<NormalizeMetadata metadata={metadata} />
</head>
<body>
<noscript
dangerouslySetInnerHTML={{
__html: `<b>Enable JavaScript to run this app.</b>`,
}}
/>

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

{metadata?.scripts?.map((script: IScript, key: number) => {
const { content, ...rest } = normalizeScripts(script);
return (
<script key={key} {...(rest as any)}>
{content}
</script>
);
})}
</body>
</html>
);
}
64 changes: 3 additions & 61 deletions packages/renderer-react/src/server.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import type { IMetadata } from '@umijs/server/dist/types';
import React from 'react';
import { StaticRouter } from 'react-router-dom/server';
import { AppContext } from './appContext';
import { Routes } from './browser';
import { Html } from './html';
import { createClientRoutes } from './routes';
import { IRouteComponents, IRoutesById } from './types';

interface IHtmlProps {
routes: IRoutesById;
routeComponents: IRouteComponents;
pluginManager: any;
location: string;
loaderData: { [routeKey: string]: any };
manifest: any;
metadata?: IMetadata;
}
import { IRootComponentOptions } from './types';

// 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 @@ -62,51 +52,3 @@ export async function getClientRootComponent(opts: IHtmlProps) {
);
return <Html {...opts}>{app}</Html>;
}

function Html({
children,
loaderData,
manifest,
metadata,
}: React.PropsWithChildren<IHtmlProps>) {
// TODO: 处理 head 标签,比如 favicon.ico 的一致性
// TODO: root 支持配置

return (
<html lang={metadata?.lang || 'en'}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{metadata?.title && <title>{metadata.title}</title>}
{metadata?.description && (
<meta name="description" content={metadata.description} />
)}
{metadata?.keywords?.length && (
<meta name="keywords" content={metadata.keywords.join(',')} />
)}
{metadata?.metas?.map((em) => (
<meta key={em.name} name={em.name} content={em.content} />
))}
{manifest.assets['umi.css'] && (
<link rel="stylesheet" href={manifest.assets['umi.css']} />
)}
</head>
<body>
<noscript
dangerouslySetInnerHTML={{
__html: `<b>Enable JavaScript to run this app.</b>`,
}}
/>

<div id="root">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `window.__UMI_LOADER_DATA__ = ${JSON.stringify(
loaderData,
)}`,
}}
/>
</body>
</html>
);
}
Loading

0 comments on commit c9530cf

Please sign in to comment.