发布 v1.10 #5
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
# 端口号
|
||||
VITE_PORT=5090
|
||||
|
||||
VITE_BASE=/flow/
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=/flow
|
||||
|
||||
# 是否打开 devtools,true 为打开,false 为关闭
|
||||
VITE_DEVTOOLS=false
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
56
easyflow-ui-admin/app/vite-base-path-redirect.test.ts
Normal file
56
easyflow-ui-admin/app/vite-base-path-redirect.test.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
95
easyflow-ui-admin/app/vite-base-path-redirect.ts
Normal file
95
easyflow-ui-admin/app/vite-base-path-redirect.ts
Normal 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',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user