Skip to content

Commit e63fed3

Browse files
gwuhaolin奇风sorryccfz6m
authored
feature(SSR): 支持通过导出metadataLoader()设置页面的TDK等meta信息 (#11833)
* fix: dev环境构建SSR server.js时,环境判断错误导致输出的dev产物带上了hash * Update packages/preset-umi/src/features/ssr/webpack/webpack.ts Co-authored-by: chencheng (云谦) <[email protected]> * fix: dev环境构建SSR server.js时,环境判断错误导致输出的dev产物带上了hash * feature: 支持自定义SSR构建目标,默认为node,可配置为webworker以支持运行在类似Cloudflare Workers的平台 以解决 react-dom/server renderToReadableStream 不存在的问题 * feature: getManifest支持传入sourceDir表示SSR产物目录 * feature: getManifest支持传入sourceDir表示SSR产物目录 * fix: rendertoreadablestream is not a function * Revert "feature: 支持自定义SSR构建目标,默认为node,可配置为webworker以支持运行在类似Cloudflare Workers的平台" This reverts commit 5b0946a. * fix: rendertoreadablestream is not a function * Update packages/server/src/ssr.ts Co-authored-by: 咲奈Sakina <[email protected]> * Update packages/server/src/ssr.ts Co-authored-by: 咲奈Sakina <[email protected]> * fix: 还原g_getAssets * fix: 被执行时才调用getManifest(),避免在入口立即调用getManifest() * fix: 被执行时才调用getManifest(),避免在入口立即调用getManifest() * fix: 被执行时才调用getManifest(),避免在入口立即调用getManifest() * feature: SSR模式下,多输出一份和index.html完全一致的*.html文件,用于ER场景 * feature: SSR模式下,多输出一份和index.html完全一致的*.html文件,用于ER场景 * fix: SSR withoutHTML模式下,包一层<div id="root">{app}</div> * fix: 回滚SSR模式下,多输出一份和index.html完全一致的*.html文件,用于ER场景 * fix: SSR withoutHTML模式下也需要注入loaderData数据用于注水时 * fix: 在有basename的情况下__serverLoader的请求路径需要加上basename * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * fix: async 函数返回值不需要 await ,不然会多造成一次异步成本。 * fix: 在请求__serverLoader时,带上cookie,以实现在SSR时请求需要登入态的接口 * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * fix: dumi 里做 ssg 时传的 path 是不带 host 的,需要加上host * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * feature: 支持在serverLoader中读取当前request,以实现在serverLoader中读取url中的参数和headers * fix: 防止多chunk情况下(有懒加载时)的文件名冲突报错 * fix: 防止多chunk情况下(有懒加载时)的文件名冲突报错 * Update packages/preset-umi/src/features/ssr/webpack/webpack.ts * feature(SSR): 支持通过导出metadataLoader()设置页面的TDK等meta信息 * feature(SSR): 支持通过导出metadataLoader()设置页面的TDK等meta信息 * feature(SSR): 支持通过导出metadataLoader()设置页面的TDK等meta信息 * feature(SSR): 支持通过导出metadataLoader()设置页面的TDK等meta信息 * feature(SSR): 支持通过导出metadataLoader()设置页面的TDK等meta信息 * chore: export types and format --------- Co-authored-by: 奇风 <[email protected]> Co-authored-by: chencheng (云谦) <[email protected]> Co-authored-by: 咲奈Sakina <[email protected]>
1 parent 84b5ca1 commit e63fed3

File tree

6 files changed

+150
-63
lines changed

6 files changed

+150
-63
lines changed

examples/ssr-demo/src/pages/index.tsx

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
2-
IServerLoaderArgs,
32
Link,
3+
MetadataLoader,
4+
ServerLoader,
45
useClientLoaderData,
56
useServerInsertedHTML,
67
useServerLoaderData,
@@ -51,8 +52,26 @@ export async function clientLoader() {
5152
return { message: 'data from client loader of index.tsx' };
5253
}
5354

54-
export async function serverLoader({ request }: IServerLoaderArgs) {
55-
const { url } = request;
55+
export const serverLoader: ServerLoader = async (req) => {
56+
const url = req!.request.url;
5657
await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000));
5758
return { message: `data from server loader of index.tsx, url: ${url}` };
58-
}
59+
};
60+
61+
// SEO-设置页面的TDK
62+
export const metadataLoader: MetadataLoader<{ message: string }> = (
63+
serverLoaderData,
64+
) => {
65+
return {
66+
title: '开发者学堂 - 支付宝开放平台',
67+
description: '支付宝小程序开发入门实战经验在线课程,让更多的开发者获得成长',
68+
keywords: ['小程序开发', '入门', '实战', '小程序云'],
69+
lang: 'zh-CN',
70+
metas: [
71+
{
72+
name: 'msg',
73+
content: serverLoaderData.message,
74+
},
75+
],
76+
};
77+
};

