Skip to content

Commit

Permalink
feat: merge
Browse files Browse the repository at this point in the history
  • Loading branch information
Jinbao1001 committed Mar 29, 2024
2 parents 024729a + a1e5cee commit f3ad7f5
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 39 deletions.
1 change: 1 addition & 0 deletions examples/ssr-demo/.umirc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export default {
svgr: {},
hash: true,
mfsu: false,
routePrefetch: {},
manifest: {},
clientLoader: {},
Expand Down
6 changes: 6 additions & 0 deletions packages/preset-umi/templates/server.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ const createOpts = {

};
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
2 changes: 1 addition & 1 deletion packages/renderer-react/src/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ export function renderClient(opts: RenderClientOpts) {
if (opts.components) return Browser;
if (opts.hydrate) {
ReactDOM.hydrateRoot(
document.querySelector('html')!,
document,
<Html {...opts}>
<Browser />
</Html>,
Expand Down
216 changes: 178 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 mergeWith from 'lodash.mergewith';
import React, { ReactElement } from 'react';
import * as ReactDomServer from 'react-dom/server';
Expand Down Expand Up @@ -251,17 +253,156 @@ export function createMarkupGenerator(opts: CreateRequestHandlerOptions) {
};
}

type IExpressRequestHandlerArgs = Parameters<RequestHandler>;
type IWorkerRequestHandlerArgs = [
ev: FetchEvent,
opts?: { modifyResponse?: (res: Response) => Promise<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 (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,
});
Expand All @@ -270,48 +411,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, callback) => {
res.write(chunk);
callback();
};

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,
Expand All @@ -333,7 +464,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, {
Expand Down

0 comments on commit f3ad7f5

Please sign in to comment.