huxy-node-server 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/example.js +34 -0
- package/package.json +50 -0
- package/src/index.js +226 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 yiru
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# Huxy Node Server
|
|
2
|
+
|
|
3
|
+
[](https://nodejs.org/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://expressjs.com/)
|
|
6
|
+
|
|
7
|
+
一个精炼、高性能的 Express.js 服务器模板,为现代 Node.js 应用程序设计,提供灵活的功能和最佳实践。
|
|
8
|
+
|
|
9
|
+
## 🚀 特性
|
|
10
|
+
|
|
11
|
+
### 核心功能
|
|
12
|
+
- **现代 ES 模块支持**:使用 `"type": "module"` 完全支持 ES Modules
|
|
13
|
+
- **高性能日志**:集成 Pino 日志系统,支持彩色输出和多级别日志
|
|
14
|
+
- **安全防护**:内置 Helmet 安全中间件,提供 CSP、XSS 等多种安全防护
|
|
15
|
+
- **请求限制**:基于 IP 的请求速率限制,防止 DDoS 和暴力攻击
|
|
16
|
+
- **跨域支持**:灵活的 CORS 配置,支持多域名和凭证
|
|
17
|
+
- **压缩支持**:自动 GZIP 压缩响应,减少带宽使用
|
|
18
|
+
- **健康检查**:内置 `/health` 端点,监控服务器状态
|
|
19
|
+
- **优雅关闭**:处理 SIGTERM 和 SIGINT 信号,确保服务器优雅关闭
|
|
20
|
+
|
|
21
|
+
### 生产环境特性
|
|
22
|
+
- **环境变量支持**:通过 dotenv 管理配置,支持 `.env` 文件
|
|
23
|
+
- **错误处理**:全局错误处理中间件,提供详细错误日志
|
|
24
|
+
- **请求日志**:详细的 HTTP 请求日志,包括响应时间、状态码等
|
|
25
|
+
- **端口检查**:自动检测端口是否被占用,并自动选择可用端口
|
|
26
|
+
- **内存监控**:实时监控服务器内存使用情况
|
|
27
|
+
- **多网络接口支持**:自动检测本地 IP 地址,支持多网卡环境
|
|
28
|
+
|
|
29
|
+
### 开发者友好
|
|
30
|
+
- **热重载**:开发环境支持 `--watch` 模式,自动重载代码
|
|
31
|
+
- **详细文档**:完整的 API 文档和使用示例
|
|
32
|
+
- **模块化设计**:清晰的代码结构,易于扩展和定制
|
|
33
|
+
- **TypeScript 友好**:代码结构适合 TypeScript 迁移
|
|
34
|
+
|
|
35
|
+
## 📦 安装
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 通过 npm 安装
|
|
39
|
+
npm install huxy-node-server
|
|
40
|
+
|
|
41
|
+
# 或者通过 yarn 安装
|
|
42
|
+
yarn add huxy-node-server
|
|
43
|
+
|
|
44
|
+
# 或者通过 pnpm 安装
|
|
45
|
+
pnpm add huxy-node-server
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 🚀 快速开始
|
|
49
|
+
|
|
50
|
+
### 基本使用
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
import { startServer } from 'huxy-node-server';
|
|
54
|
+
|
|
55
|
+
// 启动服务器
|
|
56
|
+
const { app, config, httpServer } = await startServer({
|
|
57
|
+
port: 3000,
|
|
58
|
+
host: '0.0.0.0',
|
|
59
|
+
basepath: '/api',
|
|
60
|
+
// 其他配置...
|
|
61
|
+
}, (config, app, httpServer) => {
|
|
62
|
+
// 可以在这里添加自定义路由
|
|
63
|
+
app.get('/hello', (req, res) => {
|
|
64
|
+
res.json({ message: 'Hello World!' });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 静态文件服务
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
import { startStatic } from 'huxy-node-server';
|
|
73
|
+
|
|
74
|
+
// 启动静态文件服务器
|
|
75
|
+
const server = await startStatic({
|
|
76
|
+
port: 9000,
|
|
77
|
+
basepath: '/',
|
|
78
|
+
buildPath: './dist', // 静态文件目录
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 🛠️ 配置选项
|
|
83
|
+
|
|
84
|
+
### 服务器配置
|
|
85
|
+
|
|
86
|
+
| 选项 | 类型 | 默认值 | 描述 |
|
|
87
|
+
|------|------|--------|------|
|
|
88
|
+
| `port` | number | 3000 | 服务器端口 |
|
|
89
|
+
| `host` | string | '0.0.0.0' | 服务器主机 |
|
|
90
|
+
| `basepath` | string | '/' | 基础路径前缀 |
|
|
91
|
+
| `nodeEnv` | string | 'development' | 运行环境 |
|
|
92
|
+
| `appName` | string | 'HuxyServer' | 应用名称 |
|
|
93
|
+
|
|
94
|
+
### 安全配置
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
{
|
|
98
|
+
helmet: {
|
|
99
|
+
contentSecurityPolicy: {
|
|
100
|
+
directives: {
|
|
101
|
+
defaultSrc: ["'self'"],
|
|
102
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
103
|
+
scriptSrc: ["'self'"],
|
|
104
|
+
imgSrc: ["'self'", "data:", "https:"]
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
crossOriginEmbedderPolicy: false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### CORS 配置
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
{
|
|
116
|
+
cors: {
|
|
117
|
+
origin: ['http://example.com', 'http://localhost:3000'], // 或 '*'
|
|
118
|
+
credentials: true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 请求速率限制
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
{
|
|
127
|
+
rateLimit: {
|
|
128
|
+
windowMs: 900000, // 15 分钟
|
|
129
|
+
limit: 100, // 每个窗口内最大请求数
|
|
130
|
+
message: {
|
|
131
|
+
error: '请求过于频繁,请稍后再试'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 日志配置
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
{
|
|
141
|
+
logLevel: 30, // 日志级别 (10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal)
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 🌍 环境变量
|
|
146
|
+
|
|
147
|
+
可以通过环境变量配置服务器:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# .env 文件
|
|
151
|
+
NODE_ENV=production
|
|
152
|
+
PORT=3000
|
|
153
|
+
HOST=0.0.0.0
|
|
154
|
+
BASEPATH=/api
|
|
155
|
+
CORS_ORIGIN=http://example.com,http://localhost:3000
|
|
156
|
+
RATE_LIMIT_WINDOW_MS=900000
|
|
157
|
+
RATE_LIMIT_MAX_REQUESTS=100
|
|
158
|
+
LOG_LEVEL=30
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
或者通过命令行参数:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
node server.js port=3000 host=localhost
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 📂 目录结构
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
.
|
|
171
|
+
├── src/
|
|
172
|
+
│ ├── app.js # Express 应用配置
|
|
173
|
+
│ ├── config.js # 默认配置
|
|
174
|
+
│ ├── server.js # 服务器启动逻辑
|
|
175
|
+
│ ├── routes.js # 默认路由
|
|
176
|
+
│ ├── middleware.js # 中间件集合
|
|
177
|
+
│ ├── logger.js # 日志系统
|
|
178
|
+
│ ├── utils.js # 工具函数
|
|
179
|
+
│ ├── staticServer.js # 静态文件服务器
|
|
180
|
+
│ └── resolvePath.js # 路径解析工具
|
|
181
|
+
├── example.js # 使用示例
|
|
182
|
+
└── package.json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 🔧 高级用法
|
|
186
|
+
|
|
187
|
+
### 自定义中间件
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
import { startServer } from 'huxy-node-server';
|
|
191
|
+
import customMiddleware from './customMiddleware';
|
|
192
|
+
|
|
193
|
+
const { app } = await startServer({
|
|
194
|
+
port: 3000,
|
|
195
|
+
}, (config, app) => {
|
|
196
|
+
// 添加自定义中间件
|
|
197
|
+
app.use(customMiddleware);
|
|
198
|
+
|
|
199
|
+
// 添加自定义路由
|
|
200
|
+
app.get('/custom', (req, res) => {
|
|
201
|
+
res.json({ custom: 'route' });
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 自定义错误处理
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
import { startServer } from 'huxy-node-server';
|
|
210
|
+
|
|
211
|
+
const { app } = await startServer({
|
|
212
|
+
port: 3000,
|
|
213
|
+
}, (config, app) => {
|
|
214
|
+
// 添加自定义错误处理
|
|
215
|
+
app.use((err, req, res, next) => {
|
|
216
|
+
if (err instanceof CustomError) {
|
|
217
|
+
res.status(400).json({ error: err.message });
|
|
218
|
+
} else {
|
|
219
|
+
next(err);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 自定义日志
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
import { startServer, createLogger } from 'huxy-node-server';
|
|
229
|
+
|
|
230
|
+
const customLogger = createLogger('custom-module', {
|
|
231
|
+
level: 'debug',
|
|
232
|
+
transport: {
|
|
233
|
+
target: 'pino-pretty',
|
|
234
|
+
options: { colorize: true }
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
customLogger.info('自定义日志消息');
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### 与数据库集成
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
import { startServer } from 'huxy-node-server';
|
|
245
|
+
import mongoose from 'mongoose';
|
|
246
|
+
|
|
247
|
+
const { app } = await startServer({
|
|
248
|
+
port: 3000,
|
|
249
|
+
}, async (config, app) => {
|
|
250
|
+
// 连接到 MongoDB
|
|
251
|
+
await mongoose.connect(config.DATABASE_URL, {
|
|
252
|
+
useNewUrlParser: true,
|
|
253
|
+
useUnifiedTopology: true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// 添加数据库中间件
|
|
257
|
+
app.use((req, res, next) => {
|
|
258
|
+
req.db = mongoose.connection;
|
|
259
|
+
next();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// 添加 API 路由
|
|
263
|
+
app.get('/api/users', async (req, res) => {
|
|
264
|
+
const users = await req.db.collection('users').find().toArray();
|
|
265
|
+
res.json({ success: true, data: users });
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 📊 API 文档
|
|
271
|
+
|
|
272
|
+
### `startServer(config, callback)`
|
|
273
|
+
|
|
274
|
+
启动 Express 服务器
|
|
275
|
+
|
|
276
|
+
**参数:**
|
|
277
|
+
|
|
278
|
+
- `config` (Object): 服务器配置对象
|
|
279
|
+
- `callback` (Function): 可选的回调函数,在服务器启动后调用
|
|
280
|
+
|
|
281
|
+
**返回:** Promise<{app, config, httpServer}>
|
|
282
|
+
|
|
283
|
+
### `startStatic(config, callback)`
|
|
284
|
+
|
|
285
|
+
启动静态文件服务器
|
|
286
|
+
|
|
287
|
+
**参数:**
|
|
288
|
+
|
|
289
|
+
- `config` (Object): 服务器配置对象
|
|
290
|
+
- `callback` (Function): 可选的回调函数,在服务器启动后调用
|
|
291
|
+
|
|
292
|
+
**返回:** Promise<{app, config, httpServer}>
|
|
293
|
+
|
|
294
|
+
### `createLogger(name, customConfig)`
|
|
295
|
+
|
|
296
|
+
创建自定义日志实例
|
|
297
|
+
|
|
298
|
+
**参数:**
|
|
299
|
+
|
|
300
|
+
- `name` (String): 日志实例名称
|
|
301
|
+
- `customConfig` (Object): 自定义配置
|
|
302
|
+
|
|
303
|
+
**返回:** Pino 日志实例
|
|
304
|
+
|
|
305
|
+
### `logger`
|
|
306
|
+
|
|
307
|
+
默认日志实例
|
|
308
|
+
|
|
309
|
+
### 工具函数
|
|
310
|
+
|
|
311
|
+
- `dateTime()`: 获取当前时间字符串
|
|
312
|
+
- `localIPs()`: 获取本地 IP 地址列表
|
|
313
|
+
- `nodeArgs()`: 解析命令行参数
|
|
314
|
+
- `getEnvConfig()`: 获取环境变量配置
|
|
315
|
+
- `checkPort()`: 检查端口是否可用
|
|
316
|
+
- `resolvePath()`: 解析文件路径
|
|
317
|
+
|
|
318
|
+
## 🛡️ 安全最佳实践
|
|
319
|
+
|
|
320
|
+
### 1. 环境变量
|
|
321
|
+
|
|
322
|
+
永远不要在代码中硬编码敏感信息,使用环境变量:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
# .env 文件
|
|
326
|
+
JWT_SECRET=your_secret_key_here
|
|
327
|
+
DATABASE_URL=your_database_url
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### 2. HTTPS
|
|
331
|
+
|
|
332
|
+
在生产环境中,始终使用 HTTPS。可以使用反向代理(如 Nginx)或直接配置:
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
import https from 'https';
|
|
336
|
+
import fs from 'fs';
|
|
337
|
+
|
|
338
|
+
const options = {
|
|
339
|
+
key: fs.readFileSync('server.key'),
|
|
340
|
+
cert: fs.readFileSync('server.cert')
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
https.createServer(options, app).listen(443);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### 3. 速率限制
|
|
347
|
+
|
|
348
|
+
根据您的应用需求调整速率限制:
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
{
|
|
352
|
+
rateLimit: {
|
|
353
|
+
windowMs: 15 * 60 * 1000, // 15 分钟
|
|
354
|
+
limit: 100, // 每个 IP 每个窗口内最大请求数
|
|
355
|
+
message: '太多请求,请稍后再试'
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 4. CORS
|
|
361
|
+
|
|
362
|
+
在生产环境中,限制 CORS 来源:
|
|
363
|
+
|
|
364
|
+
```javascript
|
|
365
|
+
{
|
|
366
|
+
cors: {
|
|
367
|
+
origin: ['https://yourdomain.com', 'https://yourapp.com'],
|
|
368
|
+
credentials: true
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### 5. 安全头
|
|
374
|
+
|
|
375
|
+
根据需要调整 Helmet 配置:
|
|
376
|
+
|
|
377
|
+
```javascript
|
|
378
|
+
{
|
|
379
|
+
helmet: {
|
|
380
|
+
contentSecurityPolicy: {
|
|
381
|
+
directives: {
|
|
382
|
+
defaultSrc: ["'self'"],
|
|
383
|
+
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.example.com"],
|
|
384
|
+
// 其他 CSP 指令...
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## 🚀 部署
|
|
392
|
+
|
|
393
|
+
### Docker 部署
|
|
394
|
+
|
|
395
|
+
```dockerfile
|
|
396
|
+
# Dockerfile
|
|
397
|
+
FROM node:20-alpine
|
|
398
|
+
|
|
399
|
+
WORKDIR /app
|
|
400
|
+
|
|
401
|
+
COPY package*.json ./
|
|
402
|
+
RUN npm install --production
|
|
403
|
+
|
|
404
|
+
COPY . .
|
|
405
|
+
|
|
406
|
+
EXPOSE 3000
|
|
407
|
+
|
|
408
|
+
CMD ["node", "src/index.js"]
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
构建并运行:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
docker build -t huxy-server .
|
|
415
|
+
docker run -p 3000:3000 -d huxy-server
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### PM2 部署
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
# 安装 PM2
|
|
422
|
+
npm install -g pm2
|
|
423
|
+
|
|
424
|
+
# 启动服务
|
|
425
|
+
pm2 start src/index.js --name huxy-server
|
|
426
|
+
|
|
427
|
+
# 保存进程列表
|
|
428
|
+
pm2 save
|
|
429
|
+
|
|
430
|
+
# 设置开机启动
|
|
431
|
+
pm2 startup
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Nginx 反向代理
|
|
435
|
+
|
|
436
|
+
```nginx
|
|
437
|
+
server {
|
|
438
|
+
listen 80;
|
|
439
|
+
server_name yourdomain.com;
|
|
440
|
+
|
|
441
|
+
location / {
|
|
442
|
+
proxy_pass http://localhost:3000;
|
|
443
|
+
proxy_http_version 1.1;
|
|
444
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
445
|
+
proxy_set_header Connection 'upgrade';
|
|
446
|
+
proxy_set_header Host $host;
|
|
447
|
+
proxy_cache_bypass $http_upgrade;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## 📝 许可证
|
|
453
|
+
|
|
454
|
+
MIT © [ahyiru](https://github.com/ahyiru)
|
|
455
|
+
|
|
456
|
+
## 🤝 贡献
|
|
457
|
+
|
|
458
|
+
欢迎贡献!请遵循以下步骤:
|
|
459
|
+
|
|
460
|
+
1. Fork 仓库
|
|
461
|
+
2. 创建您的特性分支 (`git checkout -b feature/AmazingFeature`)
|
|
462
|
+
3. 提交您的更改 (`git commit -m 'Add some AmazingFeature'`)
|
|
463
|
+
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
|
464
|
+
5. 打开一个 Pull Request
|
|
465
|
+
|
|
466
|
+
## 📞 支持
|
|
467
|
+
|
|
468
|
+
如果您有任何问题或建议,请通过以下方式联系:
|
|
469
|
+
|
|
470
|
+
- GitHub Issues: https://github.com/ahyiru/huxy-node-server/issues
|
|
471
|
+
- 电子邮件: ahyiru@example.com
|
|
472
|
+
|
|
473
|
+
## 📄 详细文档
|
|
474
|
+
|
|
475
|
+
- [文档索引](https://github.com/ahyiru/huxy-node-server/blob/main/docs/INDEX.md)
|
|
476
|
+
- [API 文档](https://github.com/ahyiru/huxy-node-server/blob/main/docs/API.md)
|
|
477
|
+
- [配置指南](https://github.com/ahyiru/huxy-node-server/blob/main/docs/CONFIGURATION.md)
|
|
478
|
+
- [部署指南](https://github.com/ahyiru/huxy-node-server/blob/main/docs/DEPLOYMENT.md)
|
|
479
|
+
- [更新日志](https://github.com/ahyiru/huxy-node-server/blob/main/CHANGELOG.md)
|
|
480
|
+
|
|
481
|
+
## 📚 相关资源
|
|
482
|
+
|
|
483
|
+
- [Express.js 文档](https://expressjs.com/)
|
|
484
|
+
- [Pino 日志文档](https://getpino.io/)
|
|
485
|
+
- [Helmet 安全文档](https://helmetjs.github.io/)
|
|
486
|
+
- [Node.js 文档](https://nodejs.org/docs/)
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
✨ **Huxy Node Server** - 为现代 Web 应用程序提供强大、可靠的后端解决方案!
|
package/example.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {startServer, startStatic, logger, createLogger, dateTime} from './src/index.js';
|
|
2
|
+
|
|
3
|
+
logger.info(dateTime());
|
|
4
|
+
|
|
5
|
+
const testLogger = createLogger('test');
|
|
6
|
+
|
|
7
|
+
testLogger.info({x: 123}, '测试');
|
|
8
|
+
logger.error({status: 400}, 'HTTP请求错误');
|
|
9
|
+
|
|
10
|
+
// startServer
|
|
11
|
+
const {app, config, httpServer} = await startServer({
|
|
12
|
+
port: 8080,
|
|
13
|
+
host: 'localhost',
|
|
14
|
+
// ...
|
|
15
|
+
}, (config, app, httpServer) => {
|
|
16
|
+
app.get('/config', (req, res) => {
|
|
17
|
+
logger.info('详细配置:', config);
|
|
18
|
+
res.status(200).json({
|
|
19
|
+
result: config,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// startStatic
|
|
25
|
+
const huxyServer = await startStatic({
|
|
26
|
+
port: 9000,
|
|
27
|
+
basepath: '/',
|
|
28
|
+
buildPath: './build',
|
|
29
|
+
}, (config, app) => {
|
|
30
|
+
logger.info(config);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// 启动服务可加参数如:node example.js port=8080 或 PORT=8080 node example.js
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "huxy-node-server",
|
|
3
|
+
"version": "1.0.0-beta.0",
|
|
4
|
+
"description": "一个精炼、高性能的 Express.js 服务器模板,为现代 Node.js 应用程序设计,提供灵活的功能和最佳实践。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./*": {
|
|
12
|
+
"import": "./src/*.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node ./src/index.js",
|
|
17
|
+
"dev": "NODE_ENV=development node --watch ./src/index.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"express",
|
|
21
|
+
"nodejs",
|
|
22
|
+
"esm",
|
|
23
|
+
"server",
|
|
24
|
+
"huxy"
|
|
25
|
+
],
|
|
26
|
+
"author": "ahyiru",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/ahyiru/huxy-node-server/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/ahyiru/huxy-node-server#readme",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/ahyiru/huxy-node-server.git"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"compression": "^1.8.1",
|
|
38
|
+
"cors": "^2.8.5",
|
|
39
|
+
"dotenv": "^17.2.3",
|
|
40
|
+
"express": "^5.2.1",
|
|
41
|
+
"express-rate-limit": "^8.2.1",
|
|
42
|
+
"helmet": "^8.1.0",
|
|
43
|
+
"pino": "^10.1.0",
|
|
44
|
+
"pino-http": "^11.0.0",
|
|
45
|
+
"pino-pretty": "^13.1.3"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=20.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import T from 'express';
|
|
2
|
+
import V from 'helmet';
|
|
3
|
+
import F from 'cors';
|
|
4
|
+
import {rateLimit as K, ipKeyGenerator as W} from 'express-rate-limit';
|
|
5
|
+
import z from 'compression';
|
|
6
|
+
import X from 'pino-http';
|
|
7
|
+
import {createServer as B} from 'node:http';
|
|
8
|
+
import U from 'pino';
|
|
9
|
+
import w from 'node:os';
|
|
10
|
+
import D from 'node:net';
|
|
11
|
+
var m = (t = new Date()) => t.toLocaleString('zh-CN', {timeZone: 'Asia/Shanghai', hour12: !1}),
|
|
12
|
+
l = t => {
|
|
13
|
+
let o = t ? 'https' : 'http',
|
|
14
|
+
e = w.networkInterfaces(),
|
|
15
|
+
r = [];
|
|
16
|
+
return (Object.keys(e).map(i => r.push(...e[i])), r.filter(i => i.family === 'IPv4').map(i => `${o}://${i.address}`));
|
|
17
|
+
},
|
|
18
|
+
x = t => {
|
|
19
|
+
let o = t ?? process.argv.slice(2) ?? [],
|
|
20
|
+
e = {};
|
|
21
|
+
return (
|
|
22
|
+
o.map(r => {
|
|
23
|
+
let [n, s] = r.split('=');
|
|
24
|
+
e[n] = s;
|
|
25
|
+
}),
|
|
26
|
+
e
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
H = {
|
|
30
|
+
NODE_ENV: 'nodeEnv',
|
|
31
|
+
PORT: 'port',
|
|
32
|
+
STATIC_PORT: 'staticPort',
|
|
33
|
+
HOST: 'host',
|
|
34
|
+
BASEPATH: 'basepath',
|
|
35
|
+
CORS_ORIGIN: 'cors.origin',
|
|
36
|
+
RATE_LIMIT_WINDOW_MS: 'rateLimit.windowMs',
|
|
37
|
+
RATE_LIMIT_MAX_REQUESTS: 'rateLimit.limit',
|
|
38
|
+
LOG_LEVEL: 'logLevel',
|
|
39
|
+
API_PREFIX: 'apiPrefix',
|
|
40
|
+
JWT_SECRET: 'secret',
|
|
41
|
+
AUTH_TOKEN: 'authToken',
|
|
42
|
+
},
|
|
43
|
+
M = (t, o, e) => {
|
|
44
|
+
let [r, n] = t.split('.');
|
|
45
|
+
r && n ? (e[r] || (e[r] = {}), (e[r][n] = o)) : (e[r] = o);
|
|
46
|
+
},
|
|
47
|
+
u = (t = {}, o = H) => {
|
|
48
|
+
let {env: e} = process;
|
|
49
|
+
Object.keys(o).map(n => {
|
|
50
|
+
let s = e[n];
|
|
51
|
+
s && M(o[n], s, t);
|
|
52
|
+
});
|
|
53
|
+
let r = {...t, ...x()};
|
|
54
|
+
return ((r.port = r.staticPort || r.port), (r.isDev = r.NODE_ENV === 'development'), r);
|
|
55
|
+
},
|
|
56
|
+
d = (t, o = '127.0.0.1') =>
|
|
57
|
+
new Promise(e => {
|
|
58
|
+
let r = D.createServer();
|
|
59
|
+
(r.once('error', n => {
|
|
60
|
+
(r.close(), e((n.code === 'EADDRINUSE', !1)));
|
|
61
|
+
}),
|
|
62
|
+
r.once('listening', () => {
|
|
63
|
+
(r.close(), e(!0));
|
|
64
|
+
}),
|
|
65
|
+
r.listen(Number(t), o));
|
|
66
|
+
});
|
|
67
|
+
import 'dotenv';
|
|
68
|
+
var k = {
|
|
69
|
+
nodeEnv: 'production',
|
|
70
|
+
isDev: !1,
|
|
71
|
+
port: parseInt(process.env.PORT || '3000', 10),
|
|
72
|
+
host: process.env.HOST || '0.0.0.0',
|
|
73
|
+
basepath: process.env.BASEPATH || '/',
|
|
74
|
+
cors: {origin: process.env.CORS_ORIGIN?.split(',') || '*', credentials: !0},
|
|
75
|
+
rateLimit: {
|
|
76
|
+
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10),
|
|
77
|
+
limit: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10),
|
|
78
|
+
message: {error: '\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5'},
|
|
79
|
+
},
|
|
80
|
+
helmet: {
|
|
81
|
+
contentSecurityPolicy: {directives: {defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", 'data:', 'https:']}},
|
|
82
|
+
crossOriginEmbedderPolicy: !1,
|
|
83
|
+
},
|
|
84
|
+
logLevel: process.env.LOG_LEVEL || 30,
|
|
85
|
+
},
|
|
86
|
+
f = k;
|
|
87
|
+
var c = (t, o) =>
|
|
88
|
+
U({
|
|
89
|
+
name: t,
|
|
90
|
+
level: f.logLevel,
|
|
91
|
+
transport: {target: 'pino-pretty', options: {colorize: !0}, ignore: 'pid,hostname,level,time', translateTime: 'UTC:yyyy-mm-dd HH:MM:ss', customColors: 'err:red,info:blue'},
|
|
92
|
+
...o,
|
|
93
|
+
}),
|
|
94
|
+
P = () => {
|
|
95
|
+
let t = c('http-request');
|
|
96
|
+
return (o, e, r) => {
|
|
97
|
+
let n = Date.now();
|
|
98
|
+
(e.on('finish', () => {
|
|
99
|
+
let s = Date.now() - n,
|
|
100
|
+
i = {method: o.method, url: o.originalUrl, status: e.statusCode, duration: `${s}ms`, ip: o.ip, userAgent: o.get('User-Agent'), timestamp: m()};
|
|
101
|
+
e.statusCode >= 500 ? t.error(i, 'HTTP\u8BF7\u6C42\u9519\u8BEF') : e.statusCode >= 400 ? t.warn(i, 'HTTP\u5BA2\u6237\u7AEF\u9519\u8BEF') : t.info(i, 'HTTP\u8BF7\u6C42');
|
|
102
|
+
}),
|
|
103
|
+
r());
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
a = c('huxy');
|
|
107
|
+
var y = c('error-handler'),
|
|
108
|
+
b = t => (o, e, r) => {
|
|
109
|
+
(y.error({message: 'Not Found', timestamp: m(), url: o.originalUrl, method: o.method, ip: o.ip, userAgent: o.get('User-Agent')}, '\u627E\u4E0D\u5230\u8DEF\u5F84'),
|
|
110
|
+
e.status(404).json({success: !1, timestamp: m(), status: 404, message: `\u8DEF\u7531 ${o.method} ${o.originalUrl} \u4E0D\u5B58\u5728`, url: o.originalUrl}));
|
|
111
|
+
},
|
|
112
|
+
A = t => (o, e, r, n) => {
|
|
113
|
+
let s = o.status || 500,
|
|
114
|
+
i = o.message;
|
|
115
|
+
(y.error({message: i, timestamp: m(), stack: o.stack, url: e.originalUrl, method: e.method, ip: e.ip, userAgent: e.get('User-Agent')}, '\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF'),
|
|
116
|
+
r.status(s).json({success: !1, timestamp: m(), message: t.isDev ? i : '\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF', stack: t.isDev ? o.stack : void 0}));
|
|
117
|
+
};
|
|
118
|
+
var L = t => (o, e, r) => {
|
|
119
|
+
(o.path.match(/\.(js|css|png|jpe?g|ico|webp|svg|mpeg|webm|m4a)$/) ? e.set('Cache-Control', 'public, max-age=31536000, immutable') : e.set('Cache-Control', 'no-cache'), r());
|
|
120
|
+
};
|
|
121
|
+
import {Router as j} from 'express';
|
|
122
|
+
var G = t => {
|
|
123
|
+
let o = j();
|
|
124
|
+
return (
|
|
125
|
+
o.use('/health', (e, r) => {
|
|
126
|
+
r.status(200).json({status: 'OK', timestamp: m(), uptime: process.uptime(), environment: t.nodeEnv, memoryUsage: process.memoryUsage(), pid: process.pid});
|
|
127
|
+
}),
|
|
128
|
+
o.get('/', (e, r) => {
|
|
129
|
+
r.status(200).json({message: 'Node.js \u670D\u52A1\u5668\u8FD0\u884C\u4E2D', timestamp: m(), environment: t.nodeEnv});
|
|
130
|
+
}),
|
|
131
|
+
o
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
N = G;
|
|
135
|
+
var Q = (t, o = {}) => (
|
|
136
|
+
t.disable('x-powered-by'),
|
|
137
|
+
t.set('trust proxy', 1),
|
|
138
|
+
t.use(V(o.helmet)),
|
|
139
|
+
t.use(F(o.cors)),
|
|
140
|
+
t.use(K({keyGenerator: e => W(e.ip) || e.headers['x-huxy-auth'] || e.headers['x-api-key'] || e.headers.authorization, ...o.rateLimit})),
|
|
141
|
+
t.use(z()),
|
|
142
|
+
t.use(T.json({limit: '20mb'})),
|
|
143
|
+
t.use(T.urlencoded({extended: !0, limit: '20mb'})),
|
|
144
|
+
t.use(X({logger: a, quietReqLogger: !0, autoLogging: !1})),
|
|
145
|
+
t.use(P()),
|
|
146
|
+
t.use(L(o)),
|
|
147
|
+
t
|
|
148
|
+
),
|
|
149
|
+
J = t => {
|
|
150
|
+
let o = e => {
|
|
151
|
+
(a.info(`\u6536\u5230 ${e} \u4FE1\u53F7, \u{1F6D1} \u6B63\u5728\u5173\u95ED\u670D\u52A1\u5668...`),
|
|
152
|
+
t.close(() => {
|
|
153
|
+
(a.info('\u{1F44B} \u670D\u52A1\u5668\u5DF2\u5173\u95ED'), process.exit(0));
|
|
154
|
+
}),
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
(a.error('\u274C \u5F3A\u5236\u5173\u95ED\u670D\u52A1\u5668'), process.exit(1));
|
|
157
|
+
}, 5e3));
|
|
158
|
+
};
|
|
159
|
+
(process.on('SIGTERM', () => o('SIGTERM')),
|
|
160
|
+
process.on('SIGINT', () => o('SIGINT')),
|
|
161
|
+
process.on('uncaughtException', e => {
|
|
162
|
+
(a.error(e, `\u672A\u6355\u83B7\u7684\u5F02\u5E38: ${e.message}`), process.exit(1));
|
|
163
|
+
}),
|
|
164
|
+
process.on('unhandledRejection', (e, r) => {
|
|
165
|
+
(a.error({reason: e, promise: r}, '\u672A\u5904\u7406\u7684 Promise \u62D2\u7EDD'), process.exit(1));
|
|
166
|
+
}));
|
|
167
|
+
},
|
|
168
|
+
Z = async (t, o) => {
|
|
169
|
+
let e = u(t),
|
|
170
|
+
{port: r} = e;
|
|
171
|
+
(await d(r, e.host)) || ((e.port = Number(r) + 1), a.warn(`\u7AEF\u53E3 ${r} \u5DF2\u88AB\u5360\u7528\uFF0C\u73B0\u5728\u4F7F\u7528\u7AEF\u53E3 ${e.port}`));
|
|
172
|
+
let s = T();
|
|
173
|
+
Q(s, e);
|
|
174
|
+
let i = B(s);
|
|
175
|
+
return (o?.(i, s, e), s.use(N(e)), s.use(b(e)), s.use(A(e)), J(i), {app: s, httpServer: i, config: e});
|
|
176
|
+
},
|
|
177
|
+
R = Z;
|
|
178
|
+
var Y = (t, o, e) =>
|
|
179
|
+
R({...f, ...t}, (r, n, s) => {
|
|
180
|
+
let {port: i, host: p, nodeEnv: h, basepath: I, appName: C = 'HuxyServer'} = s;
|
|
181
|
+
r.listen(i, p, () => {
|
|
182
|
+
if (!e) {
|
|
183
|
+
let $ = l()
|
|
184
|
+
.filter(v => v !== `http://${p}`)
|
|
185
|
+
.map(v => `http://${v}:${i}${I}`);
|
|
186
|
+
(a.info(`-----------------------${C}-----------------------`),
|
|
187
|
+
a.info(`\u{1F680} \u670D\u52A1\u8FD0\u884C\u5728\u3010${h}\u3011\u73AF\u5883: http://${p}:${i}${I}`),
|
|
188
|
+
a.info(`-----------------[${m()}]------------------`),
|
|
189
|
+
a.info({ips: $}, '\u672C\u5730\u5730\u5740\uFF1A'));
|
|
190
|
+
}
|
|
191
|
+
o?.(s, n, r);
|
|
192
|
+
});
|
|
193
|
+
}),
|
|
194
|
+
g = Y;
|
|
195
|
+
import oe from 'express';
|
|
196
|
+
import {fileURLToPath as q} from 'node:url';
|
|
197
|
+
import {dirname as ee, resolve as te} from 'node:path';
|
|
198
|
+
var E = (t = import.meta.url) => ee(q(t)),
|
|
199
|
+
S = t => te(E(), t),
|
|
200
|
+
_ = S;
|
|
201
|
+
var re = {port: 9e3, host: 'localhost', basepath: '/', buildPath: './build'},
|
|
202
|
+
se = (t, o) =>
|
|
203
|
+
g({...re, ...t}, (e, r, n) => {
|
|
204
|
+
let {basepath: s, buildPath: i} = e;
|
|
205
|
+
(r.use(s, oe.static(i, {maxAge: '1y', immutable: !0})),
|
|
206
|
+
r.get(`${s}/{*splat}`.replace('//', '/'), (p, h) => {
|
|
207
|
+
h.sendFile(_(i, 'index.html'));
|
|
208
|
+
}),
|
|
209
|
+
o?.(e, r, n));
|
|
210
|
+
}),
|
|
211
|
+
O = se;
|
|
212
|
+
var Ye = {startServer: g, startStatic: O, logger: a, createLogger: c, dateTime: m, localIPs: l, nodeArgs: x, getEnvConfig: u, checkPort: d, getDirName: E, resolvePath: S};
|
|
213
|
+
export {
|
|
214
|
+
d as checkPort,
|
|
215
|
+
c as createLogger,
|
|
216
|
+
m as dateTime,
|
|
217
|
+
Ye as default,
|
|
218
|
+
E as getDirName,
|
|
219
|
+
u as getEnvConfig,
|
|
220
|
+
l as localIPs,
|
|
221
|
+
a as logger,
|
|
222
|
+
x as nodeArgs,
|
|
223
|
+
S as resolvePath,
|
|
224
|
+
g as startServer,
|
|
225
|
+
O as startStatic,
|
|
226
|
+
};
|