# node-proxy **Repository Path**: scedm/node-proxy ## Basic Information - **Project Name**: node-proxy - **Description**: 通过一个 Node.js 反向代理(端口 8199),将两个管理应用暴露在**同一源**(`http://127.0.0.1:8199`)下,根据 URL 路径前缀分发到不同应用 - **Primary Language**: Unknown - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-16 - **Last Updated**: 2026-06-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 同源反向代理架构 ## 概述 通过一个 Node.js 反向代理(端口 8199),将两个 Vben Admin 应用暴露在**同一源**(`http://127.0.0.1:8199`)下,根据 URL 路径前缀分发流量: | 路径 | 目标应用 | 后端端口 | 代码路径 | |------|---------|---------|---------| | `/` 及其他路径 | App A(统一门户) | `https://127.0.0.1:3100` | `~/vue-vben-admin` | | `/heathdata` 前缀 | App B(跳转后的应用) | `https://127.0.0.1:3101` | `~/vue-vben-admin2\vue-vben-admin` | ### 架构图 ``` 浏览器 → http://127.0.0.1:8199/ │ ▼ Node.js 反向代理 (proxy.js) │ ┌───────┴───────┐ ▼ ▼ / → 3100 /heathdata → 3101 (App A) (App B, VITE_PUBLIC_PATH=/heathdata/) ``` ### 可视化架构图 `diagrams/` 目录包含 4 种视觉风格的架构图,涵盖代理路由、WebSocket HMR、Token 共享: | 风格 | 文件 | 预览 | |------|------|------| | 暗黑极客风 | `diagrams/architecture-dark-hacker.svg` | 深色背景,Neon 配色,等宽字体 | | 扁平图标风 | `diagrams/architecture-flat-icon.svg` | 白底,语义箭头,分层布局 | | 工程蓝图风 | `diagrams/architecture-blueprint.svg` | 深蓝底,网格线,青色描边 | | Claude 官方风格 | `diagrams/architecture-claude-official.svg` | 暖奶油色背景,Anthropic 品牌色 | PNG 版本(1920px 宽)同步生成,可直接用于文档和演示。 --- ## 1. 代理服务器 **文件**: `proxy.js` — 完整代码 ```js // ============================================================ // Node.js 反向代理服务器 // 功能:根据请求路径将流量分发到不同的后端服务 // 路径 /heathdata -> 后端 3101 端口(App B,跳转后的应用) // 其他路径 -> 后端 3100 端口(App A,统一门户) // ============================================================ const http = require("http"); const httpProxy = require("http-proxy"); // 创建代理服务器实例 const proxy = httpProxy.createProxyServer({ changeOrigin: true, // 修改请求的 Host 为目标的 Host }); // 全局错误处理 // 注意:WebSocket 错误时 res 可能没有 writeHead 方法,需要做类型判断 proxy.on("error", (err, req, res) => { console.error("代理出错:", err.message); if (typeof res.writeHead === 'function') { res.writeHead(502, { "Content-Type": "text/plain" }); res.end("无法连接到后端服务。"); } }); // HTTP 请求处理 // 根据路径前缀分发流量到不同的后端应用 const server = http.createServer((req, res) => { if (req.url.startsWith("/heathdata")) { // 剥离 /heathdata 前缀,因为 App B 本身不识别此前缀 // 例如: /heathdata/#/dashboard -> /#/dashboard req.url = req.url.replace(/^\/heathdata/, ""); console.log(`转发请求: ${req.url} -> 3101 (App B)`); proxy.web(req, res, { target: "https://127.0.0.1:3101/", secure: false }); } else { // 其他请求转发到 App A(统一门户) console.log(`转发请求: ${req.url} -> 3100 (App A)`); proxy.web(req, res, { target: "https://127.0.0.1:3100/", secure: false }); } }); // WebSocket 升级请求处理 // Vite HMR 使用 WebSocket,需要同步转发 upgrade 请求 server.on("upgrade", (req, socket, head) => { if (req.url.startsWith("/heathdata")) { // 同样需要剥离前缀后转发到 App B req.url = req.url.replace(/^\/heathdata/, ""); proxy.ws(req, socket, head, { target: "https://127.0.0.1:3101/", secure: false }); } else { // 其他 WebSocket 连接转发到 App A proxy.ws(req, socket, head, { target: "https://127.0.0.1:3100/", secure: false }); } }); // 启动服务 server.listen(8199, () => { console.log("Node 反向代理已启动,监听端口 8199"); }); ``` ### 关键配置 - `changeOrigin: true` — 修改 Host 头为目标地址 - `secure: false` — 跳过后端自签名 SSL 证书验证 - 错误处理中判断 `typeof res.writeHead === 'function'` — 避免 WebSocket 错误时调用 HTTP 响应方法导致崩溃 --- ## 2. 同源 Token 共享 ### 原理 两个应用通过同一个代理访问,浏览器视为**同一源**(`http://127.0.0.1:8199`),因此共享 `localStorage`。App B 启动时直接从 localStorage 读取 App A 写入的 token。 ### App B 读取逻辑 **文件**: `src/main.ts` — `syncTokenFromAppA()` ```ts function syncTokenFromAppA() { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('VUE_VBEN_ADMIN__') && key.endsWith('__COMMON__LOCAL__KEY__')) { const raw = localStorage.getItem(key); const decrypted = enableStorageEncryption ? new AesEncryption({ key: cacheCipher.key, iv: cacheCipher.iv }).decryptByAES(raw) : raw; const data = JSON.parse(decrypted); const token = data?.value?.TOKEN__?.value; if (token) { setAuthCache(TOKEN_KEY, token); } break; } } } ``` ### Token 存储结构 App A 登录后,token 存储在 localStorage,key 格式为: ``` {SHORT_NAME}__{MODE}__{VERSION}__COMMON__LOCAL__KEY__ ``` 例如:`VUE_VBEN_ADMIN__DEVELOPMENT__2.9.13__COMMON__LOCAL__KEY__` 值经过 AES 加密(生产环境),解密后结构: ```json { "value": { "TOKEN__": { "value": "eyJhbGciOiJIUz..." }, "USER__INFO__": { "value": { ... } } } } ``` ### 关键依赖 | 模块 | 路径 | |------|------| | `setAuthCache` | `src/utils/auth/index.ts` | | `TOKEN_KEY` | `src/enums/cacheEnum.ts` → `'TOKEN__'` | | `AesEncryption` | `src/utils/cipher.ts` | | `cacheCipher` | `src/settings/encryptionSetting.ts` → key=`_11111000001111@`, iv=`@11111000001111_` | | `enableStorageEncryption` | `src/settings/encryptionSetting.ts` → 生产环境加密 | ### 调用时机 在 `bootstrap()` 中,`setupRouter(router)` 之后、`setupRouterGuard(router)` 之前调用,确保路由守卫执行时 token 已就绪: ```ts async function bootstrap() { // ... 初始化 store、i18n、路由 ... setupRouter(app); syncTokenFromAppA(); // ← 同步 token setupRouterGuard(router); // ← 路由守卫使用 token // ... } ``` --- ## 3. 路径前缀配置(App B) ### VITE_PUBLIC_PATH **文件**: `.env.development` ```env VITE_PUBLIC_PATH = /heathdata/ ``` 此配置让 Vite 在生成所有资源路径时自动添加 `/heathdata/` 前缀,确保代理能正确路由。 ### 路由 Base **文件**: `src/router/index.ts` ```ts history: createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH), ``` 路由使用 Hash 模式,base 路径同样设置为 `/heathdata/`,因此 URL 格式为: ``` http://127.0.0.1:8199/heathdata/#/dashboard http://127.0.0.1:8199/heathdata/#/system/user ``` --- ## 4. Vite 开发服务器配置 ### Vite 开发服务器配置 **文件**: `vite.config.ts`(两个应用相同配置,端口根据 `VITE_PORT` 环境变量区分) ```ts server: { https: true, // HTTPS 访问 https://localhost:5173 host: true, // 可通过局域网 IP 访问 port: 5173, // Vite 端口(实际值由 VITE_PORT 环境变量指定) hmr: { protocol: 'ws', // 强制使用 ws:// 而非 wss://(代理处理 HTTP,避免 SSL 证书问题) clientPort: 8199, // HMR WebSocket 客户端连接到代理端口 8199 }, fs: { allow: ['..'], // 允许访问项目目录外的文件 }, proxy: createProxy(VITE_PROXY), // 开发环境 API 代理(从 .env 加载) } ``` > **HMR 关键点**: Vite 默认根据页面协议(HTTPS)生成 `wss://` 的 WebSocket 连接,但代理处理的是 HTTP。强制设置 `protocol: 'ws'` 生成 `ws://` 连接,避免 WebSocket 握手失败导致页面反复刷新。 ### HTTPS 两个 Vite 开发服务器均启用 HTTPS(使用 Vite 自签名证书),代理通过 `secure: false` 跳过验证。 ### 端口 | 应用 | Vite 端口 | 代理路径 | |------|-----------|---------| | App A | 3100 | `/` | | App B | 3101 | `/heathdata` | | 代理 | 8199 | 统一入口 | --- ## 5. 应用间导航 **文件**: `src/views/dashboard/workbench/components/WorkbenchHeader.vue`(App A) 在 App A 的工作台页面添加跳转链接,使用相对路径导航到 App B: ```vue 点击这里 ``` ### URL 相对路径解析(以 `http://127.0.0.1:8199/#/dashboard/workbench` 为例) | 写法 | 最终 URL | 说明 | |------|---------|------| | `'./heathdata'` | `http://127.0.0.1:8199/heathdata` | 当前目录,依赖当前路径上下文 | | `'/heathdata'` | `http://127.0.0.1:8199/heathdata` | **推荐** — 从根目录开始,无歧义 | | `'http://127.0.0.1:8199/heathdata'` | 同上 | 硬编码 host 和端口,不灵活 | --- ## 6. 启动顺序 ```bash # 1. 启动 Node.js 代理 cd ~/proxy node proxy.js # 2. 启动 App B(3101 端口) cd ~/vue-vben-admin2\vue-vben-admin pnpm dev # 3. 启动 App A(3100 端口) cd ~/vue-vben-admin pnpm dev ``` 访问 `http://127.0.0.1:8199/` → App A,登录后在 App A 工作台点击「点击这里」或直接访问 `http://127.0.0.1:8199/heathdata` → App B。 --- ## 7. 修改的文件清单 | 文件 | 修改内容 | |------|---------| | `~/proxy\proxy.js` | 路径路由、WebSocket 转发、错误处理 | | `~/vue-vben-admin2\vue-vben-admin\.env.development` | `VITE_PUBLIC_PATH = /heathdata/` | | `~/vue-vben-admin2\vue-vben-admin\src\main.ts` | 添加 `syncTokenFromAppA()` | | `~/vue-vben-admin2\vue-vben-admin\src\router\index.ts` | `createWebHashHistory(VITE_PUBLIC_PATH)` | | `~/vue-vben-admin\vite.config.ts` | HMR `protocol: 'ws'`, `clientPort: 8199` | | `~/vue-vben-admin2\vue-vben-admin\vite.config.ts` | HMR `protocol: 'ws'`, `clientPort: 8199` | | `~/vue-vben-admin\src\views\dashboard\workbench\components\WorkbenchHeader.vue` | App B 导航链接 | ## 其他方式. 共享鉴权认证信息 ### 上传认证信息 ``` (function() { var input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = function(e) { var file = e.target.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(event) { try { // 解析合并后的 JSON 数据 var data = JSON.parse(event.target.result); // 恢复 localStorage 数据 if (data.localStorage) { for (var key in data.localStorage) { localStorage.setItem(key, data.localStorage[key]); } } // 恢复 sessionStorage 数据 if (data.sessionStorage) { for (var key in data.sessionStorage) { sessionStorage.setItem(key, data.sessionStorage[key]); } } alert('✅ 数据导入成功!请刷新页面以使数据生效。'); } catch (error) { alert('❌ 导入失败:JSON 文件格式不正确'); console.error('解析错误:', error); } }; reader.readAsText(file); }; input.click(); })(); ``` ### 下载认证信息 ``` (function() { // 辅助函数:安全地提取 storage 数据转为普通对象 function getStorageData(storage) { var data = {}; for (var i = 0; i < storage.length; i++) { var key = storage.key(i); data[key] = storage.getItem(key); } return data; } // 1. 获取并组合两端数据 var combinedData = { localStorage: getStorageData(localStorage), sessionStorage: getStorageData(sessionStorage) }; var dataString = JSON.stringify(combinedData, null, 2); // 格式化 JSON 以便阅读 // 2. 创建 Blob 对象并生成下载链接 var blob = new Blob([dataString], { type: 'application/json' }); var url = URL.createObjectURL(blob); // 3. 创建临时 a 标签触发下载 var a = document.createElement('a'); a.href = url; a.download = 'browser_storage_backup.json'; document.body.appendChild(a); a.click(); // 4. 清理临时元素 document.body.removeChild(a); URL.revokeObjectURL(url); console.log('✅ Storage 数据备份下载成功!'); })(); ```