发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
8 changed files with 211 additions and 13 deletions
Showing only changes of commit a75f7baf1b - Show all commits

View File

@@ -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

View File

@@ -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

View File

@@ -1,11 +1,6 @@
# 端口号
VITE_PORT=5090
VITE_BASE=/flow/
# 接口地址
VITE_GLOB_API_URL=/flow
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false

View File

@@ -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

View File

@@ -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);
});
},
);

View File

@@ -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);
},
);
});

View File

@@ -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',
},
};
}

View File

@@ -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,
},
},
},