wangrong
2026-09-26 781539c6bea54b7689baf008e22fb25028729b4a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
 * 前端静态服务器(PM2 托管,对外 :9000)
 * - 托管 dist 静态资源
 * - /api/*    转发后端(去掉 /api 前缀)
 * - /upload/* 转发后端
 * - /socket.io 转发后端(支持 WebSocket)
 */
const path = require('path');
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
 
const PORT = Number(process.env.PORT || 9000);
const BACKEND = process.env.BACKEND || 'http://127.0.0.1:8001';
const DIST = path.join(__dirname, 'dist');
 
const onErr = (name) => (err, req, res) => {
  console.error(`[proxy:${name}]`, err && err.message);
  if (res && res.writeHead) {
    res.writeHead(502, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ code: 502, msg: `backend unreachable: ${name}` }));
  }
};
 
const app = express();
 
// 1) 业务接口:/api/admin/xxx -> /admin/xxx
app.use(
  '/api',
  createProxyMiddleware({
    target: BACKEND,
    changeOrigin: true,
    pathRewrite: { '^/api': '' },
    ws: false,
    onError: onErr('api'),
  })
);
 
// 2) 上传文件
app.use(
  '/upload',
  createProxyMiddleware({
    target: BACKEND,
    changeOrigin: true,
    ws: false,
    onError: onErr('upload'),
  })
);
 
// 3) socket.io(含 WebSocket)
const socketProxy = createProxyMiddleware({
  target: BACKEND,
  changeOrigin: true,
  ws: true,
  onError: onErr('socket.io'),
});
app.use('/socket.io', socketProxy);
 
// 4) 前端静态资源
app.use(
  express.static(DIST, {
    maxAge: '7d',
    index: 'index.html',
    setHeaders(res, filePath) {
      if (filePath.endsWith('index.html')) res.setHeader('Cache-Control', 'no-cache');
    },
  })
);
 
// 5) history 路由兜底(hash 模式不会走到这)
// app.get('*', (req, res, next) => {
//   if (req.path.startsWith('/api') || req.path.startsWith('/upload')) return next();
//   res.sendFile(path.join(DIST, 'index.html'), (err) => err && next(err));
// });
app.use((req, res, next) => {
  if (req.path.startsWith('/api') || req.path.startsWith('/upload')) return next();
  res.sendFile(path.join(DIST, 'index.html'), (err) => err && next(err));
});
 
const server = app.listen(PORT, '0.0.0.0', () => {
  console.log(`[frontend] http://0.0.0.0:${PORT}  ->  backend ${BACKEND}`);
});
 
// WebSocket 升级
server.on('upgrade', socketProxy.upgrade);
 
process.on('unhandledRejection', (r) => console.error('[unhandledRejection]', r && r.message));
process.on('uncaughtException', (e) => console.error('[uncaughtException]', e && e.message));