diff --git a/packages/preset-umi/templates/server.tpl b/packages/preset-umi/templates/server.tpl
index 13be23d98838..e01021b9836a 100644
--- a/packages/preset-umi/templates/server.tpl
+++ b/packages/preset-umi/templates/server.tpl
@@ -53,7 +53,13 @@ const createOpts = {
ServerInsertedHTMLContext,
};
const requestHandler = createRequestHandler(createOpts);
+/**
+ * @deprecated Please use `requestHandler` instead.
+ */
export const renderRoot = createUmiHandler(createOpts);
+/**
+ * @deprecated Please use `requestHandler` instead.
+ */
export const serverLoader = createUmiServerLoader(createOpts);
export const _markupGenerator = createMarkupGenerator(createOpts);
diff --git a/packages/server/src/ssr.ts b/packages/server/src/ssr.ts
index ba26f6f55ccb..d25f0c7ceb30 100644
--- a/packages/server/src/ssr.ts
+++ b/packages/server/src/ssr.ts
@@ -1,3 +1,5 @@
+///
+import type { RequestHandler } from '@umijs/bundler-utils/compiled/express';
import React, { ReactElement } from 'react';
import * as ReactDomServer from 'react-dom/server';
import { matchRoutes } from 'react-router-dom';
@@ -237,17 +239,156 @@ export function createMarkupGenerator(opts: CreateRequestHandlerOptions) {
};
}
+type IExpressRequestHandlerArgs = Parameters;
+type IWorkerRequestHandlerArgs = [
+ ev: FetchEvent,
+ opts?: { modifyResponse?: (res: Response) => Promise | Response },
+];
+
export default function createRequestHandler(
opts: CreateRequestHandlerOptions,
) {
const jsxGeneratorDeferrer = createJSXGenerator(opts);
+ const normalizeHandlerArgs = (
+ ...args: IExpressRequestHandlerArgs | IWorkerRequestHandlerArgs
+ ) => {
+ let ret: {
+ req: {
+ url: string;
+ pathname: string;
+ headers: HeadersInit;
+ query: { route?: string | null; url?: string | null };
+ };
+ sendServerLoader(data: any): Promise | void;
+ sendPage(
+ jsx: NonNullable>>,
+ ): Promise | void;
+ otherwise(): Promise | void;
+ };
- return async function (req: any, res: any, next: any) {
- // 切换路由场景下,会通过此 API 执行 server loader
- if (req.url.startsWith('/__serverLoader') && req.query.route) {
- // 在浏览器中触发的__serverLoader请求的request应该和SSR时拿到的request一致,都是当前页面的URL
- // 否则会导致serverLoader中的request.url和SSR时拿到的request.url不一致
- // 进而导致浏览器中触发的__serverLoader请求传入的参数和SSR时拿到的参数不一致,导致数据不一致
+ if (typeof FetchEvent !== 'undefined' && args[0] instanceof FetchEvent) {
+ // worker mode
+ const [ev, opts] = args as IWorkerRequestHandlerArgs;
+ const { pathname, searchParams } = new URL(ev.request.url);
+
+ ret = {
+ req: {
+ url: ev.request.url,
+ pathname,
+ headers: ev.request.headers,
+ query: {
+ route: searchParams.get('route'),
+ url: searchParams.get('url'),
+ },
+ },
+ async sendServerLoader(data) {
+ let res = new Response(JSON.stringify(data), {
+ headers: {
+ 'content-type': 'application/json; charset=utf-8',
+ },
+ status: 200,
+ });
+
+ // allow modify response
+ if (opts?.modifyResponse) {
+ res = await opts.modifyResponse(res);
+ }
+
+ ev.respondWith(res);
+ },
+ async sendPage(jsx) {
+ // handle route path request
+ const stream = await ReactDomServer.renderToReadableStream(
+ jsx.element,
+ {
+ bootstrapScripts: [jsx.manifest.assets['umi.js'] || '/umi.js'],
+ onError(x: any) {
+ console.error(x);
+ },
+ },
+ );
+ let res = new Response(stream, {
+ headers: {
+ 'content-type': 'text/html; charset=utf-8',
+ },
+ status: 200,
+ });
+
+ // allow modify response
+ if (opts?.modifyResponse) {
+ res = await opts.modifyResponse(res);
+ }
+
+ ev.respondWith(res);
+ },
+ otherwise() {
+ throw new Error('no page resource');
+ },
+ };
+ } else {
+ // express mode
+ const [req, res, next] = args as IExpressRequestHandlerArgs;
+
+ ret = {
+ req: {
+ url: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
+ pathname: req.url,
+ headers: req.headers as HeadersInit,
+ query: {
+ route: req.query.route?.toString(),
+ url: req.query.url?.toString(),
+ },
+ },
+ sendServerLoader(data) {
+ res.status(200).json(data);
+ },
+ async sendPage(jsx) {
+ const writable = new Writable();
+
+ res.type('html');
+
+ writable._write = (chunk, _encoding, cb) => {
+ res.write(chunk);
+ cb();
+ };
+
+ writable.on('finish', async () => {
+ res.write(getGenerateStaticHTML());
+ res.end();
+ });
+
+ const stream = ReactDomServer.renderToPipeableStream(jsx.element, {
+ bootstrapScripts: [jsx.manifest.assets['umi.js'] || '/umi.js'],
+ onShellReady() {
+ stream.pipe(writable);
+ },
+ onError(x: any) {
+ console.error(x);
+ },
+ });
+ },
+ otherwise: next,
+ };
+ }
+
+ return ret;
+ };
+
+ return async function unifiedRequestHandler(
+ ...args: IExpressRequestHandlerArgs | IWorkerRequestHandlerArgs
+ ) {
+ let jsx;
+ const { req, sendServerLoader, sendPage, otherwise } = normalizeHandlerArgs(
+ ...args,
+ );
+
+ if (
+ req.pathname.startsWith('/__serverLoader') &&
+ req.query.route &&
+ req.query.url
+ ) {
+ // handle server loader request when route change or csr fallback
+ // provide the same request as real SSR, so that the server loader can get the same data
const serverLoaderRequest = new Request(req.query.url, {
headers: req.headers,
});
@@ -256,48 +397,38 @@ export default function createRequestHandler(
routesWithServerLoader: opts.routesWithServerLoader,
serverLoaderArgs: { request: serverLoaderRequest },
});
- res.status(200).json(data);
- return;
- }
-
- const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
- const request = new Request(fullUrl, {
- headers: req.headers,
- });
- const jsx = await jsxGeneratorDeferrer(req.url, { request });
-
- if (!jsx) return next();
-
- const writable = new Writable();
- writable._write = (chunk, _encoding, next) => {
- res.write(chunk);
- next();
- };
-
- writable.on('finish', async () => {
- res.write(await getGenerateStaticHTML());
- res.end();
- });
-
- const stream = await ReactDomServer.renderToPipeableStream(jsx.element, {
- bootstrapScripts: [jsx.manifest.assets['umi.js'] || '/umi.js'],
- onShellReady() {
- stream.pipe(writable);
- },
- onError(x: any) {
- console.error(x);
- },
- });
+ await sendServerLoader(data);
+ } else if (
+ (jsx = await jsxGeneratorDeferrer(req.pathname, {
+ request: new Request(req.url, {
+ headers: req.headers,
+ }),
+ }))
+ ) {
+ // response route page
+ await sendPage(jsx);
+ } else {
+ await otherwise();
+ }
};
}
// 新增的给CDN worker用的SSR请求handle
export function createUmiHandler(opts: CreateRequestHandlerOptions) {
+ let isWarned = false;
+
return async function (
req: UmiRequest,
params?: CreateRequestHandlerOptions,
) {
+ if (!isWarned) {
+ console.warn(
+ '[umi] `renderRoot` is deprecated, please use `requestHandler` instead',
+ );
+ isWarned = true;
+ }
+
const jsxGeneratorDeferrer = createJSXGenerator({
...opts,
...params,
@@ -319,7 +450,16 @@ export function createUmiHandler(opts: CreateRequestHandlerOptions) {
}
export function createUmiServerLoader(opts: CreateRequestHandlerOptions) {
+ let isWarned = false;
+
return async function (req: UmiRequest) {
+ if (!isWarned) {
+ console.warn(
+ '[umi] `serverLoader` is deprecated, please use `requestHandler` instead',
+ );
+ isWarned = true;
+ }
+
const query = Object.fromEntries(new URL(req.url).searchParams);
// 切换路由场景下,会通过此 API 执行 server loader
const serverLoaderRequest = new Request(query.url, {