fix: 修复开发环境实时连接握手失败

- 开发代理转发线上接口时,WebSocket 握手补 changeOrigin 与 Origin 改写
- 实时连接握手连续失败两次后停止重连
- 版本号升至 4.11.1
This commit is contained in:
Ray.Hao
2026-10-30 17:39:50 +08:00
parent f443eb6bbe
commit 1383b58e80
8 changed files with 83 additions and 154 deletions

View File

@@ -8,9 +8,8 @@ VITE_APP_TITLE=vue3-element-admin
VITE_API_BASE=/dev-api
# 接口地址(开发代理转发目标,默认走线上)
# API_PROXY_TARGET=https://www.youlai.tech # 线上
API_PROXY_TARGET=http://localhost:8000 # 本地
# API_PROXY_TARGET=https://api.youlai.tech/v2 # 多租户线上
API_PROXY_TARGET=https://www.youlai.tech # 线上
# API_PROXY_TARGET=http://localhost:8000 # 本地
# Mock 开关
VITE_MOCK_ENABLED=false
@@ -21,5 +20,5 @@ VITE_TENANT_ENABLED=false
# AI 开关
VITE_AI_ENABLED=false
# OIDC 登录入口,后端需启用相应配置
# 统一认证登录开关
VITE_OIDC_ENABLED=false

View File

@@ -4,11 +4,11 @@ VITE_API_BASE=/prod-api
# 项目名称
VITE_APP_TITLE=vue3-element-admin
# 多租户开关(true:开启 false:关闭)
# 多租户开关
VITE_TENANT_ENABLED=false
# AI 辅助功能开关(true:开启 false:关闭,缺省关闭)
# AI 辅助功能开关
VITE_AI_ENABLED=true
# OIDC 登录入口,后端需启用相应配置
# 统一认证登录开关
VITE_OIDC_ENABLED=false

View File

