From fc2d8691e3018e7acb2fd78798c2a6c6dacc07f7 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Mon, 29 Jun 2026 11:17:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AE=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 4 ++- Dockerfile | 20 ++++++++---- docker-compose.yml | 2 +- package.json | 6 ++-- pnpm-lock.yaml | 71 +++++++++++++++++++++++++++++++++++++++++++ server/index.js | 24 +++++++++++++-- vite.config.js | 76 ++++++++++++++++++++++++---------------------- 7 files changed, 155 insertions(+), 48 deletions(-) diff --git a/.env b/.env index ce1e441..ef759af 100644 --- a/.env +++ b/.env @@ -1 +1,3 @@ -VITE_DEVICE_ID = 'agv_src2200' \ No newline at end of file +VITE_DEVICE_ID = 'agv_src2200' + +VITE_SERVICE_PORT = 3000 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f21ae67..92ec9dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,15 +13,23 @@ RUN pnpm run build FROM node:18-alpine WORKDIR /app +# 只拷贝生产需要的文件,保持镜像最小 +COPY package.json package-lock.json pnpm-lock.yaml ./ +RUN npm ci --omit=dev && npm cache clean --force + +# 拷贝 Express 服务代码 +COPY server/ ./server/ + +# 从构建阶段拷贝前端产物 COPY --from=builder /app/dist ./dist -COPY --from=builder /app/server ./server -COPY --from=builder /app/package*.json ./ -COPY --from=builder /app/pnpm-lock.yaml ./ -RUN npm install -g pnpm && pnpm install --prod --frozen-lockfile + # 暴露端口(与 Express 监听端口一致) EXPOSE 3000 +# 环境变量 +ENV NODE_ENV=production +ENV VITE_SERVICE_PORT=3000 + # 启动 Express 服务(使用 pnpm start 或 node server/index.js) -CMD ["pnpm", "start"] -# 或直接 CMD ["node", "server/index.js"] \ No newline at end of file +CMD ["pnpm", "start"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index cb58d26..07989c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: - "3000:3000" # 映射端口 environment: - NODE_ENV=production - - PORT=3000 # 传给 Express 的环境变量 + - VITE_SERVICE_PORT=3000 # 传给 Express 的环境变量 restart: unless-stopped # 容器意外停止后自动重启 # 如果需要挂载日志或配置,可以加 volumes # volumes: diff --git a/package.json b/package.json index b620ff6..3032681 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev:client": "vite", "dev:server": "node server/index.js", - "start": "node server/index.js", + "start": "cross-env NODE_ENV=production node server/index.js", "build": "vite build", - "preview": "vite preview" + "preview": "npm run build && cross-env NODE_ENV=production node server/index.js" }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", @@ -17,6 +17,7 @@ "@grpc/proto-loader": "^0.8.1", "concurrently": "^10.0.3", "cors": "^2.8.6", + "dotenv": "^17.4.2", "element-plus": "^2.14.2", "express": "^5.2.1", "fast-glob": "^3.3.3", @@ -27,6 +28,7 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.7", + "cross-env": "^10.1.0", "sass": "^1.101.0", "unplugin-auto-import": "^21.0.0", "unplugin-vue-components": "^32.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5cf5a94..3a32555 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: cors: specifier: ^2.8.6 version: 2.8.6 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 element-plus: specifier: ^2.14.2 version: 2.14.2(vue@3.5.38) @@ -48,6 +51,9 @@ importers: '@vitejs/plugin-vue': specifier: ^6.0.7 version: 6.0.7(vite@8.1.0(@types/node@26.0.0)(sass@1.101.0)(yaml@2.9.0))(vue@3.5.38) + cross-env: + specifier: ^10.1.0 + version: 10.1.0 sass: specifier: ^1.101.0 version: 1.101.0 @@ -125,6 +131,9 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -709,6 +718,15 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -844,6 +862,10 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1314,6 +1336,9 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isobject@2.1.0: resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} engines: {node: '>=0.10.0'} @@ -1616,6 +1641,10 @@ packages: resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} engines: {node: '>=0.10.0'} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -1861,6 +1890,14 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + shell-quote@1.8.4: resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} @@ -2247,6 +2284,11 @@ packages: resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2342,6 +2384,8 @@ snapshots: tslib: 2.8.1 optional: true + '@epic-web/invariant@1.0.0': {} + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -2913,6 +2957,17 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -3067,6 +3122,8 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv@17.4.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3644,6 +3701,8 @@ snapshots: isarray@2.0.5: {} + isexe@2.0.0: {} + isobject@2.1.0: dependencies: isarray: 1.0.0 @@ -3918,6 +3977,8 @@ snapshots: pascalcase@0.1.1: {} + path-key@3.1.1: {} + path-to-regexp@8.4.2: {} pathe@0.2.0: {} @@ -4223,6 +4284,12 @@ snapshots: setprototypeof@1.2.0: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + shell-quote@1.8.4: {} side-channel-list@1.0.1: @@ -4714,6 +4781,10 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which@2.0.2: + dependencies: + isexe: 2.0.0 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/server/index.js b/server/index.js index fd2c192..b87a663 100644 --- a/server/index.js +++ b/server/index.js @@ -5,6 +5,7 @@ import grpc from '@grpc/grpc-js' import protoLoader from '@grpc/proto-loader' import path from 'path' import { fileURLToPath } from 'url' +import 'dotenv/config'; const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -241,8 +242,27 @@ app.post('/api/agv/stopRobot', async (req, res) => { } }) -// ========== 启动 ========== -const PORT = 3000 +const isProd = process.env.NODE_ENV === 'production'; +const PORT = process.env.VITE_SERVICE_PORT || 3000; + +// ============ 生产环境:托管 Vite 构建产物 ============ +if (isProd) { + const distPath = resolve(__dirname, '../dist'); + + // 托管静态文件 + app.use(express.static(distPath)); + + // SPA 回退:所有非 API 请求返回 index.html + app.get('*', (req, res) => { + res.sendFile(resolve(distPath, 'index.html')); + }); + + app.post('*', (req, res) => { + res.sendFile(resolve(distPath, 'index.html')); + }); +} + +// ========== 启动 ========= app.listen(PORT, () => { console.log(`gRPC 桥接服务已启动: http://localhost:${PORT}`) diff --git a/vite.config.js b/vite.config.js index 1a533b6..3991c85 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' @@ -6,42 +6,46 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import path from 'path' // https://vite.dev/config/ -export default defineConfig({ - resolve: { - alias: { - '@': path.resolve(__dirname, 'src') - } - }, - plugins: [ - vue(), - AutoImport({ - imports: ['vue', 'vue-router', 'pinia'], - resolvers: [ElementPlusResolver()], - dts: true - }), - Components({ - resolvers: [ - ElementPlusResolver({ - importStyle: 'sass' // 使用scss源码,支持自定义主题变量 - }) - ], - dts: true - }), - ], - server: { - port: 5173, - proxy: { - '/api': { - target: 'http://localhost:3001', - changeOrigin: true +export default defineConfig(({ mode, command }) => { + const env = loadEnv(mode, process.cwd()) + const { VITE_SERVICE_PORT } = env + return { + resolve: { + alias: { + '@': path.resolve(__dirname, 'src') } - } - }, - css: { - preprocessorOptions: { - scss: { - // 全局注入scss变量/混合,所有页面直接使用无需手动import - additionalData: `@use "@/styles/vars.scss" as *;` + }, + plugins: [ + vue(), + AutoImport({ + imports: ['vue', 'vue-router', 'pinia'], + resolvers: [ElementPlusResolver()], + dts: true + }), + Components({ + resolvers: [ + ElementPlusResolver({ + importStyle: 'sass' // 使用scss源码,支持自定义主题变量 + }) + ], + dts: true + }), + ], + server: { + port: 5173, + proxy: { + '/api': { + target: `http://localhost:${VITE_SERVICE_PORT}`, + changeOrigin: true + } + } + }, + css: { + preprocessorOptions: { + scss: { + // 全局注入scss变量/混合,所有页面直接使用无需手动import + additionalData: `@use "@/styles/vars.scss" as *;` + } } } }