diff --git a/examples/max/package.json b/examples/max/package.json index 4b98d5bd03ba..653719fb1333 100644 --- a/examples/max/package.json +++ b/examples/max/package.json @@ -19,8 +19,8 @@ "@umijs/max": "workspace:*", "antd": "^4.23.2", "dayjs": "^1.11.7", - "react": "18.1.0", - "react-dom": "18.1.0" + "react": "18.3.1", + "react-dom": "18.3.1" }, "devDependencies": { "cross-env": "^7.0.3", diff --git a/examples/mf-host/package.json b/examples/mf-host/package.json index 5d15d8db6d54..72e77f6c3640 100644 --- a/examples/mf-host/package.json +++ b/examples/mf-host/package.json @@ -19,8 +19,8 @@ }, "dependencies": { "@umijs/max": "workspace:*", - "react": "18.1.0", - "react-dom": "18.1.0" + "react": "18.3.1", + "react-dom": "18.3.1" }, "devDependencies": { "cross-env": "^7.0.3", diff --git a/examples/mf-remote/package.json b/examples/mf-remote/package.json index 102d2253c4b8..d7dd8cc65cd9 100644 --- a/examples/mf-remote/package.json +++ b/examples/mf-remote/package.json @@ -12,8 +12,8 @@ }, "dependencies": { "@umijs/max": "workspace:*", - "react": "18.1.0", - "react-dom": "18.1.0" + "react": "18.3.1", + "react-dom": "18.3.1" }, "devDependencies": { "cross-env": "^7.0.3", diff --git a/examples/qiankun-master/package.json b/examples/qiankun-master/package.json index c589df08446f..e0983660d1d8 100644 --- a/examples/qiankun-master/package.json +++ b/examples/qiankun-master/package.json @@ -10,8 +10,8 @@ "dependencies": { "@umijs/max": "workspace:*", "qiankun": "^2.10.1", - "react": "18.1.0", - "react-dom": "18.1.0" + "react": "18.3.1", + "react-dom": "18.3.1" }, "devDependencies": { "cross-env": "^7.0.3" diff --git a/examples/qiankun-slave-app2/package.json b/examples/qiankun-slave-app2/package.json index 3ab4c0263798..77971381441e 100644 --- a/examples/qiankun-slave-app2/package.json +++ b/examples/qiankun-slave-app2/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@umijs/max": "workspace:*", - "react": "18.1.0", - "react-dom": "18.1.0" + "react": "18.3.1", + "react-dom": "18.3.1" } } diff --git a/examples/qiankun-slave/package.json b/examples/qiankun-slave/package.json index 4694a8cfc03b..d10ccdd91b92 100644 --- a/examples/qiankun-slave/package.json +++ b/examples/qiankun-slave/package.json @@ -18,8 +18,8 @@ }, "dependencies": { "@umijs/max": "workspace:*", - "react": "18.1.0", - "react-dom": "18.1.0" + "react": "18.3.1", + "react-dom": "18.3.1" }, "devDependencies": { "cross-env": "^7.0.3", diff --git a/examples/ssr-demo/.umirc.ts b/examples/ssr-demo/.umirc.ts index 18627a8fbbc7..bdb1c6618ff3 100644 --- a/examples/ssr-demo/.umirc.ts +++ b/examples/ssr-demo/.umirc.ts @@ -1,10 +1,27 @@ export default { svgr: {}, hash: true, + mfsu: false, routePrefetch: {}, manifest: {}, clientLoader: {}, + mako: { + plugins: [ + { + load: () => {}, + }, + ], + }, ssr: { - serverBuildPath: './umi.server.js', + builder: 'mako', }, + exportStatic: {}, + styles: [`body { color: red; }`], + + metas: [ + { + name: 'test', + content: 'content', + }, + ], }; diff --git a/examples/ssr-demo/mako.config.json b/examples/ssr-demo/mako.config.json new file mode 100644 index 000000000000..5b40b73fdcaf --- /dev/null +++ b/examples/ssr-demo/mako.config.json @@ -0,0 +1,6 @@ +{ + "optimization": { + "skipModules": true, + "concatenateModules": false + } +} diff --git a/examples/ssr-demo/package.json b/examples/ssr-demo/package.json index 564ea9a69734..3b81a184639b 100644 --- a/examples/ssr-demo/package.json +++ b/examples/ssr-demo/package.json @@ -9,6 +9,8 @@ "start:prod": "node ./production-server.js" }, "dependencies": { + "@ant-design/cssinjs": "^1.18.5", + "antd": "^5", "express": "4.18.2", "umi": "workspace:*" } diff --git a/examples/ssr-demo/src/layouts/index.tsx b/examples/ssr-demo/src/layouts/index.tsx new file mode 100644 index 000000000000..06c6f7ea086c --- /dev/null +++ b/examples/ssr-demo/src/layouts/index.tsx @@ -0,0 +1,23 @@ +import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs'; +import { useState } from 'react'; +import { Outlet, useServerInsertedHTML } from 'umi'; + +export default function Layout() { + const [cssCache] = useState(() => createCache()); + + useServerInsertedHTML(() => { + const style = extractStyle(cssCache, { plain: true }); + return ( + + ); + }); + + return ( + + + + ); +} diff --git a/examples/ssr-demo/src/pages/index.tsx b/examples/ssr-demo/src/pages/index.tsx index 279b00b9d5c1..5187116cc4ca 100644 --- a/examples/ssr-demo/src/pages/index.tsx +++ b/examples/ssr-demo/src/pages/index.tsx @@ -1,8 +1,12 @@ +import { Input } from 'antd'; +import { useId } from 'react'; import { + ClientLoader, Link, MetadataLoader, ServerLoader, useClientLoaderData, + useLoaderData, useServerInsertedHTML, useServerLoaderData, } from 'umi'; @@ -20,18 +24,29 @@ import umiLogo from './umi.png'; export default function HomePage() { const clientLoaderData = useClientLoaderData(); const serverLoaderData = useServerLoaderData(); + const loaderData = useLoaderData(); useServerInsertedHTML(() => { - return
inserted html
; + return ( + + ); }); + const id = useId(); + return (

Hello~

+

id: {id}

This is index.tsx

I should be pink

I should be cyan

client loader data: {JSON.stringify(clientLoaderData)}

server loader data: {JSON.stringify(serverLoaderData)}

+

merge loader data: {JSON.stringify(loaderData)}

); } -export async function clientLoader() { +export const clientLoader: ClientLoader = async ({}) => { await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000)); - return { message: 'data from client loader of index.tsx' }; -} + return { clientMessage: 'data from client loader of index.tsx' }; +}; +clientLoader.hydrate = true; export const serverLoader: ServerLoader = async (req) => { - const url = req!.request.url; + const url = req?.request?.url; await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000)); - return { message: `data from server loader of index.tsx, url: ${url}` }; + return { serverMessage: `data from server loader of index.tsx, url: ${url}` }; }; // SEO-设置页面的TDK diff --git a/index.js b/index.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/package.json b/package.json index 5823647d8fa1..23501db0a4e4 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "@types/jest": "^29.2.5", "@types/node": "^18.11.18", "@types/qs": "^6.9.7", - "@types/react": "^18.0.26", - "@types/react-dom": "^18.0.10", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "@types/resolve": "^1.20.2", "@types/rimraf": "3.0.2", "@types/tunnel": "^0.0.3", @@ -101,8 +101,8 @@ "prettier-plugin-organize-imports": "^3.2.2", "prettier-plugin-packagejson": "^2.4.3", "qs": "^6.11.0", - "react": "18.1.0", - "react-dom": "18.1.0", + "react": "18.3.1", + "react-dom": "18.3.1", "react-text-loop-next": "0.0.3", "regenerator-runtime": "^0.13.11", "resolve": "^1.22.0", @@ -142,8 +142,8 @@ ] }, "overrides": { - "browserslist": "$browserslist", - "@parcel/watcher": "2.1.0" + "@parcel/watcher": "2.1.0", + "browserslist": "$browserslist" } }, "_local": "This flag is used to check if the development in local, Please do not delete." diff --git a/packages/bundler-webpack/src/config/definePlugin.test.ts b/packages/bundler-webpack/src/config/definePlugin.test.ts index ad52951ce0ed..b31eb7f53a56 100644 --- a/packages/bundler-webpack/src/config/definePlugin.test.ts +++ b/packages/bundler-webpack/src/config/definePlugin.test.ts @@ -13,6 +13,7 @@ test('normal', () => { NODE_ENV: '"test"', PUBLIC_PATH: '"/"', }, + 'process.env.SSR_MANIFEST': 'process.env.SSR_MANIFEST', }); }); @@ -31,6 +32,7 @@ test('env variables', () => { UMI_APP_FOO: '"BAR"', PUBLIC_PATH: '"/"', }, + 'process.env.SSR_MANIFEST': 'process.env.SSR_MANIFEST', }); }); diff --git a/packages/bundler-webpack/src/config/definePlugin.ts b/packages/bundler-webpack/src/config/definePlugin.ts index 545a8db71aef..9e22c0b5db0e 100644 --- a/packages/bundler-webpack/src/config/definePlugin.ts +++ b/packages/bundler-webpack/src/config/definePlugin.ts @@ -66,6 +66,7 @@ export function resolveDefine(opts: IOpts) { return { 'process.env': env, + 'process.env.SSR_MANIFEST': 'process.env.SSR_MANIFEST', ...define, }; } diff --git a/packages/mfsu/src/mfsu/strategyStaticAnalyze.ts b/packages/mfsu/src/mfsu/strategyStaticAnalyze.ts index ac0901f8e421..a573d46c649b 100644 --- a/packages/mfsu/src/mfsu/strategyStaticAnalyze.ts +++ b/packages/mfsu/src/mfsu/strategyStaticAnalyze.ts @@ -1,11 +1,11 @@ import { logger, printHelp, winPath } from '@umijs/utils'; +import type { Configuration } from 'webpack'; import { checkMatch } from '../babelPlugins/awaitImport/checkMatch'; import mfImport from '../babelPlugins/awaitImport/MFImport'; import { StaticDepInfo } from '../staticDepInfo/staticDepInfo'; +import { extractBabelPluginImportOptions } from '../utils/webpackUtils'; import { IBuildDepPluginOpts } from '../webpackPlugins/buildDepPlugin'; import type { IMFSUStrategy, MFSU } from './mfsu'; -import type { Configuration } from 'webpack'; -import { extractBabelPluginImportOptions } from '../utils/webpackUtils'; export class StaticAnalyzeStrategy implements IMFSUStrategy { private readonly mfsu: MFSU; diff --git a/packages/plugins/libs/qiankun/master/masterRuntimePlugin.tsx b/packages/plugins/libs/qiankun/master/masterRuntimePlugin.tsx index 13520cb78342..d8a70444f40d 100644 --- a/packages/plugins/libs/qiankun/master/masterRuntimePlugin.tsx +++ b/packages/plugins/libs/qiankun/master/masterRuntimePlugin.tsx @@ -77,6 +77,10 @@ function patchMicroAppRouteComponent(routes: any[]) { } export async function render(oldRender: typeof noop) { + // 在 ssr 的场景下,直接返回旧的 render + if (typeof window === 'undefined') { + return oldRender(); + } const runtimeOptions = await getMasterRuntime(); let masterOptions: MasterOptions = { ...getMasterOptions(), @@ -138,6 +142,10 @@ export async function render(oldRender: typeof noop) { } export function patchClientRoutes({ routes }: { routes: any[] }) { + // 在 ssr 的场景下,不执行主应用的 patchClientRoutes + if (typeof window === 'undefined') { + return; + } const microAppRoutes = [].concat( deepFilterLeafRoutes(routes), deepFilterLeafRoutes(microAppRuntimeRoutes), diff --git a/packages/plugins/libs/qiankun/slave/slaveRuntimePlugin.ts b/packages/plugins/libs/qiankun/slave/slaveRuntimePlugin.ts index e16244405db5..bd81f6bfc2e1 100644 --- a/packages/plugins/libs/qiankun/slave/slaveRuntimePlugin.ts +++ b/packages/plugins/libs/qiankun/slave/slaveRuntimePlugin.ts @@ -3,6 +3,10 @@ import { createHistory } from '@@/core/history'; import qiankunRender, { contextOptsStack } from './lifecycles'; export function render(oldRender: any) { + // 在 ssr 的场景下,直接返回旧的 render + if (typeof window === 'undefined') { + return oldRender(); + } return qiankunRender().then(oldRender); } diff --git a/packages/plugins/src/initial-state.ts b/packages/plugins/src/initial-state.ts index d265b7640d6f..55c49629a940 100644 --- a/packages/plugins/src/initial-state.ts +++ b/packages/plugins/src/initial-state.ts @@ -52,7 +52,7 @@ export default function InitialStateProvider(props: any) { appLoaded.current = true; } }, [loading]); - if (loading && !appLoaded.current) { + if (loading && !appLoaded.current && typeof window !== 'undefined') { return ; } return props.children; diff --git a/packages/plugins/src/qiankun/master.ts b/packages/plugins/src/qiankun/master.ts index 29a332190db6..30ff0a914f43 100644 --- a/packages/plugins/src/qiankun/master.ts +++ b/packages/plugins/src/qiankun/master.ts @@ -210,4 +210,18 @@ export { MicroAppWithMemoHistory } from './MicroAppWithMemoHistory'; `, }); }); + + api.chainWebpack((config, { ssr }) => { + // 在 ssr 的场景下,把 qiankun external 到一个任意模块 + // 这样就不会把 qiankun 的依赖构建进产物中 + if (ssr) { + const originalExternals = config.get('externals'); + config.externals({ + ...originalExternals, + qiankun: 'fs', + }); + } + + return config; + }); }; diff --git a/packages/plugins/src/qiankun/slave.ts b/packages/plugins/src/qiankun/slave.ts index 2e976c28149b..c39b3fa0af76 100644 --- a/packages/plugins/src/qiankun/slave.ts +++ b/packages/plugins/src/qiankun/slave.ts @@ -180,7 +180,11 @@ export interface IRuntimeConfig { ]; }); - api.chainWebpack((config) => { + api.chainWebpack((config, { ssr }) => { + // ssr 场景下,通过 cjs 的方式来使用模块,跳过 umd方式的构建 + if (ssr) { + return; + } assert(api.pkg.name, 'You should have name in package.json.'); // 默认不修改 library chunk 的 name,从而确保可以通过 window[appName] 访问到导出 // mfsu 关闭的时候才可以修改,否则可能导致配合 mfsu 时,子应用的 umd chunk 无法被正确加载 @@ -223,11 +227,14 @@ export interface IRuntimeConfig { api.addEntryCode(() => [ ` -export const bootstrap = qiankun_genBootstrap(render); -export const mount = qiankun_genMount('${api.config.mountElementId}'); -export const unmount = qiankun_genUnmount('${api.config.mountElementId}'); -export const update = qiankun_genUpdate(); -if (!window.__POWERED_BY_QIANKUN__) { +const qiankun_noop = () => new Error('qiankun lifecycle is not available for server runtime!'); +const isServer = typeof window === 'undefined'; +export const bootstrap = isServer ? qiankun_noop: qiankun_genBootstrap(render); +export const mount = isServer ? qiankun_noop : qiankun_genMount('${api.config.mountElementId}'); +export const unmount = isServer ? qiankun_noop : qiankun_genUnmount('${api.config.mountElementId}'); +export const update = isServer ? qiankun_noop : qiankun_genUpdate(); +// 增加 ssr 的判断 +if (!isServer && !window.__POWERED_BY_QIANKUN__) { bootstrap().then(mount); } `, diff --git a/packages/plugins/src/utils/modelUtils.test.ts b/packages/plugins/src/utils/modelUtils.test.ts index 748821e827f3..7bd4e21a6ed0 100644 --- a/packages/plugins/src/utils/modelUtils.test.ts +++ b/packages/plugins/src/utils/modelUtils.test.ts @@ -1,5 +1,5 @@ -import { ModelUtils, Model, getNamespace, transformSync } from './modelUtils'; import { chalk } from '@umijs/utils'; +import { getNamespace, Model, ModelUtils, transformSync } from './modelUtils'; test('getNamespace', () => { expect(getNamespace('/a/b/src/models/foo.ts', '/a/b/src')).toEqual('foo'); @@ -199,7 +199,8 @@ test('TopologicalSort: detect circle', () => { }); test('transformSync', () => { - transformSync(` + transformSync( + ` function prop() {} export class UseDecorator { @@ -213,9 +214,11 @@ export class UseDecorator { console.log(a); } } - `, { - loader: 'ts', - sourcemap: false, - minify: false, - }); + `, + { + loader: 'ts', + sourcemap: false, + minify: false, + }, + ); }); diff --git a/packages/preset-umi/package.json b/packages/preset-umi/package.json index 1ca2d41f4789..92e8ef9a7c4e 100644 --- a/packages/preset-umi/package.json +++ b/packages/preset-umi/package.json @@ -30,7 +30,7 @@ "@umijs/ast": "workspace:*", "@umijs/babel-preset-umi": "workspace:*", "@umijs/bundler-esbuild": "workspace:*", - "@umijs/bundler-mako": "0.7.2", + "@umijs/bundler-mako": "0.7.3", "@umijs/bundler-utils": "workspace:*", "@umijs/bundler-vite": "workspace:*", "@umijs/bundler-webpack": "workspace:*", @@ -57,8 +57,8 @@ "path-to-regexp": "1.7.0", "postcss": "^8.4.21", "postcss-prefix-selector": "1.16.0", - "react": "18.1.0", - "react-dom": "18.1.0", + "react": "18.3.1", + "react-dom": "18.3.1", "react-router": "6.3.0", "react-router-dom": "6.3.0", "regenerator-runtime": "0.13.11" diff --git a/packages/preset-umi/src/commands/build.ts b/packages/preset-umi/src/commands/build.ts index 8aeacffe6606..332aa60ea37c 100644 --- a/packages/preset-umi/src/commands/build.ts +++ b/packages/preset-umi/src/commands/build.ts @@ -1,3 +1,4 @@ +import type { IServicePluginAPI } from '@umijs/core'; import { getMarkup } from '@umijs/server'; import { chalk, fsExtra, logger, rimraf, semver } from '@umijs/utils'; import { writeFileSync } from 'fs'; @@ -17,7 +18,6 @@ const bundlerWebpack: typeof import('@umijs/bundler-webpack') = lazyImportFromCurrentPkg('@umijs/bundler-webpack'); const bundlerVite: typeof import('@umijs/bundler-vite') = lazyImportFromCurrentPkg('@umijs/bundler-vite'); - export default (api: IApi) => { api.registerCommand({ name: 'build', @@ -107,7 +107,10 @@ umi build --clean react: { runtime: shouldUseAutomaticRuntime ? 'automatic' : 'classic', }, - config: api.config, + config: { + outputPath: api.userConfig.outputPath || 'dist', + ...api.config, + } as IServicePluginAPI['config'], cwd: api.cwd, entry, ...(api.config.vite @@ -136,8 +139,9 @@ umi build --clean let stats: any; if (api.config.vite) { stats = await bundlerVite.build(opts); - } else if (process.env.OKAM) { + } else if (api.config.mako) { require('@umijs/bundler-webpack/dist/requireHook'); + // @ts-ignore const { build } = require(process.env.OKAM); stats = await build(opts); } else { @@ -174,10 +178,11 @@ umi build --clean publicPath: api.config.publicPath, }); const { vite } = api.args; - const markupArgs = await getMarkupArgs({ api }); + const args = await getMarkupArgs({ api }); + const finalMarkUpArgs = { - ...markupArgs, - styles: markupArgs.styles.concat( + ...args, + styles: args.styles.concat( api.config.vite ? [] : [...(assetsMap['umi.css'] || []).map((src) => ({ src }))], @@ -185,7 +190,7 @@ umi build --clean scripts: (api.config.vite ? [] : [...(assetsMap['umi.js'] || []).map((src) => ({ src }))] - ).concat(markupArgs.scripts), + ).concat(args.scripts), esmScript: !!opts.config.esm || vite, path: '/', }; diff --git a/packages/preset-umi/src/commands/dev/createRouteMiddleware.ts b/packages/preset-umi/src/commands/dev/createRouteMiddleware.ts index 831fb8dd0efa..6afd60433143 100644 --- a/packages/preset-umi/src/commands/dev/createRouteMiddleware.ts +++ b/packages/preset-umi/src/commands/dev/createRouteMiddleware.ts @@ -14,8 +14,8 @@ function createRouteMiddleware(opts: { api: IApi }) { onStats?.(stats); }); - async function getStats() { - if (!compiler && process.env.OKAM) { + async function getStats(api: IApi) { + if (!compiler && api.config.mako) { return { compilation: { assets: { 'umi.js': 'umi.js', 'umi.css': 'umi.css' } }, hasErrors: () => false, @@ -32,12 +32,11 @@ function createRouteMiddleware(opts: { api: IApi }) { return async (req, res, next) => { const markupArgs = (await getMarkupArgs(opts)) as any; let assetsMap: Record = {}; - const stats: any = await getStats(); + const stats: any = await getStats(opts.api); assetsMap = getAssetsMap({ stats, publicPath: opts.api.config.publicPath!, }); - const requestHandler = await createRequestHandler({ ...markupArgs, styles: markupArgs.styles.concat( diff --git a/packages/preset-umi/src/commands/dev/dev.ts b/packages/preset-umi/src/commands/dev/dev.ts index aaa85944b1bc..02c835a62283 100644 --- a/packages/preset-umi/src/commands/dev/dev.ts +++ b/packages/preset-umi/src/commands/dev/dev.ts @@ -357,7 +357,10 @@ PORT=8888 umi dev react: { runtime: shouldUseAutomaticRuntime ? 'automatic' : 'classic', }, - config: api.config, + config: { + outputPath: api.userConfig.outputPath || 'dist', + ...api.config, + }, pkg: api.pkg, cwd: api.cwd, rootDir: process.cwd(), @@ -433,8 +436,9 @@ PORT=8888 umi dev if (enableVite) { await bundlerVite.dev(opts); - } else if (process.env.OKAM) { + } else if (api.config.mako) { require('@umijs/bundler-webpack/dist/requireHook'); + // @ts-ignore const { dev } = require(process.env.OKAM); await dev(opts); } else { diff --git a/packages/preset-umi/src/features/appData/umiInfo.ts b/packages/preset-umi/src/features/appData/umiInfo.ts index afc2ded7318c..1a3bbedf1bcc 100644 --- a/packages/preset-umi/src/features/appData/umiInfo.ts +++ b/packages/preset-umi/src/features/appData/umiInfo.ts @@ -3,9 +3,11 @@ import type { IApi } from '../../types'; export default (api: IApi) => { api.addEntryCode(() => [ ` -window.g_umi = { - version: '${api.appData.umi.version}', -}; - `, + if (typeof window !== 'undefined') { + window.g_umi = { + version: '${api.appData.umi.version}', + }; + } + `, ]); }; diff --git a/packages/preset-umi/src/features/devTool/devTool.ts b/packages/preset-umi/src/features/devTool/devTool.ts index ef3c503e7ab6..cfd774d9b300 100644 --- a/packages/preset-umi/src/features/devTool/devTool.ts +++ b/packages/preset-umi/src/features/devTool/devTool.ts @@ -8,7 +8,7 @@ const assetsDir = join(__dirname, '../../../assets'); export default (api: IApi) => { api.addBeforeMiddlewares(async () => { - if (process.env.OKAM) return []; + if (api.config.mako) return []; // get loading html const $ = await api.applyPlugins({ key: 'modifyDevToolLoadingHTML', diff --git a/packages/preset-umi/src/features/esbuildHelperChecker/esbuildHelperChecker.ts b/packages/preset-umi/src/features/esbuildHelperChecker/esbuildHelperChecker.ts index 5c96da0f29b0..1326b19aab6b 100644 --- a/packages/preset-umi/src/features/esbuildHelperChecker/esbuildHelperChecker.ts +++ b/packages/preset-umi/src/features/esbuildHelperChecker/esbuildHelperChecker.ts @@ -90,8 +90,7 @@ export default (api: IApi) => { }); api.onBuildComplete(async ({ err }) => { - if (api.config.vite) return; - if (process.env.OKAM) return; + if (api.config.vite || api.config.mako) return; if (err) return; const jsMinifier = api.config.jsMinifier || 'esbuild'; if (jsMinifier !== 'esbuild') return; diff --git a/packages/preset-umi/src/features/exportStatic/exportStatic.ts b/packages/preset-umi/src/features/exportStatic/exportStatic.ts index 9ceb55d72b19..ae0d2f9be2b2 100644 --- a/packages/preset-umi/src/features/exportStatic/exportStatic.ts +++ b/packages/preset-umi/src/features/exportStatic/exportStatic.ts @@ -64,25 +64,9 @@ async function getPreRenderedHTML(api: IApi, htmlTpl: string, path: string) { markupRender ??= require(absServerBuildPath(api))._markupGenerator; try { - const markup = await markupRender(path); - const [mainTpl, extraTpl = ''] = markup.split(''); - // TODO: improve return type for markup generator - const helmetContent = mainTpl.match( - /[^]*?(<[^>]+data-rh[^]+)<\/head>/, - )?.[1]; - const bodyContent = mainTpl.match(/]*>([^]+?)<\/body>/)?.[1]; - - htmlTpl = htmlTpl - // append helmet content - .replace('', `${helmetContent || ''}`) - // replace #root with pre-rendered body content - .replace( - new RegExp(`
]*>.*?
`), - bodyContent, - ) - // append hidden templates - .replace(/$/, `${extraTpl}`); + const html = await markupRender(path); logger.info(`Pre-render for ${path}`); + return html; } catch (err) { logger.error(`Pre-render ${path} error: ${err}`); if (!ignorePreRenderError) { @@ -149,22 +133,10 @@ export default (api: IApi) => { const htmlData = api.appData.exportHtmlData; const htmlFiles: { path: string; content: string }[] = []; const { markupArgs: defaultMarkupArgs } = opts; - let asyncMarkupArgs: typeof defaultMarkupArgs; for (const { file, route, prerender } of htmlData) { let markupArgs = defaultMarkupArgs; - // mark async for the scripts of pre-rendered html - if (api.config.ssr && prerender) { - // copy args to avoid affect original object - markupArgs = asyncMarkupArgs ??= { - ...markupArgs, - scripts: markupArgs.scripts.map((script: any) => - script.src ? { ...script, async: true } : script, - ), - }; - } - // handle relative publicPath, such as `./` if (publicPath.startsWith('.')) { assert( diff --git a/packages/preset-umi/src/features/mako/mako.ts b/packages/preset-umi/src/features/mako/mako.ts index ef6290f9f1c2..760656658c55 100644 --- a/packages/preset-umi/src/features/mako/mako.ts +++ b/packages/preset-umi/src/features/mako/mako.ts @@ -1,28 +1,47 @@ +import { chalk } from '@umijs/utils'; import path from 'path'; import { IApi } from '../../types'; -import { chalk } from '@umijs/utils'; +import { isWindows } from '../../utils/platform'; export default (api: IApi) => { api.describe({ key: 'mako', config: { schema({ zod }) { - return zod.object({}); + return zod + .object({ + plugins: zod.array( + zod + .object({ + load: zod.function(), + generateEnd: zod.function(), + }) + .partial(), + ), + }) + .partial(); }, }, enableBy: api.EnableBy.config, }); api.modifyConfig((memo) => { + // @TODO remove this when mako support windows + if (isWindows) { + memo.mako = false; + process.env.OKAM = ''; + } return { ...memo, mfsu: false, hmrGuardian: false, + makoPlugins: memo.mako?.plugins || [], }; }); api.onStart(() => { - process.env.OKAM = process.env.OKAM || require.resolve('@umijs/bundler-mako'); + process.env.OKAM = + process.env.OKAM || require.resolve('@umijs/bundler-mako'); try { const pkg = require(path.join( require.resolve(process.env.OKAM), @@ -31,7 +50,13 @@ export default (api: IApi) => { api.logger.info(`Using mako@${pkg.version}`); const isBigfish = process.env.BIGFISH_INFO; if (!isBigfish) { - api.logger.warn(chalk.yellow(chalk.bold(`Mako is an extremely fast, production-grade web bundler based on Rust. And it's still under active development and is not yet ready for production use. If you encounter any issues, please checkout https://makojs.dev/ to join the community and report the issue.`))); + api.logger.warn( + chalk.yellow( + chalk.bold( + `Mako is an extremely fast, production-grade web bundler based on Rust. And it's still under active development and is not yet ready for production use. If you encounter any issues, please checkout https://makojs.dev/ to join the community and report the issue.`, + ), + ), + ); } } catch (e) { console.error(e); diff --git a/packages/preset-umi/src/features/okam/okam.ts b/packages/preset-umi/src/features/okam/okam.ts index f908338d06fa..2001b95f6c59 100644 --- a/packages/preset-umi/src/features/okam/okam.ts +++ b/packages/preset-umi/src/features/okam/okam.ts @@ -3,7 +3,7 @@ import { IApi } from '../../types'; export default (api: IApi) => { api.describe({ - enableBy: () => Boolean(process.env.OKAM), + enableBy: () => Boolean(api.config.mako), }); api.onCheck(() => { diff --git a/packages/preset-umi/src/features/ssr/mako/mako.ts b/packages/preset-umi/src/features/ssr/mako/mako.ts new file mode 100644 index 000000000000..8f4a1472ff4b --- /dev/null +++ b/packages/preset-umi/src/features/ssr/mako/mako.ts @@ -0,0 +1,68 @@ +import { Env } from '@umijs/bundler-webpack/dist/types'; +import { fsExtra, logger } from '@umijs/utils'; +import { forEach } from '@umijs/utils/compiled/lodash'; +import { existsSync, writeFileSync } from 'fs'; +import path, { dirname, join } from 'path'; + +import { IApi } from 'umi'; +import { absServerBuildPath } from '../utils'; + +export const build = async (api: IApi) => { + logger.wait('[SSR] Compiling by mako...'); + const now = new Date().getTime(); + const absOutputFile = absServerBuildPath(api); + require('@umijs/bundler-webpack/dist/requireHook'); + + // @ts-ignore + const { build } = require(process.env.OKAM); + + const useHash = api.config.hash && api.env === Env.production; + const publicPath = api.userConfig.publicPath || '/'; + + const entry = path.resolve(api.paths.absTmpPath, 'umi.server.ts'); + const options = { + cwd: api.cwd, + entry: { + 'umi.server': entry, + }, + config: { + makoPlugins: api.config.mako.plugins, + ...api.config, + jsMinifier: 'none', + hash: useHash, + outputPath: path.dirname(absOutputFile), + manifest: { + fileName: 'build-manifest.json', + }, + devtool: false, + cjs: true, + dynamicImportToRequire: false, + }, + chainWebpack: async (memo: any) => { + memo.target('node'); + return memo; + }, + onBuildComplete: () => { + const finalJsonObj: any = {}; + const jsonFilePath = join(dirname(absOutputFile), 'build-manifest.json'); + const json = existsSync(jsonFilePath) + ? fsExtra.readJSONSync(jsonFilePath) + : {}; + forEach(json, (path, key) => { + json[key] = `${publicPath}${path}`; + }); + + finalJsonObj.assets = { + ...json, + 'umi.js': json['umi.server.js'], + }; + writeFileSync(jsonFilePath, JSON.stringify(finalJsonObj, null, 2), { + flag: 'w', + }); + }, + }; + await build(options); + + const diff = new Date().getTime() - now; + logger.info(`[SSR] Compiled in ${diff}ms`); +}; diff --git a/packages/preset-umi/src/features/ssr/ssr.ts b/packages/preset-umi/src/features/ssr/ssr.ts index d5fa28b509c0..4d18a42da49b 100644 --- a/packages/preset-umi/src/features/ssr/ssr.ts +++ b/packages/preset-umi/src/features/ssr/ssr.ts @@ -8,7 +8,8 @@ import assert from 'assert'; import { existsSync, writeFileSync } from 'fs'; import { dirname, join } from 'path'; import type { IApi } from '../../types'; -import { absServerBuildPath } from './utils'; +import { isWindows } from '../../utils/platform'; +import { absServerBuildPath, generateBuildManifest } from './utils'; export default (api: IApi) => { const esbuildBuilder: typeof import('./builder/builder') = importLazy( @@ -17,6 +18,10 @@ export default (api: IApi) => { const webpackBuilder: typeof import('./webpack/webpack') = importLazy( require.resolve('./webpack/webpack'), ); + const makoBuiler: typeof import('./mako/mako') = importLazy( + require.resolve('./mako/mako'), + ); + let serverBuildTarget: string; api.describe({ key: 'ssr', @@ -25,8 +30,13 @@ export default (api: IApi) => { return zod .object({ serverBuildPath: zod.string(), + serverBuildTarget: zod.enum(['express', 'worker']), platform: zod.string(), - builder: zod.enum(['esbuild', 'webpack']), + builder: zod.enum(['esbuild', 'webpack', 'mako']), + __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: zod.object({ + pureApp: zod.boolean(), + pureHtml: zod.boolean(), + }), }) .deepPartial(); }, @@ -48,6 +58,49 @@ export default (api: IApi) => { logger.warn(`SSR feature is in beta, may be unstable`); }); + api.modifyDefaultConfig((memo) => { + if (serverBuildTarget === 'worker') { + const oReactDom = memo.alias['react-dom']; + + // put react-dom after react-dom/server + delete memo.alias['react-dom']; + + // use browser version of react-dom/server for worker mode + // ref: https://github.com/facebook/react/blob/f86afca090b668d8be10b642750844759768d1ad/packages/react-server-dom-webpack/package.json#L52 + memo.alias['react-dom/server$'] = winPath( + join( + api.service.configDefaults.alias['react-dom'], + 'server.browser.js', + ), + ); + memo.alias['react-dom'] = oReactDom; + } + + return memo; + }); + + api.modifyConfig((memo) => { + // define SSR_BUILD_TARGET to strip useless logic + memo.define ??= {}; + serverBuildTarget = memo.define['process.env.SSR_BUILD_TARGET'] = + memo.ssr.serverBuildTarget || 'express'; + + // csr && ssr must use the same mako bundler + // mako builder need config manifest + if (memo.ssr.builder === 'mako') { + assert( + memo.mako, + `The \`ssr.builder mako\` config is now allowed when \`mako\` is enable!`, + ); + memo.manifest ??= {}; + if (isWindows) { + memo.ssr.builder = 'webpack'; + } + } + + return memo; + }); + api.addMiddlewares(() => [ async (req, res, next) => { const modulePath = absServerBuildPath(api); @@ -120,7 +173,7 @@ export type { }); api.onBeforeCompiler(async ({ opts }) => { - const { builder = 'esbuild' } = api.config.ssr; + const { builder = 'webpack' } = api.config.ssr; if (builder === 'esbuild') { await esbuildBuilder.build({ @@ -134,6 +187,19 @@ export type { ); await webpackBuilder.build(api, opts); + } else if (api.config.mako && builder === 'mako') { + await makoBuiler.build(api); + } + }); + api.onDevCompileDone(() => { + if (api.config.mako) { + generateBuildManifest(api); + } + }); + + api.onBuildComplete(() => { + if (api.config.mako) { + generateBuildManifest(api); } }); diff --git a/packages/preset-umi/src/features/ssr/utils.ts b/packages/preset-umi/src/features/ssr/utils.ts index 3bb13e08177e..3e9db205a882 100644 --- a/packages/preset-umi/src/features/ssr/utils.ts +++ b/packages/preset-umi/src/features/ssr/utils.ts @@ -1,7 +1,8 @@ -import { existsSync } from 'fs'; +import { fsExtra } from '@umijs/utils'; +import { forEach } from '@umijs/utils/compiled/lodash'; +import { existsSync, writeFileSync } from 'fs'; import { basename, join } from 'path'; import { IApi } from '../../types'; - /** esbuild plugin for resolving umi imports */ export function esbuildUmiPlugin(api: IApi) { return { @@ -37,3 +38,22 @@ export function absServerBuildPath(api: IApi) { // ex. /foo/umi.xxx.js -> umi.xxx.js return join(api.paths.cwd, 'server', basename(manifest.assets['umi.js'])); } + +export const generateBuildManifest = (api: IApi) => { + const publicPath = api.userConfig.publicPath || '/'; + const manifestFileName = + api.config.manifest?.fileName || 'asset-manifest.json'; + const finalJsonObj: any = {}; + const assetFilePath = join(api.paths.absOutputPath, manifestFileName); + const buildFilePath = join(api.paths.absOutputPath, 'build-manifest.json'); + const json = existsSync(assetFilePath) + ? fsExtra.readJSONSync(assetFilePath) + : {}; + forEach(json, (path, key) => { + json[key] = `${publicPath}${path}`; + }); + finalJsonObj.assets = json; + writeFileSync(buildFilePath, JSON.stringify(finalJsonObj, null, 2), { + flag: 'w', + }); +}; diff --git a/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts b/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts index 3390de345ee7..c61e875af82b 100644 --- a/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts +++ b/packages/preset-umi/src/features/tmpFiles/tmpFiles.ts @@ -2,11 +2,11 @@ import { importLazy, lodash, winPath } from '@umijs/utils'; import { existsSync, readdirSync } from 'fs'; import { basename, dirname, join } from 'path'; import { RUNTIME_TYPE_FILE_NAME } from 'umi'; +import { getMarkupArgs } from '../../commands/dev/getMarkupArgs'; import { TEMPLATES_DIR } from '../../constants'; import { IApi } from '../../types'; import { getModuleExports } from './getModuleExports'; import { importsToStr } from './importsToStr'; - const routesApi: typeof import('./routes') = importLazy( require.resolve('./routes'), ); @@ -261,7 +261,43 @@ declare module '*.txt' { } `.trimEnd(), }); + const entryCode = ( + await api.applyPlugins({ + key: 'addEntryCode', + initialValue: [], + }) + ).join('\n'); + const entryCodeAhead = ( + await api.applyPlugins({ + key: 'addEntryCodeAhead', + initialValue: [], + }) + ).join('\n'); + const importsAhead = importsToStr( + await api.applyPlugins({ + key: 'addEntryImportsAhead', + initialValue: [ + api.appData.globalCSS.length && { + source: api.appData.globalCSS[0], + }, + api.appData.globalJS.length && { + source: api.appData.globalJS[0], + }, + ].filter(Boolean), + }), + ).join('\n'); + const imports = importsToStr( + await api.applyPlugins({ + key: 'addEntryImports', + initialValue: [], + }), + ).join('\n'); + const __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = api.config.ssr + ?.__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ?? { + pureApp: false, + pureHtml: false, + }; // umi.ts api.writeTmpFile({ noPluginDir: true, @@ -272,45 +308,21 @@ declare module '*.txt' { rendererPath, publicPath: api.config.publicPath, runtimePublicPath: api.config.runtimePublicPath ? 'true' : 'false', - entryCode: ( - await api.applyPlugins({ - key: 'addEntryCode', - initialValue: [], - }) - ).join('\n'), - entryCodeAhead: ( - await api.applyPlugins({ - key: 'addEntryCodeAhead', - initialValue: [], - }) - ).join('\n'), + entryCode, + entryCodeAhead, polyfillImports: importsToStr( await api.applyPlugins({ key: 'addPolyfillImports', initialValue: [], }), ).join('\n'), - importsAhead: importsToStr( - await api.applyPlugins({ - key: 'addEntryImportsAhead', - initialValue: [ - api.appData.globalCSS.length && { - source: api.appData.globalCSS[0], - }, - api.appData.globalJS.length && { - source: api.appData.globalJS[0], - }, - ].filter(Boolean), - }), - ).join('\n'), - imports: importsToStr( - await api.applyPlugins({ - key: 'addEntryImports', - initialValue: [], - }), - ).join('\n'), + importsAhead, + imports, basename: api.config.base, historyType: api.config.history.type, + __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: JSON.stringify( + __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + ), hydrate: !!api.config.ssr, reactRouter5Compat: !!api.config.reactRouter5Compat, loadingComponent: api.appData.globalLoading, @@ -489,6 +501,9 @@ if (process.env.NODE_ENV === 'development') { if (api.config.ssr) { const umiPluginPath = winPath(join(umiDir, 'client/client/plugin.js')); const umiServerPath = winPath(require.resolve('@umijs/server/dist/ssr')); + + const mountElementId = api.config.mountElementId; + const routesWithServerLoader = Object.keys(routes).reduce< { id: string; path: string }[] >((memo, id) => { @@ -500,6 +515,8 @@ if (process.env.NODE_ENV === 'development') { } return memo; }, []); + const { headScripts, scripts, styles, title, favicons, links, metas } = + await getMarkupArgs({ api }); api.writeTmpFile({ noPluginDir: true, path: 'umi.server.ts', @@ -509,7 +526,13 @@ if (process.env.NODE_ENV === 'development') { /"component": "await import\((.*)\)"/g, '"component": await import("$1")', ), + version: api.appData.umi.version, + reactVersion: api.appData.react.version, + entryCode, + entryCodeAhead, routesWithServerLoader, + importsAhead, + imports, umiPluginPath, serverRendererPath, umiServerPath, @@ -518,6 +541,19 @@ if (process.env.NODE_ENV === 'development') { join(api.paths.absOutputPath, 'build-manifest.json'), ), env: JSON.stringify(api.env), + htmlPageOpts: JSON.stringify({ + headScripts, + styles, + title, + favicons, + links, + metas, + scripts: scripts || [], + }), + __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: JSON.stringify( + __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + ), + mountElementId, }, }); } @@ -605,7 +641,9 @@ if (process.env.NODE_ENV === 'development') { }) ).join(', ')} } from '${rendererPath}';`, ); - exports.push(`export type { History } from '${rendererPath}'`); + exports.push( + `export type { History, ClientLoader } from '${rendererPath}'`, + ); // umi/client/client/plugin exports.push('// umi/client/client/plugin'); const umiPluginPath = winPath(join(umiDir, 'client/client/plugin.js')); diff --git a/packages/preset-umi/src/types.ts b/packages/preset-umi/src/types.ts index cbbadade3f38..913866833417 100644 --- a/packages/preset-umi/src/types.ts +++ b/packages/preset-umi/src/types.ts @@ -129,6 +129,7 @@ export type IApi = PluginAPI & memo: WebpackChain, args: { env: Env; + ssr?: boolean; webpack: typeof webpack; }, ): void; diff --git a/packages/preset-umi/src/utils/platform.ts b/packages/preset-umi/src/utils/platform.ts new file mode 100644 index 000000000000..22bd7208f46b --- /dev/null +++ b/packages/preset-umi/src/utils/platform.ts @@ -0,0 +1,3 @@ +import os from 'os'; + +export const isWindows = os.platform() === 'win32'; diff --git a/packages/preset-umi/templates/server.tpl b/packages/preset-umi/templates/server.tpl index 13be23d98838..e33d49b21168 100644 --- a/packages/preset-umi/templates/server.tpl +++ b/packages/preset-umi/templates/server.tpl @@ -1,11 +1,14 @@ +{{{ importsAhead }}} import { getClientRootComponent } from '{{{ serverRendererPath }}}'; import { getRoutes } from './core/route'; import { createHistory as createClientHistory } from './core/history'; -import { getPlugins as getClientPlugins } from './core/plugin'; import { ServerInsertedHTMLContext } from './core/serverInsertedHTMLContext'; -import { PluginManager } from '{{{ umiPluginPath }}}'; -import createRequestHandler, { createMarkupGenerator, createUmiHandler, createUmiServerLoader } from '{{{ umiServerPath }}}'; - +import { createPluginManager } from './core/plugin'; +import createRequestHandler, { createMarkupGenerator, createUmiHandler, createUmiServerLoader, createAppRootElement } from '{{{ umiServerPath }}}'; +import fs from 'fs'; +import path from 'path'; +{{{ imports }}} +{{{ entryCodeAhead }}} let helmetContext; try { @@ -18,17 +21,23 @@ const routesWithServerLoader = { {{/routesWithServerLoader}} }; -export function getPlugins() { - return getClientPlugins(); -} - -export function getValidKeys() { - return [{{#validKeys}}'{{{ . }}}',{{/validKeys}}]; -} - export function getManifest(sourceDir) { - return JSON.parse(require('fs').readFileSync( - sourceDir ? require('path').join(sourceDir,'build-manifest.json') : '{{{ assetsPath }}}', 'utf-8')); + let manifestPath; + if (process.env.SSR_MANIFEST) { + return JSON.parse(process.env.SSR_MANIFEST) + } + if (sourceDir) { + manifestPath = path.join(sourceDir,'build-manifest.json') + } + else { + manifestPath = '{{{ assetsPath }}}' + } + if (fs.existsSync(manifestPath)) { + return JSON.parse(fs.readFileSync(manifestPath), 'utf-8'); + } + return { + assets: {} + } } export function createHistory(opts) { @@ -42,20 +51,35 @@ global.g_getAssets = (fileName) => { }; const createOpts = { routesWithServerLoader, - PluginManager, - getPlugins, - getValidKeys, + reactVersion: '{{{reactVersion}}}', + pluginManager: createPluginManager(), getRoutes, manifest: getManifest, getClientRootComponent, helmetContext, createHistory, ServerInsertedHTMLContext, + htmlPageOpts: {{{htmlPageOpts}}}, + __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {{{__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED}}}, + mountElementId: '{{{mountElementId}}}' + }; 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); +export const getAppRootElement = createAppRootElement.bind(null, createOpts)(); + export default requestHandler; + +export const g_umi = '{{{version}}}' + +{{{ entryCode }}} diff --git a/packages/preset-umi/templates/umi.tpl b/packages/preset-umi/templates/umi.tpl index 46761ff0d61e..8df0f745fe53 100644 --- a/packages/preset-umi/templates/umi.tpl +++ b/packages/preset-umi/templates/umi.tpl @@ -56,6 +56,7 @@ async function render() { routes, routeComponents, pluginManager, + mountElementId: '{{{mountElementId}}}', rootElement: contextOpts.rootElement || document.getElementById('{{{ mountElementId }}}'), {{#loadingComponent}} loadingComponent: Loading, @@ -65,6 +66,7 @@ async function render() { history, historyType, basename, + __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {{{__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED}}}, callback: contextOpts.callback, }; const modifiedContext = pluginManager.applyPlugins({ diff --git a/packages/renderer-react/package.json b/packages/renderer-react/package.json index d6e105d18c5b..107a19dfcf71 100644 --- a/packages/renderer-react/package.json +++ b/packages/renderer-react/package.json @@ -29,8 +29,8 @@ "react-router-dom": "6.3.0" }, "devDependencies": { - "react": "18.1.0", - "react-dom": "18.1.0" + "react": "18.3.1", + "react-dom": "18.3.1" }, "peerDependencies": { "react": ">=16.8", diff --git a/packages/renderer-react/src/appContext.ts b/packages/renderer-react/src/appContext.ts index 973526c302ef..e51850fc07f2 100644 --- a/packages/renderer-react/src/appContext.ts +++ b/packages/renderer-react/src/appContext.ts @@ -48,6 +48,8 @@ export function useRouteProps = any>() { } type ServerLoaderFunc = (...args: any[]) => Promise | any; + +// @deprecated Please use `useLoaderData` instead. export function useServerLoaderData() { const routes = useSelectedRoutes(); const { serverLoaderData, basename } = useAppData(); @@ -65,7 +67,6 @@ export function useServerLoaderData() { return has ? ret : undefined; }); React.useEffect(() => { - // @ts-ignore if (!window.__UMI_LOADER_DATA__) { // 支持 ssr 降级,客户端兜底加载 serverLoader 数据 Promise.all( @@ -97,8 +98,20 @@ export function useServerLoaderData() { }; } +// @deprecated Please use `useLoaderData` instead. export function useClientLoaderData() { const route = useRouteData(); const appData = useAppData(); return { data: appData.clientLoaderData[route.route.id] }; } + +export function useLoaderData() { + const serverLoaderData = useServerLoaderData(); + const clientLoaderData = useClientLoaderData(); + return { + data: { + ...serverLoaderData.data, + ...clientLoaderData.data, + }, + } as Awaited>; +} diff --git a/packages/renderer-react/src/browser.tsx b/packages/renderer-react/src/browser.tsx index 2e3adcc7a6f9..2c90cb6a738c 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 @@ -91,11 +91,28 @@ export type RenderClientOpts = { * @doc https://umijs.org/docs/api/config#runtimepublicpath */ runtimePublicPath?: boolean; + /** + * react dom 渲染的的目标节点 id + * @doc 一般不需要改,微前端的时候会变化 + */ + mountElementId?: string; /** * react dom 渲染的的目标 dom * @doc 一般不需要改,微前端的时候会变化 */ rootElement?: HTMLElement; + + __INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { + /** + * 内部流程, 渲染特殊 app 节点, 不要使用!!! + */ + pureApp?: boolean; + /** + * 内部流程, 渲染特殊 html 节点, 不要使用!!! + */ + + pureHtml?: boolean; + }; /** * 当前的路由配置 */ @@ -213,7 +230,6 @@ const getBrowser = ( const Browser = () => { const [clientLoaderData, setClientLoaderData] = useState({}); const [serverLoaderData, setServerLoaderData] = useState( - // @ts-ignore window.__UMI_LOADER_DATA__ || {}, ); @@ -263,9 +279,18 @@ const getBrowser = ( }); } } + const clientLoader = opts.routes[id]?.clientLoader; + const hasClientLoader = !!clientLoader; + const hasServerLoader = opts.routes[id]?.hasServerLoader; // server loader // use ?. since routes patched with patchClientRoutes is not exists in opts.routes - if (!isFirst && opts.routes[id]?.hasServerLoader) { + + if ( + !isFirst && + hasServerLoader && + !hasClientLoader && + !window.__UMI_LOADER_DATA__ + ) { fetchServerLoader({ id, basename, @@ -280,9 +305,36 @@ const getBrowser = ( } // client loader // onPatchClientRoutes 添加的 route 在 opts.routes 里是不存在的 - const clientLoader = opts.routes[id]?.clientLoader; - if (clientLoader && !clientLoaderData[id]) { - clientLoader().then((data: any) => { + const hasClientLoaderDataInRoute = !!clientLoaderData[id]; + + // Check if hydration is needed or there's no server loader for the current route + const shouldHydrateOrNoServerLoader = + (hasClientLoader && clientLoader.hydrate) || !hasServerLoader; + + // Check if server loader data is missing in the global window object + const isServerLoaderDataMissing = + hasServerLoader && !window.__UMI_LOADER_DATA__; + + if ( + hasClientLoader && + !hasClientLoaderDataInRoute && + (shouldHydrateOrNoServerLoader || isServerLoaderDataMissing) + ) { + // ... + clientLoader({ + serverLoader: () => + fetchServerLoader({ + id, + basename, + cb: (data) => { + // setServerLoaderData when startTransition because if ssr is enabled, + // the component may being hydrated and setLoaderData will break the hydration + React.startTransition(() => { + setServerLoaderData((d) => ({ ...d, [id]: data })); + }); + }, + }), + }).then((data: any) => { setClientLoaderData((d: any) => ({ ...d, [id]: data })); }); } @@ -331,12 +383,33 @@ const getBrowser = ( */ export function renderClient(opts: RenderClientOpts) { const rootElement = opts.rootElement || document.getElementById('root')!; + const Browser = getBrowser(opts, ); // 为了测试,直接返回组件 if (opts.components) return Browser; - if (opts.hydrate) { - ReactDOM.hydrateRoot(rootElement, ); + const loaderData = window.__UMI_LOADER_DATA__ || {}; + const metadata = window.__UMI_METADATA_LOADER_DATA__ || {}; + + const hydtateHtmloptions = { + metadata, + loaderData, + mountElementId: opts.mountElementId, + }; + const _isInternal = + opts.__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.pureApp || + opts.__INTERNAL_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.pureHtml; + + ReactDOM.hydrateRoot( + _isInternal ? rootElement : document, + _isInternal ? ( + + ) : ( + + + + ), + ); return; } diff --git a/packages/renderer-react/src/html.tsx b/packages/renderer-react/src/html.tsx new file mode 100644 index 000000000000..95e49de9473f --- /dev/null +++ b/packages/renderer-react/src/html.tsx @@ -0,0 +1,183 @@ +import React from 'react'; +import { IHtmlProps, IScript } from './types'; + +const RE_URL = /^(http:|https:)?\/\//; + +function isUrl(str: string) { + return ( + RE_URL.test(str) || + (str.startsWith('/') && !str.startsWith('/*')) || + str.startsWith('./') || + str.startsWith('../') + ); +} +const EnableJsScript = () => ( +