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: client metadata set for ssr #12253

Closed
wants to merge 12 commits into from
2 changes: 1 addition & 1 deletion examples/antd-pro-create/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"prettier": "^2.8.4",
"start-server-and-test": "^1.15.2",
"swagger-ui-dist": "^4.14.2",
"umi-presets-pro": "^1.0.5",
"umi-presets-pro": "^2.0.3",
"umi-serve": "^1.9.11"
},
"engines": {
Expand Down
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',
hydrateFromRoot: 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
Original file line number Diff line number Diff line change
Expand Up @@ -219,44 +219,47 @@ export default (api: IApi) => {
api.config.routeLoader?.moduleType === 'esm',
});

api.addHTMLHeadScripts(() => {
if (api.name === 'build' && routeChunkFilesMap) {
// internal tern app use map mode
return api.config.tern
? // map mode
[
{
type: PRELOAD_ROUTE_MAP_SCP_TYPE,
content: JSON.stringify(routeChunkFilesMap),
},
]
: // script mode
[
{
content: readFileSync(
join(
TEMPLATES_DIR,
'routePreloadOnLoad/preloadRouteFilesScp.js',
),
'utf-8',
)
.replace(
'"{{routeChunkFilesMap}}"',
JSON.stringify(routeChunkFilesMap),
api.addHTMLHeadScripts({
fn: () => {
if (api.name === 'build' && routeChunkFilesMap) {
// internal tern app use map mode
return api.config.tern
? // map mode
[
{
type: PRELOAD_ROUTE_MAP_SCP_TYPE,
content: JSON.stringify(routeChunkFilesMap),
},
]
: // script mode
[
{
content: readFileSync(
join(
TEMPLATES_DIR,
'routePreloadOnLoad/preloadRouteFilesScp.js',
),
'utf-8',
)
.replace('{{basename}}', api.config.base)
.replace(
'"{{publicPath}}"',
`${
// handle runtimePublicPath
api.config.runtimePublicPath ? 'window.publicPath||' : ''
}"${api.config.publicPath}"`,
),
},
];
}
.replace(
'"{{routeChunkFilesMap}}"',
JSON.stringify(routeChunkFilesMap),
)
.replace('{{basename}}', api.config.base)
.replace(
'"{{publicPath}}"',
`${
// handle runtimePublicPath
api.config.runtimePublicPath ? 'window.publicPath||' : ''
}"${api.config.publicPath}"`,
),
},
];
}

return [];
return [];
},
stage: Infinity,
});

api.onBuildComplete(async ({ err, stats }) => {
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']),
hydrateFromRoot: 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 || [],
}),
hydrateFromRoot: api.config.ssr?.hydrateFromRoot ?? 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}}},
hydrateFromRoot: {{{hydrateFromRoot}}}

};
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 根节点开始 hydrate
* @doc 默认 false, 从 app root 开始水合,为 true 时从 html 开始
*/
hydrateFromRoot?: 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
5 changes: 4 additions & 1 deletion packages/renderer-react/src/dataFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export function fetchServerLoader({
url: window.location.href,
}).toString();
// 在有basename的情况下__serverLoader的请求路径需要加上basename
const url = `${withEndSlash(basename)}__serverLoader?${query}`;
// FIXME: 先临时解自定义 serverLoader 请求路径的问题,后续改造 serverLoader 时再提取成类似 runtimeServerLoader 的配置项
const url = `${withEndSlash(
(window as any).umiServerLoaderPath || basename,
)}__serverLoader?${query}`;
fetch(url, {
credentials: 'include',
})
Expand Down
118 changes: 118 additions & 0 deletions packages/renderer-react/src/html.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 };
}

export 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?.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>
);
})}
{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 || {},
)}; 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>
);
}
Loading
Loading