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(preset-umi): unify request handler for ssr and always use stream #12229

Merged
merged 9 commits into from
Mar 28, 2024
6 changes: 6 additions & 0 deletions packages/preset-umi/templates/server.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
204 changes: 166 additions & 38 deletions packages/server/src/ssr.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/// <reference lib="webworker" />
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';
Expand Down Expand Up @@ -237,17 +239,156 @@ export function createMarkupGenerator(opts: CreateRequestHandlerOptions) {
};
}

type IExpressRequestHandlerArgs = Parameters<RequestHandler>;
type IWorkerRequestHandlerArgs = [
ev: FetchEvent,
fz6m marked this conversation as resolved.
Show resolved Hide resolved
opts?: { modifyResponse?: (res: Response) => 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> | void;
sendPage(
jsx: NonNullable<Awaited<ReturnType<typeof jsxGeneratorDeferrer>>>,
): Promise<void> | void;
otherwise(): Promise<void> | 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 (args.length !== 3) {
PeachScript marked this conversation as resolved.
Show resolved Hide resolved
// worker mode
const [ev, opts] = args;
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'),
},
},
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 = opts.modifyResponse(res);
PeachScript marked this conversation as resolved.
Show resolved Hide resolved
}

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 = opts.modifyResponse(res);
}

ev.respondWith(res);
},
otherwise() {
throw new Error('no page resource');
},
};
} else {
// express mode
const [req, res, next] = args;

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(),
},
PeachScript marked this conversation as resolved.
Show resolved Hide resolved
},
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,
});
Expand All @@ -256,43 +397,27 @@ 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
/**
* @deprecated Please use `createRequestHandler` instead
*/
export function createUmiHandler(opts: CreateRequestHandlerOptions) {
return async function (
req: UmiRequest,
Expand All @@ -318,6 +443,9 @@ export function createUmiHandler(opts: CreateRequestHandlerOptions) {
};
}

/**
* @deprecated Please use `createRequestHandler` instead
*/
export function createUmiServerLoader(opts: CreateRequestHandlerOptions) {
PeachScript marked this conversation as resolved.
Show resolved Hide resolved
return async function (req: UmiRequest) {
const query = Object.fromEntries(new URL(req.url).searchParams);
Expand Down
Loading