From a75f7baf1b54b83dd9a72d1fb7c80c6a9d825c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Fri, 24 Jul 2026 19:00:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=AB=AF=E5=BC=80=E5=8F=91=E4=B8=8E=E9=83=A8=E7=BD=B2=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 开发与生产统一使用 /flow/ 基路径和 Hash 路由 - 自动规范化本地根路径入口并补充契约测试 --- README.md | 8 ++ easyflow-ui-admin/app/.env | 5 + easyflow-ui-admin/app/.env.development | 5 - easyflow-ui-admin/app/.env.production | 8 -- .../__tests__/environment-contract.test.ts | 32 +++++++ .../app/vite-base-path-redirect.test.ts | 56 +++++++++++ .../app/vite-base-path-redirect.ts | 95 +++++++++++++++++++ easyflow-ui-admin/app/vite.config.mts | 15 +++ 8 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 easyflow-ui-admin/app/src/router/__tests__/environment-contract.test.ts create mode 100644 easyflow-ui-admin/app/vite-base-path-redirect.test.ts create mode 100644 easyflow-ui-admin/app/vite-base-path-redirect.ts diff --git a/README.md b/README.md index acb27899..23da2ffd 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,14 @@ pnpm install pnpm dev ``` +管理端开发环境与生产环境统一使用 `/flow/` 基路径和 Hash 路由,默认访问地址: + +```text +http://127.0.0.1:5090/flow/#/ +``` + +直接访问 `http://127.0.0.1:5090` 或 `/flow` 时,开发服务器会自动补齐 `/flow/`。 + 用户中心: ```bash diff --git a/easyflow-ui-admin/app/.env b/easyflow-ui-admin/app/.env index 3961cfb6..bd6e0189 100644 --- a/easyflow-ui-admin/app/.env +++ b/easyflow-ui-admin/app/.env @@ -1,6 +1,11 @@ # 应用标题 VITE_APP_TITLE=EasyFlow +# 前端部署契约:开发与生产统一使用 /flow/ 基路径和 Hash 路由 +VITE_BASE=/flow/ +VITE_ROUTER_HISTORY=hash +VITE_GLOB_API_URL=/flow + # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 VITE_APP_NAMESPACE=easyflow-web diff --git a/easyflow-ui-admin/app/.env.development b/easyflow-ui-admin/app/.env.development index 8f2d43de..62fd471d 100644 --- a/easyflow-ui-admin/app/.env.development +++ b/easyflow-ui-admin/app/.env.development @@ -1,11 +1,6 @@ # 端口号 VITE_PORT=5090 -VITE_BASE=/flow/ - -# 接口地址 -VITE_GLOB_API_URL=/flow - # 是否打开 devtools,true 为打开,false 为关闭 VITE_DEVTOOLS=false diff --git a/easyflow-ui-admin/app/.env.production b/easyflow-ui-admin/app/.env.production index 21169ed3..fc3c549c 100644 --- a/easyflow-ui-admin/app/.env.production +++ b/easyflow-ui-admin/app/.env.production @@ -1,17 +1,9 @@ -VITE_BASE=/flow/ - -# 接口地址 -VITE_GLOB_API_URL=/flow - # 是否开启压缩,可以设置为 none, brotli, gzip VITE_COMPRESS=gzip # 是否开启 PWA VITE_PWA=false -# vue-router 的模式 -VITE_ROUTER_HISTORY=hash - # 是否注入全局loading VITE_INJECT_APP_LOADING=true diff --git a/easyflow-ui-admin/app/src/router/__tests__/environment-contract.test.ts b/easyflow-ui-admin/app/src/router/__tests__/environment-contract.test.ts new file mode 100644 index 00000000..26f334a9 --- /dev/null +++ b/easyflow-ui-admin/app/src/router/__tests__/environment-contract.test.ts @@ -0,0 +1,32 @@ +import { basename, resolve } from 'node:path'; +import process from 'node:process'; + +import { loadEnv } from 'vite'; +import { describe, expect, it } from 'vitest'; + +const APP_ROOT = + basename(process.cwd()) === 'app' + ? process.cwd() + : resolve(process.cwd(), 'app'); +const EXPECTED_DEPLOYMENT_CONTRACT = { + VITE_BASE: '/flow/', + VITE_GLOB_API_URL: '/flow', + VITE_INJECT_APP_LOADING: 'true', + VITE_ROUTER_HISTORY: 'hash', +}; + +describe.each(['development', 'production'])( + '%s environment deployment contract', + (mode) => { + it('uses the same base path, API prefix and router mode', () => { + const env = loadEnv(mode, APP_ROOT, 'VITE_'); + + expect({ + VITE_BASE: env.VITE_BASE, + VITE_GLOB_API_URL: env.VITE_GLOB_API_URL, + VITE_INJECT_APP_LOADING: env.VITE_INJECT_APP_LOADING, + VITE_ROUTER_HISTORY: env.VITE_ROUTER_HISTORY, + }).toEqual(EXPECTED_DEPLOYMENT_CONTRACT); + }); + }, +); diff --git a/easyflow-ui-admin/app/vite-base-path-redirect.test.ts b/easyflow-ui-admin/app/vite-base-path-redirect.test.ts new file mode 100644 index 00000000..82d8f078 --- /dev/null +++ b/easyflow-ui-admin/app/vite-base-path-redirect.test.ts @@ -0,0 +1,56 @@ +import { runInNewContext } from 'node:vm'; + +import { describe, expect, it } from 'vitest'; + +import { + buildBasePathRedirectScript, + resolveBasePathRedirect, +} from './vite-base-path-redirect'; + +describe('vite base path redirect', () => { + it.each([ + ['/', '/flow/'], + ['/flow', '/flow/'], + ['/flow?from=bookmark', '/flow/?from=bookmark'], + ])('redirects %s to %s', (requestUrl, expected) => { + expect(resolveBasePathRedirect(requestUrl, '/flow/')).toBe(expected); + }); + + it.each(['/flow/', '/flow/api/v1/user/info', '/other'])( + 'keeps %s unchanged', + (requestUrl) => { + expect(resolveBasePathRedirect(requestUrl, '/flow/')).toBeNull(); + }, + ); + + it('does not redirect when the application is deployed at root', () => { + expect(resolveBasePathRedirect('/', '/')).toBeNull(); + }); + + it.each([ + ['/', '', '', '/flow/'], + [ + '/flow', + '?from=bookmark', + '#/dashboard', + '/flow/?from=bookmark#/dashboard', + ], + ['/flow/', '', '#/dashboard', null], + ])( + 'normalizes browser entry %s before the router starts', + (pathname, search, hash, expected) => { + const replacements: string[] = []; + runInNewContext(buildBasePathRedirectScript('/flow/'), { + window: { + location: { + hash, + pathname, + replace: (target: string) => replacements.push(target), + search, + }, + }, + }); + expect(replacements[0] ?? null).toBe(expected); + }, + ); +}); diff --git a/easyflow-ui-admin/app/vite-base-path-redirect.ts b/easyflow-ui-admin/app/vite-base-path-redirect.ts new file mode 100644 index 00000000..0324370d --- /dev/null +++ b/easyflow-ui-admin/app/vite-base-path-redirect.ts @@ -0,0 +1,95 @@ +import type { Plugin } from 'vite'; + +function normalizeBasePath(base: string) { + const baseSegment = base.trim().replaceAll(/^\/+|\/+$/g, ''); + return baseSegment ? `/${baseSegment}/` : '/'; +} + +/** + * 解析开发服务器入口地址的规范化跳转目标。 + * + * @param requestUrl 当前请求地址 + * @param base Vite 应用基路径 + * @returns 需要跳转时返回目标地址,否则返回 null + */ +export function resolveBasePathRedirect( + requestUrl: string | undefined, + base: string, +): null | string { + if (!requestUrl) { + return null; + } + const normalizedBase = normalizeBasePath(base); + if (normalizedBase === '/') { + return null; + } + + const request = new URL(requestUrl, 'http://easyflow.local'); + const baseWithoutTrailingSlash = normalizedBase.slice(0, -1); + if ( + request.pathname !== '/' && + request.pathname !== baseWithoutTrailingSlash + ) { + return null; + } + return `${normalizedBase}${request.search}`; +} + +/** + * 创建开发入口规范化脚本,确保 HTML 回退先于前端路由执行跳转。 + * + * @param base Vite 应用基路径 + * @returns 可注入 HTML 头部的同步脚本 + */ +export function buildBasePathRedirectScript(base: string): string { + const normalizedBase = normalizeBasePath(base); + const baseWithoutTrailingSlash = normalizedBase.slice(0, -1); + return `(()=>{const l=window.location;if(${JSON.stringify(normalizedBase)}!=="/"&&(l.pathname==="/"||l.pathname===${JSON.stringify(baseWithoutTrailingSlash)})){l.replace(${JSON.stringify(normalizedBase)}+l.search+l.hash)}})();`; +} + +/** + * 创建开发服务器入口规范化插件。 + * + * Vite 会对浏览器导航请求提前执行 HTML 回退,因此同时使用服务端跳转 + * 与同步头部脚本,保证 `/`、`/flow` 均在路由初始化前进入 `/flow/`。 + * + * @returns Vite 开发服务器插件 + */ +export function createBasePathRedirectPlugin(): Plugin { + let resolvedBase = '/'; + return { + apply: 'serve', + configResolved(config) { + resolvedBase = config.base; + }, + configureServer(server) { + server.middlewares.use((request, response, next) => { + const redirectTarget = resolveBasePathRedirect( + request.url, + server.config.base, + ); + if (!redirectTarget) { + next(); + return; + } + response.statusCode = 302; + response.setHeader('Location', redirectTarget); + response.end(); + }); + }, + enforce: 'pre', + name: 'easyflow-base-path-redirect', + transformIndexHtml: { + handler() { + return [ + { + children: buildBasePathRedirectScript(resolvedBase), + injectTo: 'head-prepend', + tag: 'script', + }, + ]; + }, + order: 'pre', + }, + }; +} diff --git a/easyflow-ui-admin/app/vite.config.mts b/easyflow-ui-admin/app/vite.config.mts index 1d1e04ce..d22afc10 100644 --- a/easyflow-ui-admin/app/vite.config.mts +++ b/easyflow-ui-admin/app/vite.config.mts @@ -2,6 +2,8 @@ import { defineConfig } from '@easyflow/vite-config'; import ElementPlus from 'unplugin-element-plus/vite'; +import { createBasePathRedirectPlugin } from './vite-base-path-redirect'; + export default defineConfig(async () => { return { application: {}, @@ -14,6 +16,7 @@ export default defineConfig(async () => { }, }, plugins: [ + createBasePathRedirectPlugin(), ElementPlus({ format: 'esm', }), @@ -22,19 +25,31 @@ export default defineConfig(async () => { proxy: { '/flow/api': { changeOrigin: true, + headers: { + 'X-Forwarded-Prefix': '/flow', + }, rewrite: (path) => path.replace(/^\/flow/, ''), target: 'http://127.0.0.1:8111', ws: true, + xfwd: true, }, '/flow/public-api': { changeOrigin: true, + headers: { + 'X-Forwarded-Prefix': '/flow', + }, rewrite: (path) => path.replace(/^\/flow/, ''), target: 'http://127.0.0.1:8111', + xfwd: true, }, '/flow/userCenter': { changeOrigin: true, + headers: { + 'X-Forwarded-Prefix': '/flow', + }, rewrite: (path) => path.replace(/^\/flow/, ''), target: 'http://127.0.0.1:8111', + xfwd: true, }, }, },