发布 v1.10 #5
@@ -58,6 +58,14 @@ pnpm install
|
|||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
管理端开发环境与生产环境统一使用 `/flow/` 基路径和 Hash 路由,默认访问地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:5090/flow/#/
|
||||||
|
```
|
||||||
|
|
||||||
|
直接访问 `http://127.0.0.1:5090` 或 `/flow` 时,开发服务器会自动补齐 `/flow/`。
|
||||||
|
|
||||||
用户中心:
|
用户中心:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
# 应用标题
|
# 应用标题
|
||||||
VITE_APP_TITLE=EasyFlow
|
VITE_APP_TITLE=EasyFlow
|
||||||
|
|
||||||
|
# 前端部署契约:开发与生产统一使用 /flow/ 基路径和 Hash 路由
|
||||||
|
VITE_BASE=/flow/
|
||||||
|
VITE_ROUTER_HISTORY=hash
|
||||||
|
VITE_GLOB_API_URL=/flow
|
||||||
|
|
||||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||||
VITE_APP_NAMESPACE=easyflow-web
|
VITE_APP_NAMESPACE=easyflow-web
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
# 端口号
|
# 端口号
|
||||||
VITE_PORT=5090
|
VITE_PORT=5090
|
||||||
|
|
||||||
VITE_BASE=/flow/
|
|
||||||
|
|
||||||
# 接口地址
|
|
||||||
VITE_GLOB_API_URL=/flow
|
|
||||||
|
|
||||||
# 是否打开 devtools,true 为打开,false 为关闭
|
# 是否打开 devtools,true 为打开,false 为关闭
|
||||||
VITE_DEVTOOLS=false
|
VITE_DEVTOOLS=false
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
VITE_BASE=/flow/
|
|
||||||
|
|
||||||
# 接口地址
|
|
||||||
VITE_GLOB_API_URL=/flow
|
|
||||||
|
|
||||||
# 是否开启压缩,可以设置为 none, brotli, gzip
|
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||||
VITE_COMPRESS=gzip
|
VITE_COMPRESS=gzip
|
||||||
|
|
||||||
# 是否开启 PWA
|
# 是否开启 PWA
|
||||||
VITE_PWA=false
|
VITE_PWA=false
|
||||||
|
|
||||||
# vue-router 的模式
|
|
||||||
VITE_ROUTER_HISTORY=hash
|
|
||||||
|
|
||||||
# 是否注入全局loading
|
# 是否注入全局loading
|
||||||
VITE_INJECT_APP_LOADING=true
|
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 ElementPlus from 'unplugin-element-plus/vite';
|
||||||
|
|
||||||
|
import { createBasePathRedirectPlugin } from './vite-base-path-redirect';
|
||||||
|
|
||||||
export default defineConfig(async () => {
|
export default defineConfig(async () => {
|
||||||
return {
|
return {
|
||||||
application: {},
|
application: {},
|
||||||
@@ -14,6 +16,7 @@ export default defineConfig(async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
createBasePathRedirectPlugin(),
|
||||||
ElementPlus({
|
ElementPlus({
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
}),
|
}),
|
||||||
@@ -22,19 +25,31 @@ export default defineConfig(async () => {
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/flow/api': {
|
'/flow/api': {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
headers: {
|
||||||
|
'X-Forwarded-Prefix': '/flow',
|
||||||
|
},
|
||||||
rewrite: (path) => path.replace(/^\/flow/, ''),
|
rewrite: (path) => path.replace(/^\/flow/, ''),
|
||||||
target: 'http://127.0.0.1:8111',
|
target: 'http://127.0.0.1:8111',
|
||||||
ws: true,
|
ws: true,
|
||||||
|
xfwd: true,
|
||||||
},
|
},
|
||||||
'/flow/public-api': {
|
'/flow/public-api': {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
headers: {
|
||||||
|
'X-Forwarded-Prefix': '/flow',
|
||||||
|
},
|
||||||
rewrite: (path) => path.replace(/^\/flow/, ''),
|
rewrite: (path) => path.replace(/^\/flow/, ''),
|
||||||
target: 'http://127.0.0.1:8111',
|
target: 'http://127.0.0.1:8111',
|
||||||
|
xfwd: true,
|
||||||
},
|
},
|
||||||
'/flow/userCenter': {
|
'/flow/userCenter': {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
headers: {
|
||||||
|
'X-Forwarded-Prefix': '/flow',
|
||||||
|
},
|
||||||
rewrite: (path) => path.replace(/^\/flow/, ''),
|
rewrite: (path) => path.replace(/^\/flow/, ''),
|
||||||
target: 'http://127.0.0.1:8111',
|
target: 'http://127.0.0.1:8111',
|
||||||
|
xfwd: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user