Skip to content

Commit

Permalink
refactor(preset-umi): provide unified request handler for ssr
Browse files Browse the repository at this point in the history
  • Loading branch information
PeachScript committed Mar 25, 2024
1 parent c75626a commit cf394d2
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 43 deletions.
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
199 changes: 157 additions & 42 deletions packages/server/src/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,66 +238,178 @@ export function createMarkupGenerator(opts: CreateRequestHandlerOptions) {
};
}

type IExpressRequestHandlerArgs = Parameters<RequestHandler>;
type IWorkerRequestHandlerArgs = [
ev: FetchEvent,

Check failure on line 243 in packages/server/src/ssr.ts

View workflow job for this annotation

GitHub Actions / build (14.x, ubuntu-latest)

Cannot find name 'FetchEvent'. Did you mean 'TouchEvent'?

Check failure on line 243 in packages/server/src/ssr.ts

View workflow job for this annotation

GitHub Actions / build (16.x, ubuntu-latest)

Cannot find name 'FetchEvent'. Did you mean 'TouchEvent'?

Check failure on line 243 in packages/server/src/ssr.ts

View workflow job for this annotation

GitHub Actions / build (16.x, windows-latest)

Cannot find name 'FetchEvent'. Did you mean 'TouchEvent'?

Check failure on line 243 in packages/server/src/ssr.ts

View workflow job for this annotation

GitHub Actions / build (18.x, ubuntu-latest)

Cannot find name 'FetchEvent'. Did you mean 'TouchEvent'?

Check failure on line 243 in packages/server/src/ssr.ts

View workflow job for this annotation

GitHub Actions / build (18.x, windows-latest)

Cannot find name 'FetchEvent'. Did you mean 'TouchEvent'?
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> | void;
sendPage(
jsx: NonNullable<Awaited<ReturnType<typeof jsxGeneratorDeferrer>>>,
): Promise<void> | void;
otherwise(): Promise<void> | 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,
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion packages/server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
"rootDir": "./src",
"lib": ["WebWorker"]
},
"include": ["src"]
}

0 comments on commit cf394d2

Please sign in to comment.