packages/preset-umi/src/features/ssr/ssr.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,16 @@ export function useServerInsertedHTML(callback: () => React.ReactNode): void {
105105
api.writeTmpFile({
106106
path: 'types.d.ts',
107107
content: `
108-
export type { IServerLoaderArgs, UmiRequest } from '${winPath(ssrTypesPath)}'
108+
export type {
109+
// server loader
110+
IServerLoaderArgs,
111+
UmiRequest,
112+
ServerLoader,
113+
// metadata loader
114+
MetadataLoader,
115+
IMetadata,
116+
IMetaTag,
117+
} from '${winPath(ssrTypesPath)}'
109118
`,
110119
});
111120
});

packages/preset-umi/src/features/tmpFiles/routes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export async function getRoutes(opts: {
165165
: [];
166166
if (enableSSR) {
167167
routes[id].hasServerLoader = exports.includes('serverLoader');
168+
routes[id].hasMetadataLoader = exports.includes('metadataLoader');
168169
}
169170
if (enableClientLoader && exports.includes('clientLoader')) {
170171
routes[id].clientLoader = `clientLoaders['${id}']`;

packages/renderer-react/src/server.tsx

+25-25
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1+
import type { IMetadata } from '@umijs/server/dist/types';
12
import React from 'react';
23
import { StaticRouter } from 'react-router-dom/server';
34
import { AppContext } from './appContext';
45
import { Routes } from './browser';
56
import { createClientRoutes } from './routes';
67
import { IRouteComponents, IRoutesById } from './types';
78

8-
// Get the root React component for ReactDOMServer.renderToString
9-
export async function getClientRootComponent(opts: {
9+
interface IHtmlProps {
1010
routes: IRoutesById;
1111
routeComponents: IRouteComponents;
1212
pluginManager: any;
1313
location: string;
1414
loaderData: { [routeKey: string]: any };
1515
manifest: any;
16-
withoutHTML?: boolean;
17-
}) {
16+
metadata?: IMetadata;
17+
}
18+
19+
// Get the root React component for ReactDOMServer.renderToString
20+
export async function getClientRootComponent(opts: IHtmlProps) {
1821
const basename = '/';
1922
const components = { ...opts.routeComponents };
2023
const clientRoutes = createClientRoutes({
@@ -57,36 +60,33 @@ export async function getClientRootComponent(opts: {
5760
{rootContainer}
5861
</AppContext.Provider>
5962
);
60-
if (opts.withoutHTML) {
61-
return (
62-
<>
63-
<div id="root">{app}</div>
64-
<script
65-
dangerouslySetInnerHTML={{
66-
__html: `window.__UMI_LOADER_DATA__ = ${JSON.stringify(
67-
opts.loaderData,
68-
)}`,
69-
}}
70-
/>
71-
</>
72-
);
73-
}
74-
return (
75-
<Html loaderData={opts.loaderData} manifest={opts.manifest}>
76-
{app}
77-
</Html>
78-
);
63+
return <Html {...opts}>{app}</Html>;
7964
}
8065

81-
function Html({ children, loaderData, manifest }: any) {
66+
function Html({
67+
children,
68+
loaderData,
69+
manifest,
70+
metadata,
71+
}: React.PropsWithChildren<IHtmlProps>) {
8272
// TODO: 处理 head 标签,比如 favicon.ico 的一致性
8373
// TODO: root 支持配置
8474

8575
return (
86-
<html lang="en">
76+
<html lang={metadata?.lang || 'en'}>
8777
<head>
8878
<meta charSet="utf-8" />
8979
<meta name="viewport" content="width=device-width, initial-scale=1" />
80+
{metadata?.title && <title>{metadata.title}</title>}
81+
{metadata?.description && (
82+
<meta name="description" content={metadata.description} />
83+
)}
84+
{metadata?.keywords?.length && (
85+
<meta name="keywords" content={metadata.keywords.join(',')} />
86+
)}
87+
{metadata?.metas?.map((em) => (
88+
<meta key={em.name} name={em.name} content={em.content} />
89+
))}
9090
{manifest.assets['umi.css'] && (
9191
<link rel="stylesheet" href={manifest.assets['umi.css']} />
9292
)}

packages/server/src/ssr.ts

+67-29
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import React, { ReactElement } from 'react';
22
import * as ReactDomServer from 'react-dom/server';
33
import { matchRoutes } from 'react-router-dom';
44
import { Writable } from 'stream';
5-
import type { IRoutesById, IServerLoaderArgs, UmiRequest } from './types';
5+
import type {
6+
IRoutesById,
7+
IServerLoaderArgs,
8+
MetadataLoader,
9+
ServerLoader,
10+
UmiRequest,
11+
} from './types';
612

713
interface RouteLoaders {
814
[key: string]: () => Promise<any>;
@@ -11,11 +17,6 @@ interface RouteLoaders {
1117
export type ServerInsertedHTMLHook = (callbacks: () => React.ReactNode) => void;
1218

1319
interface CreateRequestServerlessOptions {
14-
/**
15-
* only return body html
16-
* @example <div id="root">{app}</div> ...
17-
*/
18-
withoutHTML?: boolean;
1920
/**
2021
* folder path for `build-manifest.json`
2122
*/
@@ -37,6 +38,16 @@ interface CreateRequestHandlerOptions extends CreateRequestServerlessOptions {
3738
ServerInsertedHTMLContext: React.Context<ServerInsertedHTMLHook | null>;
3839
}
3940

41+
interface IExecLoaderOpts {
42+
routeKey: string;
43+
routesWithServerLoader: RouteLoaders;
44+
serverLoaderArgs?: IServerLoaderArgs;
45+
}
46+
47+
interface IExecMetaLoaderOpts extends IExecLoaderOpts {
48+
serverLoaderData?: any;
49+
}
50+
4051
const createJSXProvider = (
4152
Provider: any,
4253
serverInsertedHTMLCallbacks: Set<() => React.ReactNode>,
@@ -93,18 +104,33 @@ function createJSXGenerator(opts: CreateRequestHandlerOptions) {
93104
return;
94105
}
95106

96-
const loaderData: { [key: string]: any } = {};
107+
const loaderData: Record<string, any> = {};
108+
const metadata: Record<string, any> = {};
97109
await Promise.all(
98110
matches
99111
.filter((id: string) => routes[id].hasServerLoader)
100112
.map(
101113
(id: string) =>
102114
new Promise<void>(async (resolve) => {
103-
loaderData[id] = await executeLoader(
104-
id,
115+
loaderData[id] = await executeLoader({
116+
routeKey: id,
105117
routesWithServerLoader,
106118
serverLoaderArgs,
107-
);
119+
});
120+
// 如果有metadataLoader,执行metadataLoader
121+
// metadataLoader在serverLoader返回之后执行这样metadataLoader可以使用serverLoader的返回值
122+
// 如果有多层嵌套路由和合并多层返回的metadata但最里层的优先级最高
123+
if (routes[id].hasMetadataLoader) {
124+
Object.assign(
125+
metadata,
126+
await executeMetadataLoader({
127+
routesWithServerLoader,
128+
routeKey: id,
129+
serverLoaderArgs,
130+
serverLoaderData: loaderData[id],
131+
}),
132+
);
133+
}
108134
resolve();
109135
}),
110136
),
@@ -121,7 +147,7 @@ function createJSXGenerator(opts: CreateRequestHandlerOptions) {
121147
location: url,
122148
manifest,
123149
loaderData,
124-
withoutHTML: opts.withoutHTML,
150+
metadata,
125151
};
126152

127153
const element = (await opts.getClientRootComponent(
@@ -219,14 +245,11 @@ export default function createRequestHandler(
219245
return async function (req: any, res: any, next: any) {
220246
// 切换路由场景下,会通过此 API 执行 server loader
221247
if (req.url.startsWith('/__serverLoader') && req.query.route) {
222-
const loaderArgs: IServerLoaderArgs = {
223-
request: req,
224-
};
225-
const data = await executeLoader(
226-
req.query.route,
227-
opts.routesWithServerLoader,
228-
loaderArgs,
229-
);
248+
const data = await executeLoader({
249+
routeKey: req.query.route,
250+
routesWithServerLoader: opts.routesWithServerLoader,
251+
serverLoaderArgs: { request: req },
252+
});
230253
res.status(200).json(data);
231254
return;
232255
}
@@ -293,10 +316,11 @@ export function createUmiServerLoader(opts: CreateRequestHandlerOptions) {
293316
return async function (req: UmiRequest) {
294317
const query = Object.fromEntries(new URL(req.url).searchParams);
295318
// 切换路由场景下,会通过此 API 执行 server loader
296-
const loaderArgs: IServerLoaderArgs = {
297-
request: req,
298-
};
299-
return executeLoader(query.route, opts.routesWithServerLoader, loaderArgs);
319+
return await executeLoader({
320+
routeKey: query.route,
321+
routesWithServerLoader: opts.routesWithServerLoader,
322+
serverLoaderArgs: { request: req },
323+
});
300324
};
301325
}
302326

@@ -335,15 +359,29 @@ function createClientRoute(route: any) {
335359
};
336360
}
337361

338-
async function executeLoader(
339-
routeKey: string,
340-
routesWithServerLoader: RouteLoaders,
341-
serverLoaderArgs?: IServerLoaderArgs,
342-
) {
362+
async function executeLoader(params: IExecLoaderOpts) {
363+
const { routeKey, routesWithServerLoader, serverLoaderArgs } = params;
343364
const mod = await routesWithServerLoader[routeKey]();
344365
if (!mod.serverLoader || typeof mod.serverLoader !== 'function') {
345366
return;
346367
}
347368
// TODO: 处理错误场景
348-
return mod.serverLoader(serverLoaderArgs);
369+
return (mod.serverLoader satisfies ServerLoader)(serverLoaderArgs);
370+
}
371+
372+
async function executeMetadataLoader(params: IExecMetaLoaderOpts) {
373+
const {
374+
routesWithServerLoader,
375+
routeKey,
376+
serverLoaderArgs,
377+
serverLoaderData,
378+
} = params;
379+
const mod = await routesWithServerLoader[routeKey]();
380+
if (!mod.serverLoader || typeof mod.serverLoader !== 'function') {
381+
return;
382+
}
383+
return (mod.metadataLoader satisfies MetadataLoader)(
384+
serverLoaderData,
385+
serverLoaderArgs,
386+
);
349387
}

packages/server/src/types.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,31 @@ export interface IRouteCustom extends IRoute {
1313
[key: string]: any;
1414
}
1515

16-
export type UmiRequest = Partial<Request> & Pick<Request, 'url' | 'headers'>;
16+
type LoaderReturn<T> = T | Promise<T>;
1717

18-
/**
19-
* serverLoader 的参数类型
20-
*/
18+
export type UmiRequest = Partial<Request> & Pick<Request, 'url' | 'headers'>;
2119
export interface IServerLoaderArgs {
2220
request: UmiRequest;
2321
}
22+
export type ServerLoader<T = any> = (
23+
req?: IServerLoaderArgs,
24+
) => LoaderReturn<T>;
25+
26+
export interface IMetaTag {
27+
name: string;
28+
content: string;
29+
}
30+
export interface IMetadata {
31+
title?: string;
32+
description?: string;
33+
keywords?: string[];
34+
/**
35+
* @default 'en'
36+
*/
37+
lang?: string;
38+
metas?: IMetaTag[];
39+
}
40+
export type MetadataLoader<T = any> = (
41+
serverLoaderData: T,
42+
req?: IServerLoaderArgs,
43+
) => LoaderReturn<IMetadata>;

0 commit comments

Comments
 (0)