diff --git a/examples/antd-pro-create/package.json b/examples/antd-pro-create/package.json
index 32130a66187f..dcbbde374867 100644
--- a/examples/antd-pro-create/package.json
+++ b/examples/antd-pro-create/package.json
@@ -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": {
diff --git a/examples/ssr-demo/.umirc.ts b/examples/ssr-demo/.umirc.ts
index 18627a8fbbc7..c905a4960ba7 100644
--- a/examples/ssr-demo/.umirc.ts
+++ b/examples/ssr-demo/.umirc.ts
@@ -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',
+ },
+ ],
};
diff --git a/packages/preset-umi/src/commands/dev/getBabelOpts.ts b/packages/preset-umi/src/commands/dev/getBabelOpts.ts
index 172bf69895bd..27621fe3eb5c 100644
--- a/packages/preset-umi/src/commands/dev/getBabelOpts.ts
+++ b/packages/preset-umi/src/commands/dev/getBabelOpts.ts
@@ -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: {
diff --git a/packages/preset-umi/src/features/routePreloadOnLoad/routePreloadOnLoad.ts b/packages/preset-umi/src/features/routePreloadOnLoad/routePreloadOnLoad.ts
index 22525e4f137f..04d623b3570f 100644
--- a/packages/preset-umi/src/features/routePreloadOnLoad/routePreloadOnLoad.ts
+++ b/packages/preset-umi/src/features/routePreloadOnLoad/routePreloadOnLoad.ts
@@ -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 }) => {
diff --git a/packages/preset-umi/src/features/ssr/ssr.ts b/packages/preset-umi/src/features/ssr/ssr.ts
index d5fa28b509c0..ede66b9d22a0 100644
--- a/packages/preset-umi/src/features/ssr/ssr.ts
+++ b/packages/preset-umi/src/features/ssr/ssr.ts
@@ -27,6 +27,7 @@ export default (api: IApi) => {
serverBuildPath: zod.string(),
platform: zod.string(),
builder: zod.enum(['esbuild', 'webpack']),
+ hydrateFromRoot: zod.boolean(),
})
.deepPartial();
},
diff --git a/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts b/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts
index 1c3a4e2124bb..1863f0b19b8c 100644
--- a/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts
+++ b/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts
@@ -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'),
);
@@ -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',
@@ -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,
},
});
}
diff --git a/packages/preset-umi/templates/server.tpl b/packages/preset-umi/templates/server.tpl
index e01021b9836a..02a2a181f97f 100644
--- a/packages/preset-umi/templates/server.tpl
+++ b/packages/preset-umi/templates/server.tpl
@@ -51,6 +51,9 @@ const createOpts = {
helmetContext,
createHistory,
ServerInsertedHTMLContext,
+ metadata: {{{metadata}}},
+ hydrateFromRoot: {{{hydrateFromRoot}}}
+
};
const requestHandler = createRequestHandler(createOpts);
/**
diff --git a/packages/renderer-react/src/browser.tsx b/packages/renderer-react/src/browser.tsx
index 2e3adcc7a6f9..3aae23d7bd1c 100644
--- a/packages/renderer-react/src/browser.tsx
+++ b/packages/renderer-react/src/browser.tsx
@@ -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
@@ -96,6 +96,11 @@ export type RenderClientOpts = {
* @doc 一般不需要改,微前端的时候会变化
*/
rootElement?: HTMLElement;
+ /**
+ * ssr 是否从 app root 根节点开始 hydrate
+ * @doc 默认 false, 从 app root 开始水合,为 true 时从 html 开始
+ */
+ hydrateFromRoot?: boolean;
/**
* 当前的路由配置
*/
@@ -331,12 +336,22 @@ const getBrowser = (
*/
export function renderClient(opts: RenderClientOpts) {
const rootElement = opts.rootElement || document.getElementById('root')!;
+
const Browser = getBrowser(opts, );
// 为了测试,直接返回组件
if (opts.components) return Browser;
-
if (opts.hydrate) {
- ReactDOM.hydrateRoot(rootElement, );
+ // @ts-ignore
+ const loaderData = window.__UMI_LOADER_DATA__ || {};
+ // @ts-ignore
+ const metadata = window.__UMI_METADATA_LOADER_DATA__ || {};
+
+ ReactDOM.hydrateRoot(
+ document,
+
+
+ ,
+ );
return;
}
diff --git a/packages/renderer-react/src/dataFetcher.ts b/packages/renderer-react/src/dataFetcher.ts
index 2122a46444ec..0db63b41a6cc 100644
--- a/packages/renderer-react/src/dataFetcher.ts
+++ b/packages/renderer-react/src/dataFetcher.ts
@@ -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',
})
diff --git a/packages/renderer-react/src/html.tsx b/packages/renderer-react/src/html.tsx
new file mode 100644
index 000000000000..c80adc363e70
--- /dev/null
+++ b/packages/renderer-react/src/html.tsx
@@ -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) {
+ // TODO: 处理 head 标签,比如 favicon.ico 的一致性
+ // TODO: root 支持配置
+ return (
+
+
+
+
+ {metadata?.title && {metadata.title}}
+ {metadata?.favicons?.map((favicon: string, key: number) => {
+ return ;
+ })}
+ {metadata?.description && (
+
+ )}
+ {metadata?.keywords?.length && (
+
+ )}
+ {metadata?.metas?.map((em: any) => (
+
+ ))}
+
+ {metadata?.links?.map((link: Record, key: number) => {
+ return ;
+ })}
+ {metadata?.styles?.map((style: string, key: number) => {
+ const { type, href, content } = generatorStyle(style);
+ if (type === 'link') {
+ return ;
+ } else if (type === 'style') {
+ return ;
+ }
+ })}
+ {metadata?.headScripts?.map((script: IScript, key: number) => {
+ const { content, ...rest } = normalizeScripts(script);
+ return (
+
+ );
+ })}
+ {manifest?.assets['umi.css'] && (
+
+ )}
+
+
+