fix: 修复开发环境实时连接握手失败
- 开发代理转发线上接口时,WebSocket 握手补 changeOrigin 与 Origin 改写 - 实时连接握手连续失败两次后停止重连 - 版本号升至 4.11.1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}\"",
|
||||
|
||||
@@ -36,7 +36,7 @@ export function configureVxeUI() {
|
||||
VxeUI.setConfig({
|
||||
// 全局尺寸
|
||||
size: "medium",
|
||||
// 全局 zIndex 起始值,默认是 999,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡
|
||||
// 全局 zIndex 起始值,默认是 999,如果项目的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡
|
||||
zIndex: 9999,
|
||||
// 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据
|
||||
version: 0,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export default defineConfig({
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
},
|
||||
// 图表集合
|
||||
// 图标集合
|
||||
collections: {
|
||||
// svg 是图标集合名称,使用 `i-svg:图标名` 调用
|
||||
svg: FileSystemIconLoader(iconsDir, (svg) => {
|
||||
|
||||
208
vite.config.ts
208
vite.config.ts
@@ -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 }),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user