From cf394d21b3bcb81cc07f6b184a3b115230384362 Mon Sep 17 00:00:00 2001 From: PeachScript Date: Mon, 25 Mar 2024 21:46:04 +0800 Subject: [PATCH] refactor(preset-umi): provide unified request handler for ssr --- packages/preset-umi/templates/server.tpl | 6 + packages/server/src/ssr.ts | 199 ++++++++++++++++++----- packages/server/tsconfig.json | 3 +- 3 files changed, 165 insertions(+), 43 deletions(-) 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 7fb7aba4ece2..94535fc94afb 100644 --- a/packages/server/src/ssr.ts +++ b/packages/server/src/ssr.ts @@ -238,66 +238,178 @@ export function createMarkupGenerator(opts: CreateRequestHandlerOptions) { }; } +type IExpressRequestHandlerArgs = Parameters; +type IWorkerRequestHandlerArgs = [ + ev: FetchEvent, + opts?: { modifyResponse?: (res: Response) => Response }, +]; + export default function createRequestHandler( opts: CreateRequestHandlerOptions, -): RequestHandler { +) { 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; + }; + + if (args.length !== 3) { + // 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); + } + + 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, { 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(), + }, + }, + sendServerLoader(data) { + res.status(200).json(data); + }, + async sendPage(jsx) { + const writable = new Writable(); + + 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, + ); - return async function (req, res, next) { - // 切换路由场景下,会通过此 API 执行 server loader if ( - req.url.startsWith('/__serverLoader') && + req.pathname.startsWith('/__serverLoader') && req.query.route && req.query.url ) { - // 在浏览器中触发的__serverLoader请求的request应该和SSR时拿到的request一致,都是当前页面的URL - // 否则会导致serverLoader中的request.url和SSR时拿到的request.url不一致 - // 进而导致浏览器中触发的__serverLoader请求传入的参数和SSR时拿到的参数不一致,导致数据不一致 - const serverLoaderRequest = new Request(req.query.url.toString(), { - headers: req.headers as HeadersInit, + // 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, }); const data = await executeLoader({ - routeKey: req.query.route.toString(), + routeKey: req.query.route, 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 as HeadersInit, - }); - 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(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); - }, - }); + 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({ ..., mode: 'worker' })` instead + */ export function createUmiHandler(opts: CreateRequestHandlerOptions) { return async function ( req: UmiRequest, @@ -323,6 +435,9 @@ export function createUmiHandler(opts: CreateRequestHandlerOptions) { }; } +/** + * @deprecated Please use `createRequestHandler({ ..., mode: 'worker' })` instead + */ export function createUmiServerLoader(opts: CreateRequestHandlerOptions) { return async function (req: UmiRequest) { const query = Object.fromEntries(new URL(req.url).searchParams); diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 792172fb82f6..39a2b0b24629 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "lib": ["WebWorker"] }, "include": ["src"] }