From 8edd52885fecd67ddc72ced1d81104ccdc834424 Mon Sep 17 00:00:00 2001 From: xiaoxiao Date: Wed, 13 Mar 2024 19:37:27 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20ssr=E6=94=AF=E6=8C=81head=20body?= =?UTF-8?q?=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/boilerplate/app.tsx | 1 - examples/ssr-demo/.umirc.ts | 21 ++- .../src/features/exportStatic/exportStatic.ts | 4 +- packages/preset-umi/src/features/ssr/ssr.ts | 1 + .../src/features/tmpFiles/tmpFiles.ts | 20 +++ packages/preset-umi/src/types.ts | 2 + packages/preset-umi/templates/server.tpl | 4 + packages/renderer-react/src/browser.tsx | 16 ++- packages/renderer-react/src/html.tsx | 129 ++++++++++++++++++ packages/renderer-react/src/server.tsx | 70 ++-------- packages/renderer-react/src/types.ts | 31 +++++ packages/server/src/server.ts | 25 +--- packages/server/src/ssr.ts | 36 +++-- packages/server/src/types.ts | 31 +++++ pnpm-lock.yaml | 23 +++- 15 files changed, 304 insertions(+), 110 deletions(-) create mode 100644 packages/renderer-react/src/html.tsx diff --git a/examples/boilerplate/app.tsx b/examples/boilerplate/app.tsx index 244a588ac662..6ebb6b9eb313 100644 --- a/examples/boilerplate/app.tsx +++ b/examples/boilerplate/app.tsx @@ -40,6 +40,5 @@ export function patchClientRoutes({ routes }: any) { // } export const modifyClientRenderOpts = (opts: any) => { - console.log('modifyClientRenderOpts: ', opts); return opts; }; diff --git a/examples/ssr-demo/.umirc.ts b/examples/ssr-demo/.umirc.ts index 18627a8fbbc7..3d46e89b7478 100644 --- a/examples/ssr-demo/.umirc.ts +++ b/examples/ssr-demo/.umirc.ts @@ -1,10 +1,29 @@ export default { svgr: {}, hash: true, + mfsu: false, routePrefetch: {}, manifest: {}, clientLoader: {}, + title: '测试title', + scripts: [`https://a.com/b.js`], ssr: { - serverBuildPath: './umi.server.js', + builder: 'webpack', + hydrateRoot: 'html', }, + styles: [`body { color: red; }`, `https://a.com/b.css`], + + metas: [ + { + name: 'test', + content: 'content', + }, + ], + links: [{ href: '/foo.css', rel: 'preload' }], + + headScripts: [ + { + src: 'https://www.baidu.com', + }, + ], }; diff --git a/packages/preset-umi/src/features/exportStatic/exportStatic.ts b/packages/preset-umi/src/features/exportStatic/exportStatic.ts index c08af14a71b9..a450010bb0ef 100644 --- a/packages/preset-umi/src/features/exportStatic/exportStatic.ts +++ b/packages/preset-umi/src/features/exportStatic/exportStatic.ts @@ -2,7 +2,7 @@ import { getMarkup } from '@umijs/server'; import { lodash, logger, Mustache, winPath } from '@umijs/utils'; import assert from 'assert'; import { dirname, join, relative } from 'path'; -import type { IApi, IRoute } from '../../types'; +import type { IApi, IRoute, IUserExtraRoute } from '../../types'; import { absServerBuildPath } from '../ssr/utils'; let markupRender: any; @@ -17,8 +17,6 @@ interface IExportHtmlItem { prerender: boolean; } -type IUserExtraRoute = string | { path: string; prerender: boolean }; - /** * get export html data from routes */ diff --git a/packages/preset-umi/src/features/ssr/ssr.ts b/packages/preset-umi/src/features/ssr/ssr.ts index d5fa28b509c0..72c7a9039eff 100644 --- a/packages/preset-umi/src/features/ssr/ssr.ts +++ b/packages/preset-umi/src/features/ssr/ssr.ts @@ -27,6 +27,7 @@ export default (api: IApi) => { serverBuildPath: zod.string(), platform: zod.string(), builder: zod.enum(['esbuild', 'webpack']), + hydrateRoot: zod.enum(['html', 'root']), }) .deepPartial(); }, diff --git a/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts b/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts index 1c3a4e2124bb..acafaad12c92 100644 --- a/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts +++ b/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts @@ -496,6 +496,16 @@ if (process.env.NODE_ENV === 'development') { } return memo; }, []); + const { + headScripts, + scripts, + styles, + title, + favicons, + links, + metas, + ssr, + } = api.config; api.writeTmpFile({ noPluginDir: true, path: 'umi.server.ts', @@ -514,6 +524,16 @@ if (process.env.NODE_ENV === 'development') { join(api.paths.absOutputPath, 'build-manifest.json'), ), env: JSON.stringify(api.env), + metaData: JSON.stringify({ + headScripts, + styles, + title, + favicons, + links, + metas, + }), + scripts: JSON.stringify(scripts || []), + hydrateRoot: ssr?.hydrateRoot || 'html', }, }); } diff --git a/packages/preset-umi/src/types.ts b/packages/preset-umi/src/types.ts index cbbadade3f38..d31521f4f13b 100644 --- a/packages/preset-umi/src/types.ts +++ b/packages/preset-umi/src/types.ts @@ -28,6 +28,8 @@ import type CodeFrameError from './features/transform/CodeFrameError'; export { UmiApiRequest, UmiApiResponse } from './features/apiRoute'; export { webpack, IConfig }; +export type IUserExtraRoute = string | { path: string; prerender: boolean }; + export type IScript = | Partial<{ async: boolean; diff --git a/packages/preset-umi/templates/server.tpl b/packages/preset-umi/templates/server.tpl index 13be23d98838..fd2b746a5059 100644 --- a/packages/preset-umi/templates/server.tpl +++ b/packages/preset-umi/templates/server.tpl @@ -51,6 +51,10 @@ const createOpts = { helmetContext, createHistory, ServerInsertedHTMLContext, + metaData: {{{metaData}}}, + scripts: {{{scripts}}}, + hydrateRoot: `{{{hydrateRoot}}}` + }; const requestHandler = createRequestHandler(createOpts); export const renderRoot = createUmiHandler(createOpts); diff --git a/packages/renderer-react/src/browser.tsx b/packages/renderer-react/src/browser.tsx index 2e3adcc7a6f9..c2ab19dc936d 100644 --- a/packages/renderer-react/src/browser.tsx +++ b/packages/renderer-react/src/browser.tsx @@ -10,9 +10,9 @@ import ReactDOM from 'react-dom/client'; import { matchRoutes, Router, useRoutes } from 'react-router-dom'; import { AppContext, useAppData } from './appContext'; import { fetchServerLoader } from './dataFetcher'; +import { Html } from './html'; import { createClientRoutes } from './routes'; import { ILoaderData, IRouteComponents, IRoutesById } from './types'; - let root: ReactDOM.Root | null = null; // react 18 some scenarios need unmount such as micro app @@ -96,6 +96,11 @@ export type RenderClientOpts = { * @doc 一般不需要改,微前端的时候会变化 */ rootElement?: HTMLElement; + /** + * ssr 渲染根节点 + * @doc 默认html, 内部应用使用root + */ + hydrateRoot?: string; /** * 当前的路由配置 */ @@ -334,9 +339,14 @@ export function renderClient(opts: RenderClientOpts) { const Browser = getBrowser(opts, ); // 为了测试,直接返回组件 if (opts.components) return Browser; - if (opts.hydrate) { - ReactDOM.hydrateRoot(rootElement, ); + ReactDOM.hydrateRoot( + document, + + + , + ); + // ReactDOM.hydrateRoot(rootElement, ); return; } diff --git a/packages/renderer-react/src/html.tsx b/packages/renderer-react/src/html.tsx new file mode 100644 index 000000000000..629dbc82ef9d --- /dev/null +++ b/packages/renderer-react/src/html.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { IHtmlProps } from './types'; + +export type IScript = + | Partial<{ + async: boolean; + charset: string; + content: string; + crossOrigin: string | null; + defer: boolean; + src: string; + type: string; + }> + | string; + +const RE_URL = /^(http:|https:)?\/\//; + +function isUrl(str: string) { + return ( + RE_URL.test(str) || + (str.startsWith('/') && !str.startsWith('/*')) || + str.startsWith('./') || + str.startsWith('../') + ); +} + +function genaretorScript(script: IScript, extraProps = {}) { + if (typeof script === 'string') { + return isUrl(script) + ? { + src: script, + ...extraProps, + } + : { content: script }; + } else if (typeof script === 'object') { + return { + ...script, + ...extraProps, + }; + } else { + throw new Error(`Invalid script type: ${typeof script}`); + } +} + +function generatorStyle(style: string) { + return isUrl(style) + ? { type: 'link', href: style } + : { type: 'style', content: style }; +} + +export function Html({ + children, + loaderData, + manifest, + metadata, +}: React.PropsWithChildren) { + // TODO: 处理 head 标签,比如 favicon.ico 的一致性 + // TODO: root 支持配置 + return ( + + + + + {metadata?.title && {metadata.title}} + {metadata?.favicons?.map((favicon, key) => { + return ; + })} + {metadata?.description && ( + + )} + {metadata?.keywords?.length && ( + + )} + {metadata?.metas?.map((em) => ( + + ))} + + {metadata?.links?.map((link: Record, key) => { + return ; + })} + {metadata?.styles?.map((style: string, key) => { + const { type, href, content } = generatorStyle(style); + if (type === 'link') { + return ; + } else if (type === 'style') { + return ; + } + })} + {metadata?.headScripts?.map((script: IScript, key) => { + const { content, ...rest } = genaretorScript(script); + return ( + + ); + })} + {manifest?.assets['umi.css'] && ( + + )} + + +