diff --git a/examples/ssg-basename/.umirc.ts b/examples/ssg-basename/.umirc.ts new file mode 100644 index 000000000000..46e48430dff0 --- /dev/null +++ b/examples/ssg-basename/.umirc.ts @@ -0,0 +1,6 @@ +export default { + exportStatic: {}, + ssr: {}, + base: '/base/', + publicPath: '/base/', // 布署时需要布署在 base 文件夹下. +}; diff --git a/examples/ssg-basename/package.json b/examples/ssg-basename/package.json new file mode 100644 index 000000000000..516ef6602368 --- /dev/null +++ b/examples/ssg-basename/package.json @@ -0,0 +1,14 @@ +{ + "name": "@example/ssg-basename", + "private": true, + "description": "该案例用于测试 ssg 预渲染. 当 umi 配置表中设置了 base 后, ssg 输出的预渲染页面中的 a 标签需要有正确的 base 前缀", + "scripts": { + "build": "umi build", + "dev": "umi dev", + "setup": "umi setup", + "start": "npm run dev" + }, + "dependencies": { + "umi": "workspace:*" + } +} diff --git a/examples/ssg-basename/src/layouts/index.tsx b/examples/ssg-basename/src/layouts/index.tsx new file mode 100644 index 000000000000..6ec507a5a7fa --- /dev/null +++ b/examples/ssg-basename/src/layouts/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Outlet } from 'umi'; + +const Layout = () => { + return ( +
+
HEADER
+ +
+ ); +}; + +export default Layout; diff --git a/examples/ssg-basename/src/pages/about/index.tsx b/examples/ssg-basename/src/pages/about/index.tsx new file mode 100644 index 000000000000..e5d53662f556 --- /dev/null +++ b/examples/ssg-basename/src/pages/about/index.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; +import { Link } from 'umi'; + +const About = () => { + const [num, setNum] = useState(0); + + return ( +
+
+ ABOUT page + +
+
+ to home +
+ ); +}; + +export default About; diff --git a/examples/ssg-basename/src/pages/index.tsx b/examples/ssg-basename/src/pages/index.tsx new file mode 100644 index 000000000000..9385801fa5d1 --- /dev/null +++ b/examples/ssg-basename/src/pages/index.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; +import { Link } from 'umi'; + +const Home = () => { + const [num, setNum] = useState(0); + return ( +
+
+ HOME page + +
+ +
+ to about +
+ ); +}; + +export default Home; diff --git a/examples/ssr-export-static-basename/.umirc.ts b/examples/ssr-basename/.umirc.ts similarity index 63% rename from examples/ssr-export-static-basename/.umirc.ts rename to examples/ssr-basename/.umirc.ts index 698110bb984d..6b596c65a98c 100644 --- a/examples/ssr-export-static-basename/.umirc.ts +++ b/examples/ssr-basename/.umirc.ts @@ -1,6 +1,5 @@ export default { ssr: {}, exportStatic: {}, - hash: true, - base: '/a/', + base: '/base/', }; diff --git a/examples/ssr-export-static-basename/package.json b/examples/ssr-basename/package.json similarity index 100% rename from examples/ssr-export-static-basename/package.json rename to examples/ssr-basename/package.json diff --git a/examples/ssr-basename/plugins/mylogger.ts b/examples/ssr-basename/plugins/mylogger.ts new file mode 100644 index 000000000000..8a7f85731fdd --- /dev/null +++ b/examples/ssr-basename/plugins/mylogger.ts @@ -0,0 +1,37 @@ +import { writeFileSync } from 'fs'; +import { IApi } from 'umi'; + +export default (api: IApi) => { + api.describe({ + key: 'mylogger', + config: { + schema(joi) { + return joi.string(); + }, + }, + enableBy: api.EnableBy.register, + }); + + api.registerMethod({ + name: 'mylogger', + fn: (pathname, obj) => { + let cache = []; + const str = JSON.stringify(obj, function (key, value) { + if (typeof value === 'object' && value !== null) { + if (cache.indexOf(value) !== -1) { + // 移除 + return; + } + // 收集所有的值 + cache.push(value); + } + return value; + }); + cache = null; + + writeFileSync(pathname, str, () => { + console.log('write log finish'); + }); + }, + }); +}; diff --git a/examples/ssr-basename/plugins/picker.ts b/examples/ssr-basename/plugins/picker.ts new file mode 100644 index 000000000000..8b87eddef6c2 --- /dev/null +++ b/examples/ssr-basename/plugins/picker.ts @@ -0,0 +1,28 @@ +import type { IApi } from 'umi'; + +export default (api: IApi) => { + api.describe({ + key: 'routePicker', + config: { + schema({ zod }) { + return zod.array(zod.string()); + }, + }, + enableBy: () => { + return api.name === 'dev'; + }, + }); + + console.log('插件 picker 注册'); + + api.onCheck(async () => { + // console.log('插件', api.appData.routes); + console.log('插件config', api.config); + api.config.routes = [ + { + path: '/', + component: 'index', + }, + ]; + }); +}; diff --git a/examples/ssr-basename/src/layouts/index.tsx b/examples/ssr-basename/src/layouts/index.tsx new file mode 100644 index 000000000000..6ec507a5a7fa --- /dev/null +++ b/examples/ssr-basename/src/layouts/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Outlet } from 'umi'; + +const Layout = () => { + return ( +
+
HEADER
+ +
+ ); +}; + +export default Layout; diff --git a/examples/ssr-basename/src/pages/about/form/index.tsx b/examples/ssr-basename/src/pages/about/form/index.tsx new file mode 100644 index 000000000000..f94fd6d91678 --- /dev/null +++ b/examples/ssr-basename/src/pages/about/form/index.tsx @@ -0,0 +1,3 @@ +export default function AboutForm() { + return
About Form
; +} diff --git a/examples/ssr-basename/src/pages/about/index.tsx b/examples/ssr-basename/src/pages/about/index.tsx new file mode 100644 index 000000000000..e5d53662f556 --- /dev/null +++ b/examples/ssr-basename/src/pages/about/index.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; +import { Link } from 'umi'; + +const About = () => { + const [num, setNum] = useState(0); + + return ( +
+
+ ABOUT page + +
+
+ to home +
+ ); +}; + +export default About; diff --git a/examples/ssr-basename/src/pages/index.tsx b/examples/ssr-basename/src/pages/index.tsx new file mode 100644 index 000000000000..9385801fa5d1 --- /dev/null +++ b/examples/ssr-basename/src/pages/index.tsx @@ -0,0 +1,24 @@ +import React, { useState } from 'react'; +import { Link } from 'umi'; + +const Home = () => { + const [num, setNum] = useState(0); + return ( +
+
+ HOME page + +
+ +
+ to about +
+ ); +}; + +export default Home; diff --git a/examples/ssr-export-static-basename/src/pages/about/index.tsx b/examples/ssr-export-static-basename/src/pages/about/index.tsx deleted file mode 100644 index 28f2271f776b..000000000000 --- a/examples/ssr-export-static-basename/src/pages/about/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Link } from 'umi'; - -const About = () => { - return ( -
-
- about page -
-
- to home -
- ); -}; - -export default About; diff --git a/examples/ssr-export-static-basename/src/pages/index.tsx b/examples/ssr-export-static-basename/src/pages/index.tsx deleted file mode 100644 index 683660fae3ba..000000000000 --- a/examples/ssr-export-static-basename/src/pages/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Link } from 'umi'; - -const Home = () => { - return ( -
-
- home page -
-
- to about -
- ); -}; - -export default Home; diff --git a/packages/preset-umi/src/features/exportStatic/exportStatic.ts b/packages/preset-umi/src/features/exportStatic/exportStatic.ts index ae0d2f9be2b2..da12a00e7e63 100644 --- a/packages/preset-umi/src/features/exportStatic/exportStatic.ts +++ b/packages/preset-umi/src/features/exportStatic/exportStatic.ts @@ -60,13 +60,32 @@ function getExportHtmlData(routes: Record): IExportHtmlItem[] { async function getPreRenderedHTML(api: IApi, htmlTpl: string, path: string) { const { exportStatic: { ignorePreRenderError = false }, + base, } = api.config; markupRender ??= require(absServerBuildPath(api))._markupGenerator; try { - const html = await markupRender(path); + const location = `${base.endsWith('/') ? base.slice(0, -1) : base}${path}`; + const markup = await markupRender(location); + 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}`); logger.info(`Pre-render for ${path}`); - return html; + return htmlTpl; } catch (err) { logger.error(`Pre-render ${path} error: ${err}`); if (!ignorePreRenderError) { diff --git a/packages/renderer-react/src/server.tsx b/packages/renderer-react/src/server.tsx index 39fb97b290d1..fb3cb2b07adf 100644 --- a/packages/renderer-react/src/server.tsx +++ b/packages/renderer-react/src/server.tsx @@ -23,7 +23,12 @@ export async function getClientRootComponent(opts: IRootComponentOptions) { routes: clientRoutes, }, }); + let rootContainer = ( + // 这里的 location 需要包含 basename, 否则会影响 StaticRouter 的匹配. + // 由于 getClientRootComponent 方法会同时用于 ssr 和 ssg, 所以在调用该方法时需要注意传入的 location 是否包含 basename. + // 1. 在用于 ssr 时传入的 location 来源于 request.url, 它是包含 basename 的, 所以没有问题. + // 2. 但是在用于 ssg 时(static export), 需要注意传入的 locaiton 要拼接上 basename. diff --git a/packages/server/src/ssr.ts b/packages/server/src/ssr.ts index 8dd62639a38a..75c9c8fdd5be 100644 --- a/packages/server/src/ssr.ts +++ b/packages/server/src/ssr.ts @@ -112,7 +112,8 @@ function createJSXGenerator(opts: CreateRequestHandlerOptions) { }, }); - const matches = matchRoutesForSSR(url, routes); + const matches = matchRoutesForSSR(url, routes, basename); + if (matches.length === 0) { return; } @@ -606,9 +607,19 @@ export function createAppRootElement(opts: CreateRequestHandlerOptions) { }; } -function matchRoutesForSSR(reqUrl: string, routesById: IRoutesById) { +function matchRoutesForSSR( + reqUrl: string, + routesById: IRoutesById, + basename?: string, +) { + // react-router-dom 在 v6.4.0 版本上增加了对 basename 结尾为斜杠的支持 + // 目前 @umijs/server 依赖的 react-router-dom 版本为 v6.3.0 + // 如果传入的 basename 结尾带斜杠, 比如 '/base/', 则会匹配不到. + // 日后如果依赖的版本升级, 此段代码可以删除. + const _basename = basename?.endsWith('/') ? basename.slice(0, -1) : basename; + return ( - matchRoutes(createClientRoutes({ routesById }), reqUrl)?.map( + matchRoutes(createClientRoutes({ routesById }), reqUrl, _basename)?.map( (route: any) => route.route.id, ) || [] ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e8defdd99d8..b08b5012e5b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1153,6 +1153,18 @@ importers: specifier: workspace:* version: link:../../packages/umi + examples/ssg-basename: + dependencies: + umi: + specifier: workspace:* + version: link:../../packages/umi + + examples/ssr-basename: + dependencies: + umi: + specifier: workspace:* + version: link:../../packages/umi + examples/ssr-demo: dependencies: '@ant-design/cssinjs': @@ -1177,12 +1189,6 @@ importers: specifier: workspace:* version: link:../../packages/umi - examples/ssr-export-static-basename: - dependencies: - umi: - specifier: workspace:* - version: link:../../packages/umi - examples/ssr-todos: dependencies: axios: @@ -24340,7 +24346,6 @@ packages: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 - dev: false /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}