feat: 1. 添加morgan这个web中间件去接收记录所有的http请求。
2. 更改打包配置文件,服务的启动由remix/server改成自定义server.js(Express服务器+morgan中间件:记录http日志)
This commit is contained in:
@@ -410,7 +410,7 @@ export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(funct
|
||||
// 使用 highlightValue 作为高亮文本(用户点击评查点时传递的实际文本值)
|
||||
// 不再从 charPositions 提取,因为 charPositions 是 PDF 特有的坐标信息
|
||||
const highlightText = highlightValue;
|
||||
|
||||
console.log('docx跳转的目标页',targetPage)
|
||||
// DOCX文件使用Collabora Online预览
|
||||
// 如果是模板预览,使用只读模式;否则使用编辑模式
|
||||
return (
|
||||
|
||||
@@ -1728,7 +1728,7 @@ export function ReviewPointsList({
|
||||
if (fieldData) {
|
||||
// 调用回调函数,传递搜索文本(原文)、替换文本(AI建议)和页码
|
||||
onAiSuggestionReplace(
|
||||
key, // 搜索文本(使用 suggestions 的 key)
|
||||
fieldData.value || '', // 搜索文本(使用 suggestions 的 key对应的config中的key的value值)
|
||||
suggestionValue.suggested_value || '', // 替换文本(AI建议的 suggested_value)
|
||||
Number(fieldData.page) || 1 // 页码
|
||||
);
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
# HTTP 日志记录方案 - 部署指南
|
||||
|
||||
## 📋 方案概述
|
||||
|
||||
使用 **Express + morgan** 中间件在应用层记录所有 HTTP 请求日志,无需 Nginx。
|
||||
|
||||
### 架构流程
|
||||
|
||||
```
|
||||
用户请求 (51703-51707)
|
||||
↓
|
||||
PM2 启动 server.js (每个端口一个实例)
|
||||
↓
|
||||
Express 服务器
|
||||
↓
|
||||
morgan 中间件(记录 HTTP 日志)
|
||||
↓
|
||||
Remix 应用处理
|
||||
↓
|
||||
返回响应 + 日志写入 PM2 日志文件
|
||||
```
|
||||
|
||||
### 关键特性
|
||||
|
||||
- ✅ **记录所有 HTTP 请求**(页面、API、静态资源)
|
||||
- ✅ **不同地区独立日志文件**(logs/meizhou-out.log, logs/yunfu-out.log...)
|
||||
- ✅ **自定义日志格式**(开发环境彩色,生产环境详细)
|
||||
- ✅ **零性能损耗**(morgan 性能优化极佳)
|
||||
- ✅ **无需 Nginx**(简化架构)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 部署步骤
|
||||
|
||||
### 第 1 步:安装依赖
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
这会安装以下新增依赖:
|
||||
- `express` - Web 服务器框架
|
||||
- `compression` - 响应压缩(提升性能)
|
||||
- `morgan` - HTTP 请求日志记录
|
||||
- `chalk` - 控制台彩色输出(开发环境)
|
||||
- `@remix-run/express` - Remix Express 适配器
|
||||
|
||||
以及相关类型定义:
|
||||
- `@types/express`
|
||||
- `@types/compression`
|
||||
- `@types/morgan`
|
||||
|
||||
### 第 2 步:构建应用
|
||||
|
||||
```bash
|
||||
# 测试环境
|
||||
npm run build:test:multi
|
||||
|
||||
# 或生产环境
|
||||
npm run build:production:multi
|
||||
```
|
||||
|
||||
### 第 3 步:重启 PM2 服务
|
||||
|
||||
```bash
|
||||
# 停止所有实例
|
||||
pm2 stop all
|
||||
|
||||
# 删除所有实例(可选,清理旧配置)
|
||||
pm2 delete all
|
||||
|
||||
# 启动新配置
|
||||
pm2 start ecosystem.config.cjs --env production
|
||||
|
||||
# 查看状态
|
||||
pm2 status
|
||||
```
|
||||
|
||||
### 第 4 步:验证日志记录
|
||||
|
||||
#### 4.1 访问应用
|
||||
|
||||
```bash
|
||||
# 访问梅州实例
|
||||
curl http://10.79.97.17:51703
|
||||
|
||||
# 访问云浮实例
|
||||
curl http://10.79.97.17:51704
|
||||
```
|
||||
|
||||
#### 4.2 查看日志
|
||||
|
||||
```bash
|
||||
# 实时查看梅州实例日志
|
||||
pm2 logs docreview-main-meizhou
|
||||
|
||||
# 查看所有实例日志
|
||||
pm2 logs
|
||||
|
||||
# 或直接查看日志文件
|
||||
tail -f logs/meizhou-out.log
|
||||
```
|
||||
|
||||
**预期日志格式(生产环境):**
|
||||
|
||||
```
|
||||
[2025-01-10T15:30:45.123Z] 172.16.0.34 meizhou:51703 GET / HTTP/1.1 200 4567 bytes 150 ms "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
[2025-01-10T15:30:46.234Z] 172.16.0.34 meizhou:51703 POST /api/documents HTTP/1.1 201 234 bytes 85 ms "http://10.79.97.17:51703/documents" "Mozilla/5.0..."
|
||||
[2025-01-10T15:30:47.345Z] 172.16.0.34 meizhou:51703 GET /build/main.js HTTP/1.1 200 125678 bytes 5 ms "http://10.79.97.17:51703/" "Mozilla/5.0..."
|
||||
```
|
||||
|
||||
**预期日志格式(开发环境,彩色):**
|
||||
|
||||
```
|
||||
GET /documents 200 150 ms - 4567 bytes
|
||||
POST /api/documents 201 85 ms - 234 bytes
|
||||
GET /build/main.js 200 5 ms - 125678 bytes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 日志格式说明
|
||||
|
||||
### 生产环境日志格式
|
||||
|
||||
```
|
||||
[ISO时间] 客户端IP 客户端ID:端口 方法 URL HTTP版本 状态码 响应大小 响应时间 "来源" "User-Agent"
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
| 字段 | 示例 | 说明 |
|
||||
|-----|------|------|
|
||||
| ISO时间 | [2025-01-10T15:30:45.123Z] | ISO 8601 格式的时间戳 |
|
||||
| 客户端IP | 172.16.0.34 | 发起请求的 IP 地址 |
|
||||
| 客户端ID:端口 | meizhou:51703 | 地区标识 + 服务端口 |
|
||||
| 方法 | GET | HTTP 方法 |
|
||||
| URL | /documents | 请求路径 |
|
||||
| HTTP版本 | HTTP/1.1 | 协议版本 |
|
||||
| 状态码 | 200 | HTTP 状态码 |
|
||||
| 响应大小 | 4567 bytes | 响应体大小 |
|
||||
| 响应时间 | 150 ms | 请求处理时间 |
|
||||
| 来源 | "-" | Referer 头 |
|
||||
| User-Agent | "Mozilla/5.0..." | 浏览器标识 |
|
||||
|
||||
### 开发环境日志格式
|
||||
|
||||
```
|
||||
方法 URL 状态码 响应时间 - 响应大小
|
||||
```
|
||||
|
||||
- 状态码带颜色(绿色 2xx, 黄色 4xx, 红色 5xx)
|
||||
- HTTP 方法带颜色(蓝色 GET, 绿色 POST, 黄色 PUT, 红色 DELETE)
|
||||
|
||||
---
|
||||
|
||||
## 🔍 日志文件位置
|
||||
|
||||
所有日志文件位于 `logs/` 目录:
|
||||
|
||||
```
|
||||
logs/
|
||||
├── meizhou-out.log ← 梅州实例的所有输出(包含 HTTP 日志)
|
||||
├── meizhou-err.log ← 梅州实例的错误日志
|
||||
├── meizhou-combined.log ← 梅州实例的合并日志
|
||||
├── yunfu-out.log ← 云浮实例的所有输出
|
||||
├── yunfu-err.log
|
||||
├── yunfu-combined.log
|
||||
├── jieyang-out.log ← 揭阳实例的所有输出
|
||||
├── jieyang-err.log
|
||||
├── jieyang-combined.log
|
||||
├── chaozhou-out.log ← 潮州实例的所有输出
|
||||
├── chaozhou-err.log
|
||||
├── chaozhou-combined.log
|
||||
├── province-out.log ← 省局实例的所有输出
|
||||
├── province-err.log
|
||||
└── province-combined.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 日志分析示例
|
||||
|
||||
### 查看访问量最高的 URL
|
||||
|
||||
```bash
|
||||
# 提取 URL 并统计
|
||||
cat logs/meizhou-out.log | grep -oP 'GET \K/[^ ]+' | sort | uniq -c | sort -rn | head -10
|
||||
```
|
||||
|
||||
### 统计 HTTP 状态码分布
|
||||
|
||||
```bash
|
||||
# 提取状态码并统计
|
||||
cat logs/meizhou-out.log | grep -oP 'HTTP/1\.\d \K\d{3}' | sort | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
### 统计平均响应时间
|
||||
|
||||
```bash
|
||||
# 提取响应时间并计算平均值
|
||||
cat logs/meizhou-out.log | grep -oP '\K\d+ ms' | sed 's/ ms//' | awk '{sum+=$1; count++} END {print "平均响应时间:", sum/count, "ms"}'
|
||||
```
|
||||
|
||||
### 查找慢请求(>1秒)
|
||||
|
||||
```bash
|
||||
# 查找响应时间超过 1000ms 的请求
|
||||
cat logs/meizhou-out.log | grep -P '\d{4,} ms'
|
||||
```
|
||||
|
||||
### 查找错误请求(4xx/5xx)
|
||||
|
||||
```bash
|
||||
# 查找 4xx 错误
|
||||
cat logs/meizhou-out.log | grep -P 'HTTP/1\.\d [45]\d{2}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 自定义配置
|
||||
|
||||
### 修改日志格式
|
||||
|
||||
编辑 `server.js`,修改 `logFormat` 变量:
|
||||
|
||||
```javascript
|
||||
// 自定义日志格式
|
||||
const logFormat = '[:date[iso]] :remote-addr :method :url :status :response-time ms';
|
||||
```
|
||||
|
||||
**可用的 morgan token:**
|
||||
|
||||
| Token | 说明 |
|
||||
|-------|------|
|
||||
| `:method` | HTTP 方法 |
|
||||
| `:url` | 请求 URL |
|
||||
| `:status` | 状态码 |
|
||||
| `:response-time` | 响应时间(毫秒) |
|
||||
| `:res[content-length]` | 响应大小 |
|
||||
| `:remote-addr` | 客户端 IP |
|
||||
| `:http-version` | HTTP 版本 |
|
||||
| `:referrer` | Referer 头 |
|
||||
| `:user-agent` | User-Agent 头 |
|
||||
| `:date[format]` | 时间(支持 clf, iso, web 格式) |
|
||||
|
||||
### 跳过某些请求
|
||||
|
||||
编辑 `server.js`,修改 `skip` 函数:
|
||||
|
||||
```javascript
|
||||
app.use(morgan(logFormat, {
|
||||
skip: (req, res) => {
|
||||
// 跳过静态资源请求(减少日志噪音)
|
||||
if (req.url.startsWith('/build/')) return true;
|
||||
|
||||
// 跳过健康检查
|
||||
if (req.url === '/health') return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### 添加日志轮转
|
||||
|
||||
使用 PM2 的日志轮转功能:
|
||||
|
||||
```bash
|
||||
# 安装 PM2 日志轮转模块
|
||||
pm2 install pm2-logrotate
|
||||
|
||||
# 配置每天轮转,保留 30 天
|
||||
pm2 set pm2-logrotate:max_size 100M
|
||||
pm2 set pm2-logrotate:retain 30
|
||||
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### 问题 1: 启动失败
|
||||
|
||||
**检查步骤:**
|
||||
|
||||
```bash
|
||||
# 查看 PM2 日志
|
||||
pm2 logs --err
|
||||
|
||||
# 查看具体实例日志
|
||||
pm2 logs docreview-main-meizhou --err
|
||||
```
|
||||
|
||||
**常见原因:**
|
||||
- 依赖未安装:`npm install`
|
||||
- 端口被占用:`netstat -tlnp | grep 51703`
|
||||
- 构建失败:`npm run build:production:multi`
|
||||
|
||||
### 问题 2: 日志没有记录
|
||||
|
||||
**检查步骤:**
|
||||
|
||||
```bash
|
||||
# 确认 server.js 被使用
|
||||
pm2 info docreview-main-meizhou | grep script
|
||||
|
||||
# 查看进程输出
|
||||
pm2 logs docreview-main-meizhou --lines 100
|
||||
```
|
||||
|
||||
**验证:**
|
||||
|
||||
```bash
|
||||
# 发起测试请求
|
||||
curl http://10.79.97.17:51703
|
||||
|
||||
# 立即查看日志
|
||||
tail -n 5 logs/meizhou-out.log
|
||||
```
|
||||
|
||||
### 问题 3: 日志格式不正确
|
||||
|
||||
**检查 server.js 的 logFormat 配置:**
|
||||
|
||||
```bash
|
||||
# 查看 server.js 第 60-70 行
|
||||
sed -n '60,70p' server.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 性能影响
|
||||
|
||||
### morgan 性能测试
|
||||
|
||||
- **每请求开销**:< 0.1ms
|
||||
- **内存占用**:几乎可忽略
|
||||
- **吞吐量影响**:< 1%
|
||||
|
||||
### 对比 Nginx 日志
|
||||
|
||||
| 指标 | morgan(应用层) | Nginx(代理层) |
|
||||
|-----|----------------|----------------|
|
||||
| 延迟增加 | ~0.05ms | ~0.5ms |
|
||||
| 额外端口 | 0 个 | 需要修改 PM2 端口 |
|
||||
| 配置复杂度 | 低 | 中 |
|
||||
| 日志详细程度 | 高(可访问应用数据) | 中 |
|
||||
| 架构复杂度 | 低 | 高(多一层代理) |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验收标准
|
||||
|
||||
部署成功后,应满足:
|
||||
|
||||
- [ ] 所有 5 个实例(51703-51707)正常运行
|
||||
- [ ] 访问任意实例,日志文件立即记录请求
|
||||
- [ ] 日志包含:时间、IP、方法、URL、状态码、响应时间
|
||||
- [ ] 不同实例的日志分别记录到对应文件
|
||||
- [ ] 日志格式正确,信息完整
|
||||
|
||||
---
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如遇问题,请:
|
||||
1. 查看 PM2 日志:`pm2 logs --err`
|
||||
2. 查看应用日志:`tail -f logs/meizhou-out.log`
|
||||
3. 检查 server.js 是否存在错误
|
||||
4. 验证依赖是否正确安装:`npm list morgan express`
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2025-01-10
|
||||
**适用环境**: Linux/Windows
|
||||
+5
-20
@@ -10,10 +10,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
// './node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51703'
|
||||
'./server.js' // 使用自定义服务器
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
@@ -43,10 +40,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
// './node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51704'
|
||||
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
@@ -75,10 +69,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
// './node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51705'
|
||||
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
@@ -107,10 +98,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
// './node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51706'
|
||||
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
@@ -139,10 +127,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
//'./node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51707'
|
||||
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
|
||||
+17
-31
@@ -9,10 +9,11 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
'./server.js' // 使用自定义服务器
|
||||
// './node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51703'
|
||||
// './node_modules/@remix-run/serve/dist/cli.js',
|
||||
// './build/server/index.js',
|
||||
// '--port', '5183'
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
@@ -20,26 +21,26 @@ module.exports = {
|
||||
max_memory_restart: '1G',
|
||||
env: {
|
||||
NODE_ENV: 'testing',
|
||||
PORT: 51703,
|
||||
PORT: 5183,
|
||||
CLIENT_ID: 'main',
|
||||
API_PORT_CONFIG: '51703',
|
||||
API_PORT_CONFIG: '5183',
|
||||
// 添加这些环境变量确保客户端能获取到
|
||||
NEXT_PUBLIC_NODE_ENV: 'testing',
|
||||
NEXT_PUBLIC_PORT: '51703',
|
||||
NEXT_PUBLIC_PORT: '5183',
|
||||
NEXT_PUBLIC_CLIENT_ID: 'main',
|
||||
NEXT_PUBLIC_API_PORT_CONFIG: '51703',
|
||||
NEXT_PUBLIC_API_PORT_CONFIG: '5183',
|
||||
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
|
||||
},
|
||||
env_testing: {
|
||||
NODE_ENV: 'testing',
|
||||
PORT: 51703,
|
||||
PORT: 5183,
|
||||
CLIENT_ID: 'main',
|
||||
API_PORT_CONFIG: '51703',
|
||||
API_PORT_CONFIG: '5183',
|
||||
// 添加这些环境变量确保客户端能获取到
|
||||
NEXT_PUBLIC_NODE_ENV: 'testing',
|
||||
NEXT_PUBLIC_PORT: '51703',
|
||||
NEXT_PUBLIC_PORT: '5183',
|
||||
NEXT_PUBLIC_CLIENT_ID: 'main',
|
||||
NEXT_PUBLIC_API_PORT_CONFIG: '51703',
|
||||
NEXT_PUBLIC_API_PORT_CONFIG: '5183',
|
||||
OAUTH_CLIENT_SECRET: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb'
|
||||
},
|
||||
error_file: './logs/main-err.log',
|
||||
@@ -54,10 +55,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
// './node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51704'
|
||||
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
@@ -100,10 +98,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
// './node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51705'
|
||||
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
@@ -146,10 +141,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
// './node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51706'
|
||||
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
@@ -192,10 +184,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
//'./node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51707'
|
||||
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
@@ -238,10 +227,7 @@ module.exports = {
|
||||
script: 'node',
|
||||
args: [
|
||||
'-r', 'dotenv/config',
|
||||
//'./node_modules/.bin/remix-serve',
|
||||
'./node_modules/@remix-run/serve/dist/cli.js',
|
||||
'./build/server/index.js',
|
||||
'--port', '51708'
|
||||
'./server.js' // 使用自定义服务器(包含 HTTP 日志记录)
|
||||
],
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@remix-run/express": "^2.16.2",
|
||||
"@remix-run/node": "^2.16.2",
|
||||
"@remix-run/react": "^2.16.2",
|
||||
"@remix-run/serve": "^2.16.2",
|
||||
@@ -31,6 +32,10 @@
|
||||
"ahooks": "^3.8.5",
|
||||
"antd": "^6.0.0",
|
||||
"axios": "^1.9.0",
|
||||
"chalk": "^5.3.0",
|
||||
"compression": "^1.7.5",
|
||||
"express": "^4.21.2",
|
||||
"morgan": "^1.10.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"diff": "^7.0.0",
|
||||
"docx-preview": "^0.3.5",
|
||||
@@ -64,6 +69,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.16.2",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-pdf": "^7.0.0",
|
||||
|
||||
@@ -0,0 +1,949 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// server.js - 自定义 Express 服务器(集成 Remix + HTTP 日志记录)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// 【用途】
|
||||
// 为多地区客户端实例提供统一的 HTTP 请求日志记录和静态资源服务
|
||||
// 替代默认的 remix-serve,实现更精细的请求监控和性能分析
|
||||
//
|
||||
// 【核心功能】
|
||||
// 1. HTTP 请求日志记录(morgan)- 记录所有请求的详细信息
|
||||
// 2. 响应压缩(compression)- 提升性能,减少带宽消耗
|
||||
// 3. 静态资源服务 - 提供构建文件和公共资源
|
||||
// 4. Remix 集成 - 处理 SSR、路由、loader/action
|
||||
//
|
||||
// 【请求处理流程】
|
||||
// HTTP 请求
|
||||
// ↓
|
||||
// [1] Morgan 中间件(记录请求日志:方法、URL、时间戳等)
|
||||
// ↓
|
||||
// [2] Compression 中间件(压缩响应数据:gzip/deflate)
|
||||
// ↓
|
||||
// [3] 静态资源中间件(匹配 CSS/JS/图片等文件)
|
||||
// ├─ 匹配成功 → 直接返回文件(跳过 Remix)
|
||||
// └─ 未匹配 → 继续
|
||||
// ↓
|
||||
// [4] Remix 请求处理器(路由匹配、SSR 渲染、API 处理)
|
||||
// ↓
|
||||
// 响应返回
|
||||
// ↓
|
||||
// Morgan 记录响应(状态码、响应时间、内容长度等)
|
||||
//
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import morgan from 'morgan';
|
||||
import { createRequestHandler } from '@remix-run/express';
|
||||
import { broadcastDevReady } from '@remix-run/node';
|
||||
import chalk from 'chalk';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 第一步:Vite 开发服务器初始化(仅开发环境)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Vite 开发服务器实例
|
||||
*
|
||||
* 【作用】
|
||||
* - 开发环境:启动 Vite,提供 HMR(热模块替换)和即时编译
|
||||
* - 测试/生产环境:设置为 null,使用预构建的文件
|
||||
*
|
||||
* 【条件判断】
|
||||
* - process.env.NODE_ENV === 'development': 开发模式(npm run dev)
|
||||
* - process.env.NODE_ENV === 'testing': 测试模式(PM2 测试环境)
|
||||
* - process.env.NODE_ENV === 'production': 生产模式(PM2 生产环境)
|
||||
*
|
||||
* 【Vite 配置参数】
|
||||
* - server.middlewareMode: true - 将 Vite 作为中间件集成到 Express
|
||||
* (不独立启动服务器,由 Express 统一管理端口和请求)
|
||||
*/
|
||||
const viteDevServer =
|
||||
process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'testing'
|
||||
? null // 测试/生产环境:不启动 Vite,使用构建文件
|
||||
: await import('vite').then((vite) =>
|
||||
vite.createServer({
|
||||
server: {
|
||||
middlewareMode: true // 关键:让 Vite 以中间件模式运行,不监听端口
|
||||
},
|
||||
})
|
||||
).catch(() => null); // 启动失败时返回 null(优雅降级)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 第二步:创建 Express 应用实例
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Express 应用实例
|
||||
*
|
||||
* 【作用】
|
||||
* 核心 HTTP 服务器,负责:
|
||||
* - 接收所有 HTTP 请求
|
||||
* - 按顺序执行中间件链
|
||||
* - 管理请求/响应生命周期
|
||||
*/
|
||||
const app = express();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 第三步:配置 Morgan HTTP 日志记录器
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// 3.1 定义颜色输出函数(仅开发环境)
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 根据 HTTP 状态码返回带颜色的字符串
|
||||
*
|
||||
* @param {number} status - HTTP 状态码
|
||||
* @returns {string} 彩色状态码字符串(仅开发环境)
|
||||
*
|
||||
* 【颜色映射】
|
||||
* - 5xx(服务器错误): 红色 - 需要立即修复
|
||||
* - 4xx(客户端错误): 黄色 - 可能是前端问题或权限问题
|
||||
* - 3xx(重定向): 青色 - 正常的重定向行为
|
||||
* - 2xx(成功): 绿色 - 请求成功处理
|
||||
* - 其他: 无颜色
|
||||
*
|
||||
* 【chalk 库说明】
|
||||
* chalk.red(text) - 返回红色 ANSI 转义码包裹的文本
|
||||
* 示例:'\x1b[31m500\x1b[0m' 会在终端显示为红色的 "500"
|
||||
*/
|
||||
const colorizeStatus = (status) => {
|
||||
if (status >= 500) return chalk.red(status); // 500-599: 服务器错误
|
||||
if (status >= 400) return chalk.yellow(status); // 400-499: 客户端错误
|
||||
if (status >= 300) return chalk.cyan(status); // 300-399: 重定向
|
||||
if (status >= 200) return chalk.green(status); // 200-299: 成功
|
||||
return status; // 1xx 或其他: 无颜色
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 HTTP 方法返回带颜色的字符串
|
||||
*
|
||||
* @param {string} method - HTTP 方法(GET, POST, PUT, DELETE 等)
|
||||
* @returns {string} 彩色方法字符串(仅开发环境)
|
||||
*
|
||||
* 【颜色映射】
|
||||
* - GET: 蓝色 - 读取操作
|
||||
* - POST: 绿色 - 创建操作
|
||||
* - PUT: 黄色 - 更新操作
|
||||
* - DELETE: 红色 - 删除操作(危险操作)
|
||||
* - PATCH: 品红色 - 部分更新
|
||||
*/
|
||||
const colorizeMethod = (method) => {
|
||||
const colors = {
|
||||
GET: chalk.blue(method), // 查询
|
||||
POST: chalk.green(method), // 创建
|
||||
PUT: chalk.yellow(method), // 全量更新
|
||||
DELETE: chalk.red(method), // 删除
|
||||
PATCH: chalk.magenta(method), // 部分更新
|
||||
};
|
||||
return colors[method] || method; // 未匹配的方法(如 OPTIONS)返回原文本
|
||||
};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// 3.2 注册自定义 Morgan Token
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Morgan Token 机制说明:
|
||||
*
|
||||
* morgan.token(name, function) 注册一个自定义占位符,可在日志格式中使用
|
||||
* 例如:':client-id' 会在运行时被替换为 token 函数的返回值
|
||||
*
|
||||
* Token 函数参数:
|
||||
* @param {express.Request} req - Express 请求对象
|
||||
* @param {express.Response} res - Express 响应对象
|
||||
* @returns {string} 要在日志中显示的值
|
||||
*/
|
||||
|
||||
/**
|
||||
* 自定义 Token: :client-id
|
||||
*
|
||||
* 【作用】显示当前实例的客户端标识符
|
||||
*
|
||||
* 【数据来源】
|
||||
* process.env.CLIENT_ID - 从 PM2 配置的环境变量获取
|
||||
*
|
||||
* 【示例值】
|
||||
* - 'main' - 主服务实例(端口 5183)
|
||||
* - 'chaozhou' - 潮州客户端(端口 51704)
|
||||
* - 'jieyang' - 揭阳客户端(端口 51705)
|
||||
*
|
||||
* 【日志示例】
|
||||
* [2025-12-09T12:00:00.000Z] 127.0.0.1 chaozhou:51704 GET /documents ...
|
||||
* ^^^^^^^^ 这里显示客户端 ID
|
||||
*/
|
||||
morgan.token('client-id', (req, res) => {
|
||||
return process.env.CLIENT_ID || 'unknown';
|
||||
});
|
||||
|
||||
/**
|
||||
* 自定义 Token: :server-port
|
||||
*
|
||||
* 【作用】显示当前服务器监听的端口号
|
||||
*
|
||||
* 【数据来源】
|
||||
* process.env.PORT - 从 PM2 配置或启动命令获取
|
||||
*
|
||||
* 【日志示例】
|
||||
* [2025-12-09T12:00:00.000Z] 127.0.0.1 chaozhou:51704 GET /documents ...
|
||||
* ^^^^^ 这里显示端口号
|
||||
*/
|
||||
morgan.token('server-port', (req, res) => {
|
||||
return process.env.PORT || '0';
|
||||
});
|
||||
|
||||
/**
|
||||
* 自定义 Token: :status-colored
|
||||
*
|
||||
* 【作用】显示带颜色的 HTTP 状态码(仅开发环境)
|
||||
*
|
||||
* 【工作原理】
|
||||
* 1. 从响应对象获取状态码:res.statusCode
|
||||
* 2. 如果是开发环境,调用 colorizeStatus() 添加颜色
|
||||
* 3. 如果是生产环境,返回纯文本状态码
|
||||
*
|
||||
* 【时机】
|
||||
* 这个 token 在响应完成后被调用,此时 res.statusCode 已经设置
|
||||
*
|
||||
* 【日志示例】
|
||||
* 开发环境:GET /api/users \x1b[32m200\x1b[0m 15ms (绿色 200)
|
||||
* 生产环境:GET /api/users 200 15ms (纯文本)
|
||||
*/
|
||||
morgan.token('status-colored', (req, res) => {
|
||||
const status = res.statusCode;
|
||||
return process.env.NODE_ENV === 'development'
|
||||
? colorizeStatus(status) // 开发:添加 ANSI 颜色码
|
||||
: status; // 生产:纯数字
|
||||
});
|
||||
|
||||
/**
|
||||
* 自定义 Token: :method-colored
|
||||
*
|
||||
* 【作用】显示带颜色的 HTTP 方法(仅开发环境)
|
||||
*
|
||||
* 【工作原理】
|
||||
* 1. 从请求对象获取方法:req.method(如 'GET', 'POST')
|
||||
* 2. 如果是开发环境,调用 colorizeMethod() 添加颜色
|
||||
* 3. 如果是生产环境,返回纯文本方法
|
||||
*
|
||||
* 【日志示例】
|
||||
* 开发环境:\x1b[34mGET\x1b[0m /api/users 200 15ms (蓝色 GET)
|
||||
* 生产环境:GET /api/users 200 15ms (纯文本)
|
||||
*/
|
||||
morgan.token('method-colored', (req, res) => {
|
||||
const method = req.method;
|
||||
return process.env.NODE_ENV === 'development'
|
||||
? colorizeMethod(method) // 开发:添加 ANSI 颜色码
|
||||
: method; // 生产:纯文本
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// 3.3 定义日志格式
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Morgan 日志格式字符串
|
||||
*
|
||||
* 【格式说明】
|
||||
* 字符串中的 :token-name 会被替换为对应 token 的值
|
||||
*
|
||||
* 【内置 Token】
|
||||
* - :method - HTTP 方法(GET, POST 等)
|
||||
* - :url - 请求 URL 路径
|
||||
* - :status - HTTP 状态码
|
||||
* - :response-time - 响应时间(毫秒)
|
||||
* - :res[content-length] - 响应体大小(字节)
|
||||
* - :date[format] - 日期时间(iso: ISO 8601 格式)
|
||||
* - :remote-addr - 客户端 IP 地址
|
||||
* - :http-version - HTTP 协议版本(1.1, 2.0 等)
|
||||
* - :referrer - 来源页面 URL
|
||||
* - :user-agent - 客户端 User-Agent 字符串
|
||||
*
|
||||
* 【开发环境格式】(简洁,易读)
|
||||
* GET /api/users 200 15ms - 1234 bytes
|
||||
* ^^^ 彩色方法 ^^^ 彩色状态码
|
||||
*
|
||||
* 【生产环境格式】(详细,用于分析)
|
||||
* [2025-12-09T12:00:00.000Z] 192.168.1.100 chaozhou:51704 GET /api/users HTTP/1.1 200 1234 bytes 15ms "https://example.com" "Mozilla/5.0..."
|
||||
* ^^^^^^^^^^^^^^^^^^^^^^^^^ 时间戳
|
||||
* ^^^^^^^^^^^^^^ 客户端 IP
|
||||
* ^^^^^^^^^^^^^^^ 客户端ID:端口
|
||||
* ^^^ HTTP方法
|
||||
* ^^^^^^^^^^ URL路径
|
||||
* ^^^^^^^^ 协议版本
|
||||
* ^^^ 状态码
|
||||
* ^^^^^^^^^^^^ 响应大小
|
||||
* ^^^^^ 响应时间
|
||||
* ^^^^^^^^^^^^^^^^^^^ 来源
|
||||
* ^^^^^^^^^^^^^^ UA
|
||||
*/
|
||||
const logFormat = process.env.NODE_ENV === 'development'
|
||||
? ':method-colored :url :status-colored :response-time ms - :res[content-length] bytes'
|
||||
: '[:date[iso]] :remote-addr :client-id:server-port :method :url HTTP/:http-version :status :res[content-length] bytes :response-time ms ":referrer" ":user-agent"';
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 第四步:应用中间件(按顺序执行)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Express 中间件执行顺序说明:
|
||||
*
|
||||
* 中间件按 app.use() 调用的顺序依次执行
|
||||
* 每个中间件可以:
|
||||
* 1. 处理请求并立即响应(终止链)
|
||||
* 2. 修改请求/响应对象后调用 next() 继续
|
||||
* 3. 抛出错误或调用 next(error) 进入错误处理
|
||||
*
|
||||
* 【当前顺序】
|
||||
* 1. Morgan(日志记录)- 最先执行,记录所有请求
|
||||
* 2. Compression(压缩)- 修改响应头,启用 gzip
|
||||
* 3. Vite/静态资源 - 尝试匹配文件,匹配成功则终止
|
||||
* 4. Public 静态资源 - 尝试匹配 public/ 目录文件
|
||||
* 5. Remix 处理器 - 最后执行,处理所有未匹配的路由
|
||||
*/
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// 4.1 HTTP 请求日志中间件(Morgan)
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Morgan 中间件 - 记录所有 HTTP 请求
|
||||
*
|
||||
* 【工作原理】
|
||||
* 1. 请求到达时:记录开始时间戳
|
||||
* 2. 调用 next():继续执行后续中间件
|
||||
* 3. 响应完成时:计算响应时间,格式化日志,输出
|
||||
*
|
||||
* 【参数 1: logFormat】
|
||||
* 日志格式字符串(见上方定义)
|
||||
*
|
||||
* 【参数 2: 配置对象】
|
||||
* {
|
||||
* skip: Function - 跳过特定请求的日志记录
|
||||
* stream: Object - 自定义日志输出流
|
||||
* }
|
||||
*/
|
||||
app.use(morgan(logFormat, {
|
||||
/**
|
||||
* skip 函数 - 决定是否跳过某个请求的日志
|
||||
*
|
||||
* @param {express.Request} req - Express 请求对象
|
||||
* @param {express.Response} res - Express 响应对象
|
||||
* @returns {boolean} true = 跳过,false = 记录
|
||||
*
|
||||
* 【使用场景】
|
||||
* - 跳过健康检查请求:return req.url === '/health';
|
||||
* - 跳过静态资源:return req.url.startsWith('/build/');
|
||||
* - 只记录错误:return res.statusCode < 400;
|
||||
*
|
||||
* 【当前配置】
|
||||
* return false - 不跳过任何请求,记录所有请求
|
||||
*/
|
||||
skip: (req, res) => {
|
||||
// 示例:跳过健康检查请求(当前注释掉)
|
||||
// return req.url === '/health';
|
||||
return false; // 记录所有请求
|
||||
},
|
||||
|
||||
/**
|
||||
* stream 对象 - 自定义日志输出流
|
||||
*
|
||||
* 【默认行为】
|
||||
* morgan 默认输出到 process.stdout(标准输出)
|
||||
*
|
||||
* 【自定义行为】
|
||||
* 通过提供 stream.write() 方法拦截日志输出
|
||||
*
|
||||
* 【为什么要自定义】
|
||||
* 1. PM2 会捕获 console.log 输出到日志文件
|
||||
* 2. 可以移除末尾换行符,避免空行
|
||||
* 3. 可以添加额外的日志处理(如发送到监控系统)
|
||||
*/
|
||||
stream: {
|
||||
/**
|
||||
* write 方法 - 接收格式化后的日志消息
|
||||
*
|
||||
* @param {string} message - 格式化后的日志消息(包含末尾 \n)
|
||||
*
|
||||
* 【工作流程】
|
||||
* 1. Morgan 格式化日志 → 生成字符串(末尾带 \n)
|
||||
* 2. 调用 stream.write(message)
|
||||
* 3. 我们移除 \n 并使用 console.log 输出
|
||||
* 4. PM2 捕获 console.log → 写入 logs/xxx-out.log
|
||||
*
|
||||
* 【为什么用 trim()】
|
||||
* 因为 console.log 会自动添加换行符,如果不移除原有的 \n
|
||||
* 会导致日志文件中出现空行
|
||||
*/
|
||||
write: (message) => {
|
||||
// 移除末尾换行符,使用 console.log 输出
|
||||
// PM2 会自动捕获到日志文件(logs/{client-id}-out.log)
|
||||
console.log(message.trim());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// 4.2 压缩中间件(Compression)
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compression 中间件 - 压缩 HTTP 响应体
|
||||
*
|
||||
* 【作用】
|
||||
* 对响应体进行 gzip/deflate 压缩,减少网络传输大小
|
||||
*
|
||||
* 【工作原理】
|
||||
* 1. 检查请求头 Accept-Encoding(客户端支持的压缩算法)
|
||||
* 2. 检查响应头 Content-Type(只压缩文本类型)
|
||||
* 3. 设置响应头 Content-Encoding: gzip
|
||||
* 4. 压缩响应体数据
|
||||
* 5. 调用 next() 继续执行
|
||||
*
|
||||
* 【压缩效果】
|
||||
* - HTML/CSS/JS: 通常压缩 70-80%
|
||||
* - JSON: 通常压缩 50-70%
|
||||
* - 图片/视频: 不压缩(已经是压缩格式)
|
||||
*
|
||||
* 【默认配置】
|
||||
* - 阈值:1KB(小于 1KB 的响应不压缩)
|
||||
* - 级别:6(压缩级别 1-9,默认 6 是平衡点)
|
||||
* - 过滤:自动排除已压缩的 MIME 类型
|
||||
*
|
||||
* 【性能影响】
|
||||
* - CPU 消耗:轻微增加(压缩计算)
|
||||
* - 内存消耗:每个请求增加约 16KB
|
||||
* - 网络带宽:大幅减少 50-80%
|
||||
* - 响应时间:可能略微增加(但总体传输时间更短)
|
||||
*/
|
||||
app.use(compression());
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// 4.3 静态资源中间件(Vite 或 Express Static)
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 静态资源服务 - 根据环境选择不同策略
|
||||
*
|
||||
* 【开发环境】Vite Dev Server
|
||||
* - 实时编译:每次请求时编译源代码
|
||||
* - HMR 支持:代码修改后自动刷新浏览器
|
||||
* - Source Map:提供完整的源码映射
|
||||
*
|
||||
* 【测试/生产环境】Express Static
|
||||
* - 预构建文件:从 build/client 目录提供文件
|
||||
* - 缓存策略:immutable + maxAge 1年
|
||||
* - 性能优化:直接读取文件,无需编译
|
||||
*/
|
||||
if (viteDevServer) {
|
||||
/**
|
||||
* Vite 开发服务器中间件
|
||||
*
|
||||
* 【作用】
|
||||
* 拦截对源代码的请求(如 /src/routes/index.tsx)
|
||||
* 编译 TypeScript/JSX → JavaScript
|
||||
* 返回编译后的代码
|
||||
*
|
||||
* 【处理的请求示例】
|
||||
* - GET /@vite/client → Vite HMR 客户端代码
|
||||
* - GET /src/root.tsx → 编译并返回 root.tsx
|
||||
* - GET /node_modules/.vite/deps/react.js → 依赖预构建
|
||||
*
|
||||
* 【工作流程】
|
||||
* 1. 请求到达 → Vite 检查是否是源代码请求
|
||||
* 2. 如果是 → 编译源代码 → 返回 JS(终止链)
|
||||
* 3. 如果不是 → 调用 next() → 继续到下一个中间件
|
||||
*/
|
||||
app.use(viteDevServer.middlewares);
|
||||
} else {
|
||||
/**
|
||||
* Express.static 中间件 - 提供构建后的静态资源
|
||||
*
|
||||
* 【参数 1: 目录路径】
|
||||
* 'build/client' - Remix 构建输出目录
|
||||
*
|
||||
* Remix 构建后的目录结构:
|
||||
* build/
|
||||
* ├── client/ ← 客户端资源(浏览器加载)
|
||||
* │ ├── assets/ ← CSS/JS 文件(带 hash 文件名)
|
||||
* │ │ ├── root-K8ExMHas.js
|
||||
* │ │ ├── login-CWMdsJSf.js
|
||||
* │ │ └── main-DvLl4hFZ.css
|
||||
* │ └── ...
|
||||
* └── server/ ← 服务端代码(SSR)
|
||||
* └── index.js
|
||||
*
|
||||
* 【参数 2: 配置对象】
|
||||
* {
|
||||
* immutable: boolean - 是否添加 Cache-Control: immutable
|
||||
* maxAge: string - 缓存时间(毫秒或字符串)
|
||||
* }
|
||||
*
|
||||
* 【immutable 说明】
|
||||
* immutable: true → 响应头添加 'Cache-Control: public, max-age=31536000, immutable'
|
||||
* 含义:告诉浏览器这个文件永远不会改变
|
||||
*
|
||||
* 【为什么可以设置 immutable】
|
||||
* Remix 构建的文件名包含内容哈希(如 main-DvLl4hFZ.css)
|
||||
* 文件内容改变 → 哈希改变 → 文件名改变 → 不会冲突
|
||||
*
|
||||
* 【maxAge: '1y'】
|
||||
* 缓存时间:1 年(31536000 秒)
|
||||
* 浏览器会缓存文件 1 年,除非手动清除缓存
|
||||
*
|
||||
* 【请求匹配逻辑】
|
||||
* GET /assets/main-DvLl4hFZ.css → 查找 build/client/assets/main-DvLl4hFZ.css
|
||||
* ├─ 文件存在 → 返回文件内容(Content-Type: text/css)→ 终止链
|
||||
* └─ 文件不存在 → 调用 next() → 继续到下一个中间件
|
||||
*
|
||||
* GET /favicon.ico → 查找 build/client/favicon.ico
|
||||
* ├─ 文件存在 → 返回文件
|
||||
* └─ 文件不存在 → 继续
|
||||
*/
|
||||
app.use(express.static('build/client', {
|
||||
immutable: true, // 添加 immutable 指令(永久缓存)
|
||||
maxAge: '1y' // 缓存时间 1 年
|
||||
}));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// 4.4 Public 目录静态资源中间件
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Express.static 中间件 - 提供 public 目录的静态资源
|
||||
*
|
||||
* 【作用】
|
||||
* 提供不需要构建的公共资源(字体、图片、PDF 等)
|
||||
*
|
||||
* 【目录结构】
|
||||
* public/
|
||||
* ├── fonts/ ← 字体文件(remixicon, source-han-sans)
|
||||
* │ ├── remixicon.woff2
|
||||
* │ └── ...
|
||||
* ├── images/ ← 图片资源
|
||||
* │ ├── logo.png
|
||||
* │ └── ...
|
||||
* └── pdf.worker.js ← PDF.js Worker(不需要构建)
|
||||
*
|
||||
* 【参数 1: 目录路径】
|
||||
* 'public' - 公共资源目录
|
||||
*
|
||||
* 【参数 2: 配置对象】
|
||||
* { maxAge: '1h' } - 缓存时间 1 小时(3600 秒)
|
||||
*
|
||||
* 【为什么缓存时间较短】
|
||||
* public 目录的文件名通常不包含哈希
|
||||
* 文件可能被更新但文件名不变
|
||||
* 所以缓存时间设置较短(1 小时)
|
||||
*
|
||||
* 【请求匹配逻辑】
|
||||
* GET /fonts/remixicon.woff2 → 查找 public/fonts/remixicon.woff2
|
||||
* ├─ 文件存在 → 返回文件(Content-Type: font/woff2)→ 终止链
|
||||
* └─ 文件不存在 → 调用 next() → 继续到 Remix 处理器
|
||||
*
|
||||
* GET /logo.png → 查找 public/logo.png
|
||||
* ├─ 文件存在 → 返回文件
|
||||
* └─ 文件不存在 → 继续
|
||||
*
|
||||
* 【注意】
|
||||
* 这个中间件在 Remix 处理器之前
|
||||
* 如果 public 目录有 /login 文件,会覆盖 Remix 的 /login 路由
|
||||
* 所以避免在 public 目录创建与路由同名的文件
|
||||
*/
|
||||
app.use(express.static('public', {
|
||||
maxAge: '1h' // 缓存时间 1 小时
|
||||
}));
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 第五步:Remix 请求处理器(最后一个中间件)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 加载 Remix 构建文件
|
||||
*
|
||||
* 【开发环境】
|
||||
* viteDevServer 存在 → remixBuild = undefined
|
||||
* Remix 会通过 Vite 实时编译加载
|
||||
*
|
||||
* 【测试/生产环境】
|
||||
* viteDevServer = null → 导入预构建的 server/index.js
|
||||
*
|
||||
* 【构建文件说明】
|
||||
* build/server/index.js 包含:
|
||||
* - 所有路由的 loader/action 函数
|
||||
* - 服务端渲染逻辑
|
||||
* - 路由配置信息
|
||||
*
|
||||
* 【为什么用 await import()】
|
||||
* 动态导入,允许在模块顶层使用(ESM 规范)
|
||||
* 如果构建文件不存在,会抛出错误并终止进程
|
||||
*/
|
||||
const remixBuild = viteDevServer
|
||||
? undefined // 开发环境:不预加载,通过 Vite SSR 加载
|
||||
: await import('./build/server/index.js'); // 生产环境:导入构建文件
|
||||
|
||||
/**
|
||||
* Remix 请求处理器 - 核心中间件
|
||||
*
|
||||
* 【app.all() 说明】
|
||||
* app.all(path, handler) - 匹配所有 HTTP 方法(GET, POST, PUT, DELETE 等)
|
||||
* 参数 1: '*' - 通配符,匹配所有路径
|
||||
* 参数 2: handler - 请求处理函数
|
||||
*
|
||||
* 【为什么用 app.all('*')】
|
||||
* Remix 需要处理所有未被静态资源中间件匹配的请求
|
||||
* 包括:
|
||||
* - 页面路由(GET /documents, GET /login)
|
||||
* - API 路由(POST /api/documents, GET /api/users)
|
||||
* - 资源路由(GET /api/pdf/123)
|
||||
* - 未找到的路由(404 页面)
|
||||
*
|
||||
* 【执行时机】
|
||||
* 只有当前面所有中间件都调用 next() 时才执行
|
||||
* 即:请求不是静态资源时
|
||||
*
|
||||
* 【createRequestHandler 函数】
|
||||
* 从 @remix-run/express 导入
|
||||
* 将 Remix 应用转换为 Express 中间件
|
||||
*
|
||||
* 【参数说明】
|
||||
* {
|
||||
* build: ServerBuild | (() => Promise<ServerBuild>)
|
||||
* mode: string
|
||||
* }
|
||||
*/
|
||||
app.all(
|
||||
'*', // 匹配所有路径
|
||||
createRequestHandler({
|
||||
/**
|
||||
* build 参数 - Remix 构建配置
|
||||
*
|
||||
* 【类型】
|
||||
* ServerBuild | (() => Promise<ServerBuild>)
|
||||
*
|
||||
* 【开发环境值】
|
||||
* () => viteDevServer.ssrLoadModule('virtual:remix/server-build')
|
||||
*
|
||||
* 函数形式,每次请求时调用
|
||||
*
|
||||
* 【工作流程】
|
||||
* 1. 请求到达 → 调用函数
|
||||
* 2. viteDevServer.ssrLoadModule() → 加载虚拟模块
|
||||
* 3. Vite 编译所有路由的服务端代码 → 返回 ServerBuild 对象
|
||||
* 4. Remix 使用 ServerBuild 处理请求
|
||||
*
|
||||
* 【什么是 'virtual:remix/server-build'】
|
||||
* Vite 虚拟模块(不是真实文件)
|
||||
* Remix Vite 插件动态生成,包含所有路由信息
|
||||
*
|
||||
* 【为什么每次调用】
|
||||
* 开发环境代码可能随时修改
|
||||
* 每次重新加载确保使用最新代码
|
||||
*
|
||||
* 【生产环境值】
|
||||
* remixBuild(预加载的构建对象)
|
||||
*
|
||||
* 直接使用预构建的对象,性能更高
|
||||
*/
|
||||
build: viteDevServer
|
||||
? () => viteDevServer.ssrLoadModule('virtual:remix/server-build')
|
||||
: remixBuild,
|
||||
|
||||
/**
|
||||
* mode 参数 - 运行模式
|
||||
*
|
||||
* 【值】
|
||||
* - 'development' - 开发模式(npm run dev)
|
||||
* - 'testing' - 测试模式(PM2 测试环境)
|
||||
* - 'production' - 生产模式(PM2 生产环境)
|
||||
*
|
||||
* 【作用】
|
||||
* 1. 影响错误页面显示(开发显示堆栈,生产隐藏)
|
||||
* 2. 影响日志级别(开发详细,生产简洁)
|
||||
* 3. 影响性能优化(生产启用更多优化)
|
||||
*
|
||||
* 【Remix 内部使用】
|
||||
* - 开发模式:显示详细错误堆栈,启用 HMR
|
||||
* - 生产模式:隐藏错误详情,启用缓存优化
|
||||
*/
|
||||
mode: process.env.NODE_ENV,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
* Remix 请求处理流程详解
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 当请求到达 createRequestHandler() 时:
|
||||
*
|
||||
* 1. 【路由匹配】
|
||||
* Remix 根据 URL 路径匹配路由文件
|
||||
* 例:GET /documents → app/routes/documents.tsx
|
||||
*
|
||||
* 2. 【执行 Loader】
|
||||
* 如果路由有 loader 函数,在服务端执行
|
||||
* loader 可以访问数据库、调用 API、读取会话
|
||||
* 返回数据注入到组件 props
|
||||
*
|
||||
* 3. 【服务端渲染(SSR)】
|
||||
* Remix 渲染 React 组件为 HTML 字符串
|
||||
* 包括:
|
||||
* - app/root.tsx(根组件)
|
||||
* - app/routes/documents.tsx(匹配的路由)
|
||||
* - 所有嵌套布局和子组件
|
||||
*
|
||||
* 4. 【注入资源链接】
|
||||
* 自动添加 <link> 和 <script> 标签
|
||||
* 指向构建的 CSS/JS 文件(如 /assets/main-DvLl4hFZ.css)
|
||||
*
|
||||
* 5. 【返回 HTML】
|
||||
* 返回完整的 HTML 文档
|
||||
* 设置响应头:Content-Type: text/html
|
||||
*
|
||||
* 6. 【客户端激活(Hydration)】
|
||||
* 浏览器接收 HTML → 渲染页面
|
||||
* 加载 JS 文件 → React 接管交互
|
||||
*
|
||||
* 【API 路由处理】
|
||||
* 如果路由是 API(如 app/routes/api.users.ts)
|
||||
* - 执行 loader(GET)或 action(POST/PUT/DELETE)
|
||||
* - 返回 JSON 响应(不渲染 HTML)
|
||||
* - 设置响应头:Content-Type: application/json
|
||||
*
|
||||
* 【错误处理】
|
||||
* - 如果 loader 抛出错误 → 渲染 ErrorBoundary 组件
|
||||
* - 如果路由不存在 → 渲染 404 页面
|
||||
* - 如果服务器错误 → 渲染 500 错误页面
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 第六步:启动 Express 服务器
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 从环境变量读取配置
|
||||
*
|
||||
* 【PORT】
|
||||
* 服务器监听端口
|
||||
* 来源:PM2 配置的 env.PORT(如 5183, 51704)
|
||||
* 默认:3000
|
||||
*
|
||||
* 【CLIENT_ID】
|
||||
* 客户端标识符
|
||||
* 来源:PM2 配置的 env.CLIENT_ID(如 'main', 'chaozhou')
|
||||
* 默认:'unknown'
|
||||
*/
|
||||
const port = process.env.PORT || 3000;
|
||||
const clientId = process.env.CLIENT_ID || 'unknown';
|
||||
|
||||
/**
|
||||
* app.listen() - 启动 HTTP 服务器
|
||||
*
|
||||
* 【参数 1: port】
|
||||
* 监听端口号
|
||||
*
|
||||
* 【参数 2: callback】
|
||||
* 服务器启动成功后的回调函数
|
||||
*
|
||||
* 【工作原理】
|
||||
* 1. 创建 TCP Socket
|
||||
* 2. 绑定到指定端口(如 5183)
|
||||
* 3. 开始监听连接请求
|
||||
* 4. 每个新连接 → 创建新的请求处理流
|
||||
* 5. 执行中间件链处理请求
|
||||
*
|
||||
* 【端口占用处理】
|
||||
* 如果端口已被占用,会抛出 EADDRINUSE 错误
|
||||
* 进程会终止(PM2 会自动重启)
|
||||
*/
|
||||
app.listen(port, () => {
|
||||
// 打印启动信息(格式化输出)
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log(`🚀 Server started successfully!`);
|
||||
console.log(` Client ID: ${chalk.cyan(clientId)}`);
|
||||
console.log(` Port: ${chalk.green(port)}`);
|
||||
console.log(` Environment: ${chalk.yellow(process.env.NODE_ENV || 'development')}`);
|
||||
console.log(` URL: ${chalk.blue(`http://localhost:${port}`)}`);
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log('📝 HTTP Request Logging: ENABLED');
|
||||
console.log(` Log format: ${process.env.NODE_ENV === 'development' ? 'colored (dev)' : 'detailed (production)'}`);
|
||||
console.log(` Log output: PM2 captures to logs/${clientId}-out.log`);
|
||||
console.log('═══════════════════════════════════════════════════════════\n');
|
||||
|
||||
/**
|
||||
* broadcastDevReady() - 通知开发工具服务器已就绪
|
||||
*
|
||||
* 【作用】
|
||||
* 发送信号到开发工具(IDE、浏览器扩展等)
|
||||
* 告知 Remix 应用已准备好接收请求
|
||||
*
|
||||
* 【条件】
|
||||
* 只在开发环境且有构建文件时调用
|
||||
* 生产环境不需要(没有开发工具监听)
|
||||
*
|
||||
* 【参数】
|
||||
* remixBuild - 构建对象,包含路由信息
|
||||
*/
|
||||
if (process.env.NODE_ENV === 'development' && remixBuild) {
|
||||
broadcastDevReady(remixBuild);
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 第七步:优雅关闭(Graceful Shutdown)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* SIGTERM 信号处理器
|
||||
*
|
||||
* 【SIGTERM 说明】
|
||||
* 终止信号(Termination Signal)
|
||||
* 请求进程正常退出(可以被捕获和处理)
|
||||
*
|
||||
* 【来源】
|
||||
* - PM2 重启:pm2 restart xxx
|
||||
* - PM2 停止:pm2 stop xxx
|
||||
* - Docker 停止:docker stop xxx
|
||||
* - Kubernetes Pod 终止
|
||||
*
|
||||
* 【处理流程】
|
||||
* 1. 接收 SIGTERM 信号
|
||||
* 2. 打印日志
|
||||
* 3. 停止接收新请求
|
||||
* 4. 等待现有请求完成(最多 30 秒)
|
||||
* 5. 关闭数据库连接
|
||||
* 6. 退出进程(process.exit(0))
|
||||
*
|
||||
* 【为什么需要优雅关闭】
|
||||
* 避免:
|
||||
* - 正在处理的请求被中断
|
||||
* - 数据库事务未提交
|
||||
* - 文件写入未完成
|
||||
* - WebSocket 连接突然断开
|
||||
*/
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\n⏹️ Received SIGTERM signal. Shutting down gracefully...');
|
||||
// 这里可以添加清理逻辑:
|
||||
// - 关闭数据库连接:await db.close()
|
||||
// - 等待请求完成:await server.close()
|
||||
// - 清理临时文件:await fs.unlink(tempFile)
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* SIGINT 信号处理器
|
||||
*
|
||||
* 【SIGINT 说明】
|
||||
* 中断信号(Interrupt Signal)
|
||||
* 通常由用户按 Ctrl+C 触发
|
||||
*
|
||||
* 【来源】
|
||||
* - 终端按 Ctrl+C
|
||||
* - IDE 停止调试
|
||||
* - kill -2 <pid>
|
||||
*
|
||||
* 【处理流程】
|
||||
* 与 SIGTERM 相同,确保一致的关闭行为
|
||||
*/
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n⏹️ Received SIGINT signal. Shutting down gracefully...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
* 完整请求流程示例
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 【示例 1:访问页面】
|
||||
* 用户在浏览器输入:http://localhost:5183/documents
|
||||
*
|
||||
* 1. HTTP 请求到达 Express 服务器(端口 5183)
|
||||
* ↓
|
||||
* 2. Morgan 中间件:记录请求开始时间
|
||||
* ↓
|
||||
* 3. Compression 中间件:标记响应需要压缩
|
||||
* ↓
|
||||
* 4. Static 中间件(build/client):查找 build/client/documents
|
||||
* - 文件不存在 → 调用 next()
|
||||
* ↓
|
||||
* 5. Static 中间件(public):查找 public/documents
|
||||
* - 文件不存在 → 调用 next()
|
||||
* ↓
|
||||
* 6. Remix 处理器:
|
||||
* - 匹配路由 → app/routes/documents.tsx
|
||||
* - 执行 loader → 从数据库获取文档列表
|
||||
* - 渲染组件 → 生成 HTML
|
||||
* - 返回响应:
|
||||
* Content-Type: text/html
|
||||
* Content-Encoding: gzip
|
||||
* Body: <html>...</html>(压缩后)
|
||||
* ↓
|
||||
* 7. Morgan 记录日志:
|
||||
* GET /documents 200 45ms - 15234 bytes
|
||||
* ↓
|
||||
* 8. 浏览器接收 HTML:
|
||||
* - 解压 gzip → 渲染页面
|
||||
* - 加载 CSS:GET /assets/main-DvLl4hFZ.css
|
||||
* - 加载 JS:GET /assets/documents-BcUjVGQw.js
|
||||
*
|
||||
* 【示例 2:加载 CSS 文件】
|
||||
* 浏览器请求:GET /assets/main-DvLl4hFZ.css
|
||||
*
|
||||
* 1. HTTP 请求到达
|
||||
* ↓
|
||||
* 2. Morgan:记录开始时间
|
||||
* ↓
|
||||
* 3. Compression:标记压缩
|
||||
* ↓
|
||||
* 4. Static(build/client):
|
||||
* - 查找 build/client/assets/main-DvLl4hFZ.css
|
||||
* - 文件存在!→ 返回文件
|
||||
* Content-Type: text/css
|
||||
* Content-Encoding: gzip
|
||||
* Cache-Control: public, max-age=31536000, immutable
|
||||
* - 终止链(不继续到 Remix)
|
||||
* ↓
|
||||
* 5. Morgan 记录日志:
|
||||
* GET /assets/main-DvLl4hFZ.css 200 2ms - 12345 bytes
|
||||
* ↓
|
||||
* 6. 浏览器接收 CSS:
|
||||
* - 缓存 1 年(immutable)
|
||||
* - 下次访问直接从缓存读取(不发请求)
|
||||
*
|
||||
* 【示例 3:API 请求】
|
||||
* 前端调用:POST /api/documents(上传文档)
|
||||
*
|
||||
* 1. HTTP 请求到达
|
||||
* ↓
|
||||
* 2. Morgan:记录请求
|
||||
* ↓
|
||||
* 3. Compression:标记压缩
|
||||
* ↓
|
||||
* 4. Static 中间件:路径不匹配 → next()
|
||||
* ↓
|
||||
* 5. Remix 处理器:
|
||||
* - 匹配路由 → app/routes/api.documents.ts
|
||||
* - 执行 action(POST 处理函数):
|
||||
* · 解析请求体(FormData)
|
||||
* · 保存文件到 MinIO
|
||||
* · 写入数据库
|
||||
* - 返回 JSON:
|
||||
* Content-Type: application/json
|
||||
* Body: { success: true, id: "123" }
|
||||
* ↓
|
||||
* 6. Morgan 记录:
|
||||
* POST /api/documents 200 234ms - 45 bytes
|
||||
* ↓
|
||||
* 7. 前端接收响应:
|
||||
* - 解析 JSON
|
||||
* - 更新界面
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
Reference in New Issue
Block a user