diff --git a/examples/ssr-demo/src/pages/index.tsx b/examples/ssr-demo/src/pages/index.tsx index 1c1efec02c6d..e5b750b5354c 100644 --- a/examples/ssr-demo/src/pages/index.tsx +++ b/examples/ssr-demo/src/pages/index.tsx @@ -1,5 +1,5 @@ -import React from 'react'; import { + IServerLoaderArgs, Link, useClientLoaderData, useServerInsertedHTML, @@ -18,7 +18,7 @@ import umiLogo from './umi.png'; export default function HomePage() { const clientLoaderData = useClientLoaderData(); - const serverLoaderData = useServerLoaderData(); + const serverLoaderData = useServerLoaderData(); useServerInsertedHTML(() => { return
inserted html
; @@ -51,7 +51,8 @@ export async function clientLoader() { return { message: 'data from client loader of index.tsx' }; } -export async function serverLoader() { +export async function serverLoader({ request }: IServerLoaderArgs) { + const { url } = request; await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000)); - return { message: 'data from server loader of index.tsx' }; + return { message: `data from server loader of index.tsx, url: ${url}` }; } diff --git a/examples/ssr-demo/tsconfig.json b/examples/ssr-demo/tsconfig.json new file mode 100644 index 000000000000..133cfd82a23f --- /dev/null +++ b/examples/ssr-demo/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./src/.umi/tsconfig.json" +} diff --git a/packages/preset-umi/src/features/ssr/ssr.ts b/packages/preset-umi/src/features/ssr/ssr.ts index 2136eb37006b..ec65e3c5971e 100644 --- a/packages/preset-umi/src/features/ssr/ssr.ts +++ b/packages/preset-umi/src/features/ssr/ssr.ts @@ -3,10 +3,10 @@ import type { Compiler, } from '@umijs/bundler-webpack/compiled/webpack'; import { EnableBy } from '@umijs/core/dist/types'; -import { fsExtra, importLazy, logger } from '@umijs/utils'; +import { fsExtra, importLazy, logger, winPath } from '@umijs/utils'; import assert from 'assert'; import { existsSync, writeFileSync } from 'fs'; -import { join } from 'path'; +import { dirname, join } from 'path'; import type { IApi } from '../../types'; import { absServerBuildPath } from './utils'; @@ -61,6 +61,11 @@ export default (api: IApi) => { }, ]); + const serverPackagePath = dirname( + require.resolve('@umijs/server/package.json'), + ); + const ssrTypesPath = join(serverPackagePath, './dist/types'); + api.onGenerateFiles(() => { // react-shim.js is for esbuild to build umi.server.js api.writeTmpFile({ @@ -93,6 +98,14 @@ export function useServerInsertedHTML(callback: () => React.ReactNode): void { addInsertedServerHTMLCallback(callback); } } +`, + }); + + // types + api.writeTmpFile({ + path: 'types.d.ts', + content: ` +export type { IServerLoaderArgs, UmiRequest } from '${winPath(ssrTypesPath)}' `, }); }); diff --git a/packages/preset-umi/src/features/ssr/webpack/webpack.ts b/packages/preset-umi/src/features/ssr/webpack/webpack.ts index 687caf4fbec3..e4a18c190be9 100644 --- a/packages/preset-umi/src/features/ssr/webpack/webpack.ts +++ b/packages/preset-umi/src/features/ssr/webpack/webpack.ts @@ -1,10 +1,10 @@ import * as bundlerWebpack from '@umijs/bundler-webpack'; import type WebpackChain from '@umijs/bundler-webpack/compiled/webpack-5-chain'; +import { Env } from '@umijs/bundler-webpack/dist/types'; import { lodash, logger } from '@umijs/utils'; import { dirname, resolve } from 'path'; import { IApi } from '../../../types'; import { absServerBuildPath } from '../utils'; -import { Env } from "@umijs/bundler-webpack/dist/types"; export const build = async (api: IApi, opts: any) => { logger.wait('[SSR] Compiling...'); @@ -47,7 +47,9 @@ export const build = async (api: IApi, opts: any) => { memo.output .path(dirname(absOutputFile)) // 避免多 chunk 时的命名冲突,虽然 ssr 在项目里禁用了 import() 语法,但 node_modules 下可能存在的 import() 没有被 babel 插件覆盖到 - .filename(useHash ? '[name].[contenthash:8].server.js' : '[name].server.js') + .filename( + useHash ? '[name].[contenthash:8].server.js' : '[name].server.js', + ) .chunkFilename( useHash ? '[name].[contenthash:8].server.js' : '[name].server.js', ) diff --git a/packages/renderer-react/src/appContext.ts b/packages/renderer-react/src/appContext.ts index bf6a79c59ddf..dc7ba72bff54 100644 --- a/packages/renderer-react/src/appContext.ts +++ b/packages/renderer-react/src/appContext.ts @@ -43,10 +43,13 @@ export function useRouteProps = any>() { return props as T; } -export function useServerLoaderData() { +type ServerLoaderFunc = (...args: any[]) => Promise | any; +export function useServerLoaderData() { const route = useRouteData(); const appData = useAppData(); - return { data: appData.serverLoaderData[route.route.id] }; + return { + data: appData.serverLoaderData[route.route.id] as Awaited>, + }; } export function useClientLoaderData() { diff --git a/packages/renderer-react/src/browser.tsx b/packages/renderer-react/src/browser.tsx index 9d36283bfd43..4ad527a96edc 100644 --- a/packages/renderer-react/src/browser.tsx +++ b/packages/renderer-react/src/browser.tsx @@ -266,8 +266,9 @@ const getBrowser = ( // use ?. since routes patched with patchClientRoutes is not exists in opts.routes if (!isFirst && opts.routes[id]?.hasServerLoader) { // 在有basename的情况下__serverLoader的请求路径需要加上basename - fetch((basename.endsWith('/') ? basename : basename + '/') + '__serverLoader?route=' + id, { - credentials: 'include' + const url = `${withEndSlash(basename)}'__serverLoader?route='${id}`; + fetch(url, { + credentials: 'include', }) .then((d) => d.json()) .then((data) => { @@ -350,3 +351,7 @@ export function renderClient(opts: RenderClientOpts) { // @ts-ignore ReactDOM.render(, rootElement); } + +function withEndSlash(str: string) { + return str.endsWith('/') ? str : `${str}/`; +} diff --git a/packages/server/src/ssr.ts b/packages/server/src/ssr.ts index 56398612bdcd..a254026b3b9c 100644 --- a/packages/server/src/ssr.ts +++ b/packages/server/src/ssr.ts @@ -2,7 +2,7 @@ import React, { ReactElement } from 'react'; import * as ReactDomServer from 'react-dom/server'; import { matchRoutes } from 'react-router-dom'; import { Writable } from 'stream'; -import type { IRoutesById } from './types'; +import type { IRoutesById, IServerLoaderArgs, UmiRequest } from './types'; interface RouteLoaders { [key: string]: () => Promise; @@ -10,12 +10,19 @@ interface RouteLoaders { export type ServerInsertedHTMLHook = (callbacks: () => React.ReactNode) => void; -// serverLoader的参数类型 -export interface IServerLoaderArgs { - request: Request; +interface CreateRequestServerlessOptions { + /** + * only return body html + * @example
{app}
... + */ + withoutHTML?: boolean; + /** + * folder path for `build-manifest.json` + */ + sourceDir?: string; } -interface CreateRequestHandlerOptions { +interface CreateRequestHandlerOptions extends CreateRequestServerlessOptions { routesWithServerLoader: RouteLoaders; PluginManager: any; manifest: @@ -28,8 +35,6 @@ interface CreateRequestHandlerOptions { createHistory: (opts: any) => any; helmetContext?: any; ServerInsertedHTMLContext: React.Context; - withoutHTML?: boolean; - sourceDir?: string; } const createJSXProvider = ( @@ -214,21 +219,22 @@ export default function createRequestHandler( return async function (req: any, res: any, next: any) { // 切换路由场景下,会通过此 API 执行 server loader if (req.url.startsWith('/__serverLoader') && req.query.route) { + const loaderArgs: IServerLoaderArgs = { + request: req, + }; const data = await executeLoader( req.query.route, opts.routesWithServerLoader, - { request: req }, + loaderArgs, ); res.status(200).json(data); return; } - const request = new Request( - req.protocol + '://' + req.get('host') + req.originalUrl, - { - headers: req.headers, - }, - ); + 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(); @@ -259,14 +265,21 @@ export default function createRequestHandler( // 新增的给CDN worker用的SSR请求handle export function createUmiHandler(opts: CreateRequestHandlerOptions) { - return async function (req: Request, params?: CreateRequestHandlerOptions) { + return async function ( + req: UmiRequest, + params?: CreateRequestHandlerOptions, + ) { const jsxGeneratorDeferrer = createJSXGenerator({ ...opts, ...params, }); - const jsx = await jsxGeneratorDeferrer(new URL(req.url).pathname, { + const loaderArgs: IServerLoaderArgs = { request: req, - }); + }; + const jsx = await jsxGeneratorDeferrer( + new URL(req.url).pathname, + loaderArgs, + ); if (!jsx) { throw new Error('no page resource'); @@ -277,12 +290,13 @@ export function createUmiHandler(opts: CreateRequestHandlerOptions) { } export function createUmiServerLoader(opts: CreateRequestHandlerOptions) { - return async function (req: Request) { + return async function (req: UmiRequest) { const query = Object.fromEntries(new URL(req.url).searchParams); // 切换路由场景下,会通过此 API 执行 server loader - return await executeLoader(query.route, opts.routesWithServerLoader, { + const loaderArgs: IServerLoaderArgs = { request: req, - }); + }; + return executeLoader(query.route, opts.routesWithServerLoader, loaderArgs); }; } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index f8b498ab6226..8726ab7dd271 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -12,3 +12,12 @@ export interface IRoutesById { export interface IRouteCustom extends IRoute { [key: string]: any; } + +export type UmiRequest = Partial & Pick; + +/** + * serverLoader 的参数类型 + */ +export interface IServerLoaderArgs { + request: UmiRequest; +}