From b04143c6bc505343f1d6edbb646418a0a3cd1e5f Mon Sep 17 00:00:00 2001 From: feirlau Date: Wed, 26 Jun 2024 17:25:36 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81htmlSuffix?= =?UTF-8?q?=E3=80=81dynamicRoot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit htmlSuffix 会生成 xxx.html,非xxx/index.html dynamicRoot可不用做任何修改部署在任意目录下 --- examples/export-static/.umirc.ts | 8 + examples/export-static/package.json | 13 ++ examples/export-static/src/pages/404.tsx | 5 + examples/export-static/src/pages/bar.css | 3 + examples/export-static/src/pages/foo.less | 10 + examples/export-static/src/pages/index.tsx | 34 +++ .../export-static/src/pages/page1/index.tsx | 5 + .../src/pages/page1/page1_1/index.tsx | 5 + .../src/features/exportStatic/exportStatic.ts | 220 ++++++++++++------ pnpm-lock.yaml | 6 + 10 files changed, 238 insertions(+), 71 deletions(-) create mode 100644 examples/export-static/.umirc.ts create mode 100644 examples/export-static/package.json create mode 100644 examples/export-static/src/pages/404.tsx create mode 100644 examples/export-static/src/pages/bar.css create mode 100644 examples/export-static/src/pages/foo.less create mode 100644 examples/export-static/src/pages/index.tsx create mode 100644 examples/export-static/src/pages/page1/index.tsx create mode 100644 examples/export-static/src/pages/page1/page1_1/index.tsx diff --git a/examples/export-static/.umirc.ts b/examples/export-static/.umirc.ts new file mode 100644 index 000000000000..a90436728d03 --- /dev/null +++ b/examples/export-static/.umirc.ts @@ -0,0 +1,8 @@ +export default { + runtimePublicPath: {}, + exportStatic: { + htmlSuffix: true, + dynamicRoot: true, + }, + hash: true, +}; diff --git a/examples/export-static/package.json b/examples/export-static/package.json new file mode 100644 index 000000000000..69f1f8d07148 --- /dev/null +++ b/examples/export-static/package.json @@ -0,0 +1,13 @@ +{ + "name": "@example/export-static", + "private": true, + "scripts": { + "build": "umi build", + "dev": "umi dev", + "setup": "umi setup", + "start": "npm run dev" + }, + "dependencies": { + "umi": "workspace:*" + } +} diff --git a/examples/export-static/src/pages/404.tsx b/examples/export-static/src/pages/404.tsx new file mode 100644 index 000000000000..e7e1cd7a58e5 --- /dev/null +++ b/examples/export-static/src/pages/404.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Page1() { + return

404

; +} diff --git a/examples/export-static/src/pages/bar.css b/examples/export-static/src/pages/bar.css new file mode 100644 index 000000000000..83de5cdab3e2 --- /dev/null +++ b/examples/export-static/src/pages/bar.css @@ -0,0 +1,3 @@ +.bar { + background: green; +} diff --git a/examples/export-static/src/pages/foo.less b/examples/export-static/src/pages/foo.less new file mode 100644 index 000000000000..7a241960dc2f --- /dev/null +++ b/examples/export-static/src/pages/foo.less @@ -0,0 +1,10 @@ + +.foo { + color: red; +} +.foo2 { + font-size: 40px; +} +.foo3 { + font-weight: bold; +} diff --git a/examples/export-static/src/pages/index.tsx b/examples/export-static/src/pages/index.tsx new file mode 100644 index 000000000000..19d5482876a7 --- /dev/null +++ b/examples/export-static/src/pages/index.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +// @ts-ignore +import { Helmet, Link } from 'umi'; +// @ts-ignore +import fooStyles from './foo.less'; +// @ts-ignore +import barStyles from './bar.css'; + +export default function HomePage() { + return ( +
+ + helmet title + +
+ Home Page +
+ +

Page 1(/page1)

+ + +