@@ -1,7 +1,7 @@
{
"name": "vue3-element-admin",
"description": "Vue3 + Vite + TypeScript + Element-Plus 的后台管理模板,vue-element-admin 的 Vue3 版本",
"version": "4.11.0",
"version": "4.11.1",
"private": true,
"type": "module",
"scripts": {
@@ -14,7 +14,6 @@
"lint:eslint": "eslint --cache \"src/**/*.{vue,ts,js}\" --fix",
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html}\"",
"lint:stylelint": "stylelint --cache \"**/*.{css,scss,vue}\" --fix",
"lint:lint-staged": "lint-staged",
"lint": "pnpm run lint:eslint && pnpm run lint:prettier && pnpm run lint:stylelint",
"lint:check": "eslint src mock vite.config.ts uno.config.ts eslint.config.ts",
"format:check": "prettier --check --no-error-on-unmatched-pattern \"src/**/*.{ts,vue,scss,css,json}\" \"mock/**/*.ts\" \"*.{ts,cjs,mjs,json,yaml}\" \".github/workflows/*.{yml,yaml}\"",

View File

@@ -36,7 +36,7 @@ export function configureVxeUI() {
VxeUI.setConfig({
// 全局尺寸
size: "medium",
// 全局 zIndex 起始值,默认是 999,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡
// 全局 zIndex 起始值,默认是 999,如果项目的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡
zIndex: 9999,
// 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据
version: 0,

View File

@@ -115,7 +115,6 @@ export const useTagsViewStore = defineStore("tagsView", () => {
if (index > -1) {
cachedViews.value = cachedViews.value.slice(index, index + 1);
} else {
// if index = -1, there is no cached tags
cachedViews.value = [];
}
resolve([...cachedViews.value]);

View File

@@ -39,6 +39,8 @@ interface ConnectionAttempt {
type MessageHandler = (message: RealtimeServerMessage) => void;
const MAX_RECONNECT_ATTEMPTS = 10;
// 从未握手成功时只重试两次,服务端不可用时直接停止
const MAX_HANDSHAKE_ATTEMPTS = 2;
const MAX_BUFFERED_BYTES = 16_384;
const STOP_RECONNECT_CODES = new Set([1000, 1008, 1009, 4003, 4009]);
@@ -129,6 +131,12 @@ export function createRealtimeClient(options: RealtimeClientOptions) {
}
}
if (!retry && enabled) blocked = true;
// 从未连上时快速放弃,避免服务端不可用时反复重连
if (retry && !attempt.connectedAt && retryCount >= MAX_HANDSHAKE_ATTEMPTS) {
enabled = false;
blocked = true;
return;
}
if (retry) scheduleRetry(minimumDelay);
}

View File

@@ -64,7 +64,7 @@ export default defineConfig({
width: "1em",
height: "1em",
},
// 图表集合
// 图标集合
collections: {
// svg 是图标集合名称,使用 `i-svg:图标名` 调用
svg: FileSystemIconLoader(iconsDir, (svg) => {

View File

@@ -8,6 +8,7 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import { mockDevServerPlugin } from "vite-plugin-mock-dev-server";
import UnoCSS from "unocss/vite";
import { existsSync, readdirSync } from "node:fs";
import { resolve } from "node:path";
import packageInfo from "./package.json" with { type: "json" };
@@ -19,21 +20,13 @@ const __APP_INFO__ = {
const pathSrc = resolve(import.meta.dirname, "src");
export default defineConfig(({ mode, command, isPreview }: ConfigEnv): UserConfig => {
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
const env = loadEnv(mode, import.meta.dirname, "");
const isDevServer = command === "serve" && !isPreview;
const port = Number(env.APP_PORT ?? "3000");
if (!Number.isInteger(port) || port < 1 || port > 65535)
throw new Error("APP_PORT 必须是 1–65535 的整数");
const port = Number(env.APP_PORT) || undefined;
const proxyTarget = env.API_PROXY_TARGET;
if (isDevServer) {
if (!proxyTarget || !/^https?:\/\//.test(proxyTarget))
throw new Error("API_PROXY_TARGET 必须是 HTTP(S) 地址");
new URL(proxyTarget);
}
return {
// 部署在子路径时由 .env.youlai 指定 VITE_BASE_URL,缺省用站点根路径
// 部署在子路径时由环境变量 VITE_BASE_URL 指定,缺省用站点根路径
base: env.VITE_BASE_URL || "/",
resolve: {
alias: {
@@ -48,76 +41,58 @@ export default defineConfig(({ mode, command, isPreview }: ConfigEnv): UserConfi
},
},
},
server: isDevServer
? {
host: "0.0.0.0",
port,
open: true,
proxy: {
"/api/v1/realtime/ws": {
target: proxyTarget,
ws: true,
},
[env.VITE_API_BASE]: {
changeOrigin: true,
target: proxyTarget,
rewrite: (path: string) => path.slice(env.VITE_API_BASE.length),
},
// 保留浏览器访问地址,OIDC 回调经开发代理返回。
"/oauth2": {
changeOrigin: true,
xfwd: true,
target: proxyTarget,
},
"/login/oauth2": {
changeOrigin: true,
xfwd: true,
target: proxyTarget,
},
},
}
: undefined,
server: {
host: "0.0.0.0",
port,
open: true,
proxy: {
[env.VITE_API_BASE]: {
changeOrigin: true,
target: proxyTarget,
rewrite: (path: string) => path.slice(env.VITE_API_BASE.length),
},
// 通知中心 WebSocket,单独代理以完成 ws 升级
"/api/v1/realtime/ws": {
changeOrigin: true,
rewriteWsOrigin: true,
target: proxyTarget,
ws: true,
},
// 统一认证登录的授权跳转与回调,路径不改写,跳转回当前开发地址
"^/(oauth2|login/oauth2)": {
changeOrigin: true,
xfwd: true,
target: proxyTarget,
},
},
},
plugins: [
vue(),
...(env.VITE_MOCK_ENABLED === "true" ? [mockDevServerPlugin()] : []),
UnoCSS(),
// API 自动导入
// 函数自动导入
AutoImport({
// 导入 Vue 函数,如:ref, reactive, toRef 等
imports: ["vue", "@vueuse/core", "pinia", "vue-router", "vue-i18n"],
resolvers: [
// 导入 Element Plus函数,如:ElMessage, ElMessageBox 等
ElementPlusResolver({ importStyle: "sass" }),
],
// Element Plus 函数按需导入
resolvers: [ElementPlusResolver({ importStyle: "sass" })],
vueTemplate: true,
// 类型声明默认不生成,新增自动导入的函数后手动打开并提交一次声明文件
// 默认不生成类型声明,需要时改用下面的 dts 路径
dts: false,
// dts: "types/auto-imports.d.ts",
}),
// 组件自动导入
Components({
resolvers: [
// 导入 Element Plus 组件
// Element Plus 组件按需导入
ElementPlusResolver({ importStyle: "sass" }),
],
// 指定自定义组件位置(默认:src/components)
// 全局组件与页面内组件目录
dirs: ["src/components", "src/**/components"],
// 类型声明默认不生成,新增自动导入的组件后手动打开并提交一次声明文件
// 默认不生成类型声明,需要时改用下面的 dts 路径
dts: false,
// dts: "types/components.d.ts",
}),
// 生成版本号文件
{
name: "dist-version",
apply: "build",
generateBundle() {
this.emitFile({
type: "asset",
fileName: "version.json",
source: JSON.stringify({ version: __APP_INFO__.buildTimestamp }),
});
},
},
distVersionPlugin,
],
// 预加载项目必需的依赖
optimizeDeps: {
@@ -142,90 +117,14 @@ export default defineConfig(({ mode, command, isPreview }: ConfigEnv): UserConfi
"vxe-table",
"path-browserify",
"lodash-es",
"dompurify",
"@element-plus/icons-vue",
"element-plus/es",
"element-plus/es/locale/lang/en",
"element-plus/es/locale/lang/zh-cn",
// Element Plus 组件样式预构建:按 src 实际用到的组件清单硬编码,首启即预加载,
// 避免首次使用某组件时触发依赖重优化导致页面刷新
...[
"alert",
"avatar",
"backtop",
"badge",
"base",
"breadcrumb",
"breadcrumb-item",
"button",
"card",
"cascader",
"checkbox",
"checkbox-group",
"checkbox-button",
"col",
"color-picker",
"config-provider",
"collapse-transition",
"date-picker",
"descriptions",
"descriptions-item",
"dialog",
"divider",
"drawer",
"dropdown",
"dropdown-item",
"dropdown-menu",
"empty",
"form",
"form-item",
"icon",
"image",
"image-viewer",
"input",
"input-number",
"input-tag",
"link",
"loading",
"menu",
"menu-item",
"message",
"message-box",
"notification",
"option",
"pagination",
"popover",
"progress",
"radio",
"radio-button",
"radio-group",
"result",
"row",
"scrollbar",
"select",
"skeleton",
"skeleton-item",
"space",
"step",
"steps",
"sub-menu",
"switch",
"tab-pane",
"table",
"table-column",
"tabs",
"tag",
"text",
"time-picker",
"time-select",
"timeline",
"timeline-item",
"tooltip",
"tree",
"tree-select",
"upload",
"watermark",
].map((c) => `element-plus/es/components/${c}/style/index`),
// 流程设计器与预览器为路由懒加载,bpmn-js 体积大,首启预构建避免进入页面时重优化刷新
// Element Plus 组件样式:按组件目录枚举,新增组件时不会在运行期补优化并刷新页面
...getElementPlusStyleDeps(),
// 流程设计器与预览器为路由懒加载,首启预构建,避免进入页面时重优化刷新
"bpmn-js/lib/Modeler",
"bpmn-js/lib/NavigatedViewer",
"bpmn-js/lib/util/ModelUtil",
@@ -285,3 +184,28 @@ export default defineConfig(({ mode, command, isPreview }: ConfigEnv): UserConfi
},
};
});
// 按组件目录枚举 Element Plus 样式入口,避免手写清单漏项后运行期补优化并刷新页面
function getElementPlusStyleDeps(): string[] {
const componentsDir = resolve(import.meta.dirname, "node_modules/element-plus/es/components");
if (!existsSync(componentsDir)) return [];
return readdirSync(componentsDir, { withFileTypes: true })
.filter(
(entry) =>
entry.isDirectory() && existsSync(resolve(componentsDir, entry.name, "style/index.mjs"))
)
.map((entry) => `element-plus/es/components/${entry.name}/style/index`);
}
// 生成 version.json,供前端检测版本更新
const distVersionPlugin: Plugin = {
name: "dist-version",
apply: "build",
generateBundle() {
this.emitFile({
type: "asset",
fileName: "version.json",
source: JSON.stringify({ version: __APP_INFO__.buildTimestamp }),
});
},
};