diff --git a/examples/ssr-demo/.umirc.ts b/examples/ssr-demo/.umirc.ts index 7f04c0edcf9b..3226de45a815 100644 --- a/examples/ssr-demo/.umirc.ts +++ b/examples/ssr-demo/.umirc.ts @@ -7,20 +7,8 @@ export default { clientLoader: {}, ssr: {}, exportStatic: {}, - // mako: {}, - // mako: { - // plugins: [ - // { - // load: () => {}, - // }, - // ], - // }, - // ssr: { - // // builder: 'mako', - // }, - // exportStatic: {}, styles: [`body { color: red; }`], - + // mako: {}, metas: [ { name: 'test', diff --git a/examples/ssr-demo/src/pages/index.tsx b/examples/ssr-demo/src/pages/index.tsx index 2d3d2fb2f2b8..a86932621d0b 100644 --- a/examples/ssr-demo/src/pages/index.tsx +++ b/examples/ssr-demo/src/pages/index.tsx @@ -21,7 +21,6 @@ import styles from './index.less'; // @ts-ignore import umiLogo from './umi.png'; // @ts-ignore -import styles1 from './a.module.css'; export default function HomePage() { const clientLoaderData = useClientLoaderData(); @@ -42,7 +41,7 @@ export default function HomePage() { return (
-

Hello~1

+

Hello~1

id: {id}

This is index.tsx

I should be pink

diff --git a/packages/preset-umi/src/features/ssr/ssr.ts b/packages/preset-umi/src/features/ssr/ssr.ts index 22d0da952d32..3409ad7c554e 100644 --- a/packages/preset-umi/src/features/ssr/ssr.ts +++ b/packages/preset-umi/src/features/ssr/ssr.ts @@ -37,6 +37,7 @@ export default (api: IApi) => { pureApp: zod.boolean(), pureHtml: zod.boolean(), }), + useStream: zod.boolean().default(true), }) .deepPartial(); }, diff --git a/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts b/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts index f75e00c4e4bc..aff9e9b3cfc9 100644 --- a/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts +++ b/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts @@ -301,6 +301,7 @@ declare module '*.txt' { }), ).join('\n'); + const ssrConfig = api.config.ssr; const __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = api.config.ssr ?.__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ?? { pureApp: false, @@ -331,7 +332,8 @@ declare module '*.txt' { __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: JSON.stringify( __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, ), - hydrate: !!api.config.ssr, + hydrate: !!ssrConfig, + useStream: ssrConfig?.useStream ?? true, reactRouter5Compat: !!api.config.reactRouter5Compat, loadingComponent: api.appData.globalLoading, }, @@ -506,7 +508,7 @@ if (process.env.NODE_ENV === 'development') { }); // umi.server.ts - if (api.config.ssr) { + if (ssrConfig) { const umiPluginPath = winPath(join(umiDir, 'client/client/plugin.js')); const umiServerPath = winPath(require.resolve('@umijs/server/dist/ssr')); @@ -563,6 +565,7 @@ if (process.env.NODE_ENV === 'development') { ), mountElementId, basename: api.config.base, + useStream: ssrConfig?.useStream ?? true, }, }); } diff --git a/packages/preset-umi/templates/server.tpl b/packages/preset-umi/templates/server.tpl index 1c17334cb48a..f3c08393018c 100644 --- a/packages/preset-umi/templates/server.tpl +++ b/packages/preset-umi/templates/server.tpl @@ -62,7 +62,8 @@ const createOpts = { htmlPageOpts: {{{htmlPageOpts}}}, __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {{{__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED}}}, mountElementId: '{{{mountElementId}}}', - basename: '{{{basename}}}' + basename: '{{{basename}}}', + useStream: {{{useStream}}} }; const requestHandler = createRequestHandler(createOpts); /** diff --git a/packages/preset-umi/templates/umi.tpl b/packages/preset-umi/templates/umi.tpl index 8df0f745fe53..0ee335e6479d 100644 --- a/packages/preset-umi/templates/umi.tpl +++ b/packages/preset-umi/templates/umi.tpl @@ -50,6 +50,7 @@ async function render() { {{#hydrate}} hydrate: true, {{/hydrate}} + useStream: {{{useStream}}}, {{#reactRouter5Compat}} reactRouter5Compat: true, {{/reactRouter5Compat}} diff --git a/packages/renderer-react/src/browser.tsx b/packages/renderer-react/src/browser.tsx index 2c90cb6a738c..116d60d1b2be 100644 --- a/packages/renderer-react/src/browser.tsx +++ b/packages/renderer-react/src/browser.tsx @@ -145,6 +145,11 @@ export type RenderClientOpts = { */ hydrate?: boolean; + /** + * ssr 是否启用流式渲染, 默认 true, 对 SEO 存在一定的负优化 + */ + useStream?: boolean; + /** * 直接返回组件,是为了方便测试 */ @@ -188,6 +193,7 @@ const getBrowser = ( routeComponents: opts.routeComponents, loadingComponent: opts.loadingComponent, reactRouter5Compat: opts.reactRouter5Compat, + useStream: opts.useStream, }); opts.pluginManager.applyPlugins({ key: 'patchClientRoutes', diff --git a/packages/renderer-react/src/routes.tsx b/packages/renderer-react/src/routes.tsx index 709499468b5b..0cbbdc4ff177 100644 --- a/packages/renderer-react/src/routes.tsx +++ b/packages/renderer-react/src/routes.tsx @@ -17,8 +17,9 @@ export function createClientRoutes(opts: { parentId?: string; loadingComponent?: React.ReactNode; reactRouter5Compat?: boolean; + useStream?: boolean; }) { - const { routesById, parentId, routeComponents } = opts; + const { routesById, parentId, routeComponents, useStream = true } = opts; return Object.keys(routesById) .filter((id) => routesById[id].parentId === parentId) .map((id) => { @@ -34,6 +35,7 @@ export function createClientRoutes(opts: { (rid) => routesById[rid].parentId === id, ).length > 0, }), + useStream, }); const children = createClientRoutes({ routesById, @@ -41,6 +43,7 @@ export function createClientRoutes(opts: { parentId: route.id, loadingComponent: opts.loadingComponent, reactRouter5Compat: opts.reactRouter5Compat, + useStream, }); if (children.length > 0) { route.children = children; @@ -74,8 +77,9 @@ function createClientRoute(opts: { loadingComponent?: React.ReactNode; hasChildren?: boolean; reactRouter5Compat?: boolean; + useStream?: boolean; }): IClientRoute { - const { route } = opts; + const { route, useStream = true } = opts; const { redirect, ...props } = route; const Remote = opts.reactRouter5Compat ? RemoteComponentReactRouter5 @@ -93,6 +97,7 @@ function createClientRoute(opts: { loader={React.memo(opts.routeComponent)} loadingComponent={opts.loadingComponent || DefaultLoading} hasChildren={opts.hasChildren} + useStream={useStream} /> ), @@ -117,28 +122,33 @@ function RemoteComponentReactRouter5(props: any) { // staticContext 没有兼容 好像没看到对应的兼容写法 const Component = props.loader; - - return ( + const ComponentProps = { + location: history.location, + match, + history, + params, + route, + routes: clientRoutes, + }; + const Remote = () => ( + {props.hasChildren && } + ); + return props.useStream ? ( }> - - {props.hasChildren && } - + + ) : ( + ); } function RemoteComponent(props: any) { const Component = props.loader; - return ( + return props.useStream ? ( }> + ) : ( + ); } diff --git a/packages/renderer-react/src/server.tsx b/packages/renderer-react/src/server.tsx index e10690e9f962..86e1521de732 100644 --- a/packages/renderer-react/src/server.tsx +++ b/packages/renderer-react/src/server.tsx @@ -14,6 +14,7 @@ export async function getClientRootComponent(opts: IRootComponentOptions) { const clientRoutes = createClientRoutes({ routesById: opts.routes, routeComponents: components, + useStream: opts.useStream, }); opts.pluginManager.applyPlugins({ diff --git a/packages/renderer-react/src/types.ts b/packages/renderer-react/src/types.ts index c7c05b6a59f9..fa0e2e0d2648 100644 --- a/packages/renderer-react/src/types.ts +++ b/packages/renderer-react/src/types.ts @@ -78,6 +78,7 @@ export interface IRootComponentOptions extends IHtmlHydrateOptions { loaderData: { [routeKey: string]: any }; manifest: any; basename?: string; + useStream?: boolean; } export interface IHtmlProps extends IHtmlHydrateOptions { diff --git a/packages/server/src/ssr.ts b/packages/server/src/ssr.ts index 75c9c8fdd5be..5281b2e1d969 100644 --- a/packages/server/src/ssr.ts +++ b/packages/server/src/ssr.ts @@ -55,6 +55,7 @@ interface CreateRequestHandlerOptions extends CreateRequestServerlessOptions { }; mountElementId: string; basename?: string; + useStream?: boolean; } interface IExecLoaderOpts { @@ -173,6 +174,7 @@ function createJSXGenerator(opts: CreateRequestHandlerOptions) { opts.__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, mountElementId: opts.mountElementId, basename, + useStream: opts.useStream, }; const element = (await opts.getClientRootComponent( context, @@ -229,7 +231,7 @@ export function createMarkupGenerator(opts: CreateRequestHandlerOptions) { next(); }; writable.on('finish', async () => { - let html = Buffer.concat(chunks).toString('utf8'); + let html = Buffer.concat(chunks as any).toString('utf8'); const serverHTML = getGenerateStaticHTML(serverInsertedHTMLCallbacks); if (serverHTML) { html = html.replace(/<\/head>/, `${serverHTML}`);