Page 1(/page1.html)

+ + +

Page 1-1(/page1/page1_1)

+ + +

Page 1-1(/page1/page1_1.html)

+ +
+ ); +} diff --git a/examples/export-static/src/pages/page1/index.tsx b/examples/export-static/src/pages/page1/index.tsx new file mode 100644 index 000000000000..8b2708cc87f4 --- /dev/null +++ b/examples/export-static/src/pages/page1/index.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Page1() { + return

Page 1

; +} diff --git a/examples/export-static/src/pages/page1/page1_1/index.tsx b/examples/export-static/src/pages/page1/page1_1/index.tsx new file mode 100644 index 000000000000..1ed83874fa74 --- /dev/null +++ b/examples/export-static/src/pages/page1/page1_1/index.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Page1() { + return

Page 1-1

; +} diff --git a/packages/preset-umi/src/features/exportStatic/exportStatic.ts b/packages/preset-umi/src/features/exportStatic/exportStatic.ts index 9ceb55d72b19..cbccf356ed9c 100644 --- a/packages/preset-umi/src/features/exportStatic/exportStatic.ts +++ b/packages/preset-umi/src/features/exportStatic/exportStatic.ts @@ -19,26 +19,47 @@ interface IExportHtmlItem { type IUserExtraRoute = string | { path: string; prerender: boolean }; +function isHtmlRoute(route: IRoute): boolean { + const is404 = route.absPath === '/*'; + + if ( + // skip layout + !route.isLayout && + // skip duplicate route + !route.path.endsWith('.html') && + // skip dynamic route for win, because `:` is not allowed in file name + (!IS_WIN || !route.path.includes('/:')) && + // skip `*` route, because `*` is not working for most site serve services + (!route.path.includes('*') || + // except `404.html` + is404) + ) { + return true; + } + return false; +} +function getHtmlPath(path: string, htmlSuffix: boolean): string { + if (!path) return path; + if (path === '/*') return '/404.html'; + if (path === '/') return '/index.html'; + + if (path.endsWith('/')) path = path.slice(0, -1); + return htmlSuffix ? `${path}.html` : `${path}/index.html`; +} /** * get export html data from routes */ -function getExportHtmlData(routes: Record): IExportHtmlItem[] { +function getExportHtmlData( + routes: Record, + htmlSuffix: boolean, +): IExportHtmlItem[] { const map = new Map(); - Object.values(routes).forEach((route) => { + for (const route of Object.values(routes)) { const is404 = route.absPath === '/*'; - if ( - // skip layout - !route.isLayout && - // skip dynamic route for win, because `:` is not allowed in file name - (!IS_WIN || !route.path.includes('/:')) && - // skip `*` route, because `*` is not working for most site serve services - (!route.path.includes('*') || - // except `404.html` - is404) - ) { - const file = is404 ? '404.html' : join('.', route.absPath, 'index.html'); + if (isHtmlRoute(route)) { + const file = join('.', getHtmlPath(route.absPath, htmlSuffix)); map.set(file, { route: { @@ -49,7 +70,7 @@ function getExportHtmlData(routes: Record): IExportHtmlItem[] { file, }); } - }); + } return Array.from(map.values()); } @@ -128,6 +149,8 @@ export default (api: IApi) => { schema: ({ zod }) => zod .object({ + htmlSuffix: zod.boolean(), + dynamicRoot: zod.boolean(), extraRoutePaths: zod.union([ zod.function(), zod.array(zod.string()), @@ -145,7 +168,11 @@ export default (api: IApi) => { // export routes to html files api.modifyExportHTMLFiles(async (_defaultFiles, opts) => { - const { publicPath } = api.config; + const { + publicPath, + base, + exportStatic: { htmlSuffix, dynamicRoot }, + } = api.config; const htmlData = api.appData.exportHtmlData; const htmlFiles: { path: string; content: string }[] = []; const { markupArgs: defaultMarkupArgs } = opts; @@ -164,65 +191,93 @@ export default (api: IApi) => { ), }; } - - // handle relative publicPath, such as `./` - if (publicPath.startsWith('.')) { + let routerBaseStr = JSON.stringify(base || '/'); + let publicPathStr = JSON.stringify(publicPath || '/'); + // handle relative publicPath, such as `./`, same with dynamicRoot + if (publicPath.startsWith('.') || dynamicRoot) { assert( api.config.runtimePublicPath, - '`runtimePublicPath` should be enable when `publicPath` is relative!', + '`runtimePublicPath` should be enable when `publicPath` is relative or `exportStatic.dynamicRoot` is true!', ); - const rltPrefix = relative(dirname(file), '.'); + let pathS = route.path; + const isSlash = pathS.endsWith('/'); + if (pathS === '/404') { + //do nothing + } + // keep the relative path same for route /xxx and /xxx.html + else if (htmlSuffix && isSlash) { + pathS = pathS.slice(0, -1); + } + // keep the relative path same for route /xxx/ and /xxx/index.html + else if (!htmlSuffix && !isSlash) { + pathS = pathS + '/'; + } + + const pathN = Math.max(pathS.split('/').length - 1, 1); + routerBaseStr = `location.pathname.split('/').slice(0, -${pathN}).concat('').join('/')`; + publicPathStr = `location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '') + window.routerBase`; + const rltPrefix = relative(dirname(file), '.'); + const joinRltPrefix = (path: string) => { + if (!rltPrefix || rltPrefix == '.') { + return `.${path.startsWith('/') ? '' : '/'}${path}`; + } + return winPath(join(rltPrefix, path)); + }; // prefix for all assets - if (rltPrefix) { - // HINT: clone for keep original markupArgs unmodified - const picked = lodash.cloneDeep( - lodash.pick(markupArgs, [ - 'favicons', - 'links', - 'styles', - 'headScripts', - 'scripts', - ]), - ); - - // handle favicons - picked.favicons.forEach((item: string, i: number) => { - if (item.startsWith(publicPath)) { - picked.favicons[i] = winPath(join(rltPrefix, item)); - } - }); - - // handle links - picked.links.forEach((link: { href: string }) => { - if (link.href?.startsWith(publicPath)) { - link.href = winPath(join(rltPrefix, link.href)); - } - }); - - // handle scripts - [picked.headScripts, picked.scripts, picked.styles].forEach( - (group: ({ src: string } | string)[]) => { - group.forEach((script, i) => { - if ( - typeof script === 'string' && - script.startsWith(publicPath) - ) { - group[i] = winPath(join(rltPrefix, script)); - } else if ( - typeof script === 'object' && - script.src?.startsWith(publicPath) - ) { - script.src = winPath(join(rltPrefix, script.src)); - } - }); - }, - ); - - // update markupArgs - markupArgs = Object.assign({}, markupArgs, picked); - } + // HINT: clone for keep original markupArgs unmodified + const picked = lodash.cloneDeep( + lodash.pick(markupArgs, [ + 'favicons', + 'links', + 'styles', + 'headScripts', + 'scripts', + ]), + ); + + // handle favicons + picked.favicons.forEach((item: string, i: number) => { + if (item.startsWith(publicPath)) { + picked.favicons[i] = joinRltPrefix(item); + } + }); + + // handle links + picked.links.forEach((link: { href: string }) => { + if (link.href?.startsWith(publicPath)) { + link.href = joinRltPrefix(link.href); + } + }); + + // handle scripts + [picked.headScripts, picked.scripts, picked.styles].forEach( + (group: ({ src: string } | string)[]) => { + group.forEach((script, i) => { + if (typeof script === 'string' && script.startsWith(publicPath)) { + group[i] = joinRltPrefix(script); + } else if ( + typeof script === 'object' && + script.src?.startsWith(publicPath) + ) { + script.src = joinRltPrefix(script.src); + } + }); + }, + ); + + picked.headScripts.unshift( + `window.routerBase = ${routerBaseStr};`, + ` +if(!window.publicPath) { +window.publicPath = ${publicPathStr}; +} + `, + ); + + // update markupArgs + markupArgs = Object.assign({}, markupArgs, picked); } // append html file @@ -246,12 +301,13 @@ export default (api: IApi) => { api.onGenerateFiles(async () => { const { - exportStatic: { extraRoutePaths = [] }, + exportStatic: { extraRoutePaths = [], htmlSuffix }, } = api.config; const extraHtmlData = getExportHtmlData( await getRoutesFromUserExtraPaths(extraRoutePaths), + htmlSuffix, ); - const htmlData = getExportHtmlData(api.appData.routes).concat( + const htmlData = getExportHtmlData(api.appData.routes, htmlSuffix).concat( extraHtmlData, ); @@ -269,6 +325,13 @@ export function modifyClientRenderOpts(memo: any) { hydrate: hydrate && !{{{ ignorePaths }}}.includes(history.location.pathname), }; } + +export function modifyContextOpts(memo: any) { + return { + ...memo, + basename: window.routerBase || memo.basename, + } +} `.trim(), { ignorePaths: JSON.stringify( @@ -281,7 +344,22 @@ export function modifyClientRenderOpts(memo: any) { noPluginDir: true, }); }); - + api.modifyRoutes((routes: Record) => { + const { + exportStatic: { htmlSuffix }, + } = api.config; + // copy / to /index.html and /xxx to /xxx.html or /xxx/index.html + for (const key of Object.keys(routes)) { + const route = routes[key]; + if (isHtmlRoute(route)) { + key = `${key}.html`; + routes[key] = { + ...route, + path: getHtmlPath(route.path, htmlSuffix), + }; + } + } + }); api.addRuntimePlugin(() => { return [`@@/core/exportStaticRuntimePlugin.ts`]; }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1557873f8afd..25531215d2f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -658,6 +658,12 @@ importers: specifier: workspace:* version: link:../../packages/umi + examples/export-static: + dependencies: + umi: + specifier: workspace:* + version: link:../../packages/umi + examples/legacy: dependencies: umi: From 00f90746e2309ea6f33a5c8bd8aff12f4cbee84a Mon Sep 17 00:00:00 2001 From: feirlau Date: Thu, 27 Jun 2024 17:06:47 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E4=B8=8D=E5=8F=AF=E5=8F=98=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E8=B5=8B=E5=80=BC=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/preset-umi/src/features/exportStatic/exportStatic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preset-umi/src/features/exportStatic/exportStatic.ts b/packages/preset-umi/src/features/exportStatic/exportStatic.ts index 62186f3f7d67..d290e4ef5bd9 100644 --- a/packages/preset-umi/src/features/exportStatic/exportStatic.ts +++ b/packages/preset-umi/src/features/exportStatic/exportStatic.ts @@ -322,7 +322,7 @@ export function modifyContextOpts(memo: any) { exportStatic: { htmlSuffix }, } = api.config; // copy / to /index.html and /xxx to /xxx.html or /xxx/index.html - for (const key of Object.keys(routes)) { + for (let key of Object.keys(routes)) { const route = routes[key]; if (isHtmlRoute(route)) { key = `${key}.html`; From 44fbe0b94fd988040237b73ee3618e3d65332109 Mon Sep 17 00:00:00 2001 From: feirlau Date: Thu, 27 Jun 2024 17:25:25 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E4=BB=A3=E7=A0=81=E7=9A=84=E5=8F=AF=E8=AF=BB=E6=80=A7?= =?UTF-8?q?=E5=92=8C=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/exportStatic/exportStatic.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/preset-umi/src/features/exportStatic/exportStatic.ts b/packages/preset-umi/src/features/exportStatic/exportStatic.ts index d290e4ef5bd9..9b458f8a176b 100644 --- a/packages/preset-umi/src/features/exportStatic/exportStatic.ts +++ b/packages/preset-umi/src/features/exportStatic/exportStatic.ts @@ -20,9 +20,7 @@ interface IExportHtmlItem { type IUserExtraRoute = string | { path: string; prerender: boolean }; function isHtmlRoute(route: IRoute): boolean { - const is404 = route.absPath === '/*'; - - if ( + return ( // skip layout !route.isLayout && // skip duplicate route @@ -31,12 +29,9 @@ function isHtmlRoute(route: IRoute): boolean { (!IS_WIN || !route.path.includes('/:')) && // skip `*` route, because `*` is not working for most site serve services (!route.path.includes('*') || - // except `404.html` - is404) - ) { - return true; - } - return false; + // skip `404.html` + route.absPath === '/*') + ); } function getHtmlPath(path: string, htmlSuffix: boolean): string { if (!path) return path; From 2369fc2d0c487b1af5b4794d43c431ce81dde897 Mon Sep 17 00:00:00 2001 From: feirlau Date: Tue, 9 Jul 2024 15:38:58 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0api.modifyRoutes?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E8=BF=94=E5=9B=9E=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/preset-umi/src/features/exportStatic/exportStatic.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/preset-umi/src/features/exportStatic/exportStatic.ts b/packages/preset-umi/src/features/exportStatic/exportStatic.ts index 9b458f8a176b..e07772809a22 100644 --- a/packages/preset-umi/src/features/exportStatic/exportStatic.ts +++ b/packages/preset-umi/src/features/exportStatic/exportStatic.ts @@ -327,6 +327,7 @@ export function modifyContextOpts(memo: any) { }; } } + return routes; }); api.addRuntimePlugin(() => { return [`@@/core/exportStaticRuntimePlugin.ts`]; From 86d10e35bdc62c26216a83a38904c756c2bac901 Mon Sep 17 00:00:00 2001 From: feirlau Date: Fri, 12 Jul 2024 09:48:33 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=BC=8F=E8=B7=AF=E7=94=B1=E4=B8=ADpath=E4=BB=A5'.html'=20?= =?UTF-8?q?=E7=BB=93=E5=B0=BE=E7=9A=84=E5=9C=BA=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复配置式路由中path以'.html' 结束的场景不生成html文件bug --- examples/export-static/.umirc.ts | 6 ++++++ .../preset-umi/src/features/exportStatic/exportStatic.ts | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/export-static/.umirc.ts b/examples/export-static/.umirc.ts index a90436728d03..813d97a77e44 100644 --- a/examples/export-static/.umirc.ts +++ b/examples/export-static/.umirc.ts @@ -5,4 +5,10 @@ export default { dynamicRoot: true, }, hash: true, + // 配置式路由 + // routes: [ + // { path: '/', component: 'index'}, + // { path: '/page1', component: 'page1/index'}, + // { path: '/page1/page1_1', component: 'page1/page1_1/index'}, + // ], }; diff --git a/packages/preset-umi/src/features/exportStatic/exportStatic.ts b/packages/preset-umi/src/features/exportStatic/exportStatic.ts index e07772809a22..0dacf0dfd279 100644 --- a/packages/preset-umi/src/features/exportStatic/exportStatic.ts +++ b/packages/preset-umi/src/features/exportStatic/exportStatic.ts @@ -24,7 +24,7 @@ function isHtmlRoute(route: IRoute): boolean { // skip layout !route.isLayout && // skip duplicate route - !route.path.endsWith('.html') && + !route.noHtml && // skip dynamic route for win, because `:` is not allowed in file name (!IS_WIN || !route.path.includes('/:')) && // skip `*` route, because `*` is not working for most site serve services @@ -324,6 +324,7 @@ export function modifyContextOpts(memo: any) { routes[key] = { ...route, path: getHtmlPath(route.path, htmlSuffix), + noHtml: true, }; } }