一、实战项目四:即时聊天应用

1.1 项目概述

使用 Socket.io 实现一个实时聊天应用,包含:

  • 多房间聊天室
  • 用户在线状态
  • 消息广播与私信
  • 输入状态提示
  • 聊天历史记录

1.2 项目结构

1
2
3
4
5
6
7
8
9
10
chat-app/
├── public/
│ ├── index.html
│ ├── css/style.css
│ └── js/client.js
├── server/
│ └── index.js
├── models/
│ └── Room.js
└── package.json

1.3 安装依赖

1
2
npm init -y
npm install express socket.io

1.4 服务端实现

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// server/index.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: '*',
methods: ['GET', 'POST']
}
});

// 内存存储
const rooms = new Map(); // roomId -> Set of userId
const users = new Map(); // socketId -> { userId, username, roomId }
const messageHistory = new Map(); // roomId -> Array of messages

app.use(express.static(path.join(__dirname, '../public')));

// Socket.io 事件处理
io.on('connection', (socket) => {
console.log(`用户连接: ${socket.id}`);

// 用户加入房间
socket.on('join-room', ({ roomId, username }) => {
socket.join(roomId);
users.set(socket.id, { userId: socket.id, username, roomId });

if (!rooms.has(roomId)) {
rooms.set(roomId, new Set());
messageHistory.set(roomId, []);
}
rooms.get(roomId).add(socket.id);

// 通知房间其他人
io.to(roomId).emit('user-joined', {
userId: socket.id,
username,
onlineCount: rooms.get(roomId).size
});

// 发送历史消息
const history = messageHistory.get(roomId) || [];
socket.emit('message-history', history);

console.log(`${username} 加入房间 ${roomId}`);
});

// 发送消息
socket.on('send-message', ({ roomId, message }) => {
const user = users.get(socket.id);
if (!user) return;

const messageData = {
id: Date.now() + Math.random(),
userId: user.userId,
username: user.username,
content: message,
timestamp: new Date().toISOString()
};

// 保存历史
const history = messageHistory.get(roomId) || [];
history.push(messageData);
if (history.length > 100) history.shift(); // 最多保留100条
messageHistory.set(roomId, history);

// 广播消息
io.to(roomId).emit('receive-message', messageData);
});

// 私信
socket.on('send-private-message', ({ targetId, message }) => {
const user = users.get(socket.id);
if (!user) return;

const messageData = {
id: Date.now() + Math.random(),
userId: user.userId,
username: user.username,
content: message,
timestamp: new Date().toISOString()
};

// 发送给目标用户
io.to(targetId).emit('receive-private-message', messageData);

// 也发送给自己(显示在聊天框)
socket.emit('receive-private-message', messageData);
});

// 输入状态
socket.on('typing', ({ roomId, isTyping }) => {
const user = users.get(socket.id);
if (!user) return;

socket.to(roomId).emit('user-typing', {
username: user.username,
isTyping
});
});

// 获取房间用户列表
socket.on('get-room-users', ({ roomId }, callback) => {
const roomUsers = [];
if (rooms.has(roomId)) {
for (const socketId of rooms.get(roomId)) {
const u = users.get(socketId);
if (u) roomUsers.push(u);
}
}
callback(roomUsers);
});

// 用户断开连接
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (!user) return;

const { roomId, username } = user;

// 从房间移除
if (rooms.has(roomId)) {
rooms.get(roomId).delete(socket.id);
io.to(roomId).emit('user-left', {
userId: socket.id,
username,
onlineCount: rooms.get(roomId).size
});
}

users.delete(socket.id);
console.log(`${username} 断开连接`);
});
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`聊天服务器运行在 http://localhost:${PORT}`);
});

1.5 客户端实现

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
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Node.js 聊天室</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<!-- 登录页面 -->
<div id="login-page" class="page">
<h1>💬 Node.js 聊天室</h1>
<input type="text" id="username-input" placeholder="请输入昵称">
<input type="text" id="room-input" placeholder="请输入房间号">
<button id="join-btn">进入聊天室</button>
</div>

<!-- 聊天页面 -->
<div id="chat-page" class="page hidden">
<div class="chat-container">
<div class="chat-header">
<h3 id="room-title">房间</h3>
<span id="online-count">在线: 0</span>
</div>

<div class="chat-main">
<div class="messages" id="messages"></div>
<div class="typing-indicator" id="typing-indicator"></div>
</div>

<div class="user-list" id="user-list"></div>

<div class="chat-input">
<input type="text" id="message-input" placeholder="输入消息...">
<button id="send-btn">发送</button>
</div>
</div>
</div>

<script src="/socket.io/socket.io.js"></script>
<script src="/js/client.js"></script>
</body>
</html>
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// public/js/client.js
const loginPage = document.getElementById('login-page');
const chatPage = document.getElementById('chat-page');
const joinBtn = document.getElementById('join-btn');
const sendBtn = document.getElementById('send-btn');
const usernameInput = document.getElementById('username-input');
const roomInput = document.getElementById('room-input');
const messagesDiv = document.getElementById('messages');
const onlineCountSpan = document.getElementById('online-count');
const roomTitle = document.getElementById('room-title');
const messageInput = document.getElementById('message-input');
const typingIndicator = document.getElementById('typing-indicator');
const userListDiv = document.getElementById('user-list');

let socket = null;
let currentRoom = null;
let currentUsername = null;

joinBtn.addEventListener('click', () => {
const username = usernameInput.value.trim();
const roomId = roomInput.value.trim();

if (!username || !roomId) {
alert('请输入昵称和房间号');
return;
}

currentUsername = username;
currentRoom = roomId;

socket = io();

socket.emit('join-room', { roomId, username });

socket.on('user-joined', ({ userId, username, onlineCount }) => {
appendSystemMessage(`${username} 加入了房间`);
updateOnlineCount(onlineCount);
});

socket.on('user-left', ({ username, onlineCount }) => {
appendSystemMessage(`${username} 离开了房间`);
updateOnlineCount(onlineCount);
});

socket.on('receive-message', (message) => {
appendMessage(message);
});

socket.on('message-history', (history) => {
messagesDiv.innerHTML = '';
history.forEach(msg => appendMessage(msg));
});

socket.on('user-typing', ({ username, isTyping }) => {
if (isTyping) {
typingIndicator.textContent = `${username} 正在输入...`;
} else {
typingIndicator.textContent = '';
}
});

socket.on('connect', () => {
socket.emit('get-room-users', { roomId }, (users) => {
renderUserList(users);
});
});

roomTitle.textContent = `房间: ${roomId}`;
loginPage.classList.add('hidden');
chatPage.classList.remove('hidden');
});

sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();

socket.emit('typing', {
roomId: currentRoom,
isTyping: messageInput.value.length > 0
});
});

function sendMessage() {
const value = messageInput.value.trim();
if (!value) return;

socket.emit('send-message', { roomId: currentRoom, message: value });
messageInput.value = '';
}

function appendMessage(message) {
const div = document.createElement('div');
div.className = 'message';
div.innerHTML = `
<span class="username">${escapeHtml(message.username)}</span>
<span class="time">${formatTime(message.timestamp)}</span>
<span class="content">${escapeHtml(message.content)}</span>
`;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

function appendSystemMessage(text) {
const div = document.createElement('div');
div.className = 'system-message';
div.textContent = text;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

function updateOnlineCount(count) {
onlineCountSpan.textContent = `在线: ${count}`;
}

function renderUserList(users) {
userListDiv.innerHTML = users.map(u =>
`<div class="user-item">${escapeHtml(u.username)}</div>`
).join('');
}

function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

function formatTime(timestamp) {
return new Date(timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}

二、实战项目五:文件管理系统

2.1 项目概述

实现一个完整的文件管理系统,包含:

  • 文件上传/下载
  • 文件夹浏览
  • 文件预览
  • 权限控制
  • 缩略图生成

2.2 安装依赖

1
2
npm init -y
npm install express multer sharp mime-types

2.3 核心实现

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
// file-manager.js
const express = require('express');
const multer = require('multer');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const mime = require('mime-types');

const app = express();
const PORT = 3000;
const UPLOAD_DIR = path.join(__dirname, 'uploads');
const THUMBNAIL_DIR = path.join(__dirname, 'thumbnails');

// 确保目录存在
[UPLOAD_DIR, THUMBNAIL_DIR].forEach(dir => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
});

// 配置存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const userId = req.user?.id || 'anonymous';
const userDir = path.join(UPLOAD_DIR, userId);
if (!fs.existsSync(userDir)) fs.mkdirSync(userDir, { recursive: true });
cb(null, userDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, uniqueSuffix + path.extname(file.originalname));
}
});

const upload = multer({
storage,
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB
fileFilter: (req, file, cb) => {
const allowedTypes = [
'image/', 'video/', 'audio/',
'application/pdf', 'application/zip',
'text/'
];
const isAllowed = allowedTypes.some(type => file.mimetype.startsWith(type));
if (isAllowed) {
cb(null, true);
} else {
cb(new Error('不支持的文件类型'));
}
}
});

// 1. 上传文件
app.post('/api/files/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: '没有上传文件' });
}

// 生成缩略图(图片类型)
if (req.file.mimetype.startsWith('image/')) {
generateThumbnail(req.file.path, req.file.filename);
}

res.json({
message: '上传成功',
file: {
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
mimeType: req.file.mimetype,
path: req.file.path
}
});
});

// 2. 列出文件
app.get('/api/files', (req, res) => {
const userId = req.query.userId || 'anonymous';
const userDir = path.join(UPLOAD_DIR, userId);

if (!fs.existsSync(userDir)) {
return res.json({ files: [] });
}

const files = fs.readdirSync(userDir).map(filename => {
const filePath = path.join(userDir, filename);
const stats = fs.statSync(filePath);
return {
filename,
size: stats.size,
createdAt: stats.birthtime,
isDirectory: stats.isDirectory()
};
});

res.json({ files });
});

// 3. 下载文件
app.get('/api/files/download/:filename', (req, res) => {
const userId = req.query.userId || 'anonymous';
const filePath = path.join(UPLOAD_DIR, userId, req.params.filename);

if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: '文件不存在' });
}

const originalName = req.query.name || req.params.filename;
res.download(filePath, originalName);
});

// 4. 预览文件
app.get('/api/files/preview/:filename', (req, res) => {
const userId = req.query.userId || 'anonymous';
const filePath = path.join(UPLOAD_DIR, userId, req.params.filename);

if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: '文件不存在' });
}

const mimeType = mime.lookup(filePath);
const stat = fs.statSync(filePath);
const fileStream = fs.createReadStream(filePath);

res.setHeader('Content-Type', mimeType);
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Disposition', 'inline');

fileStream.pipe(res);
});

// 5. 获取缩略图
app.get('/api/files/thumbnail/:filename', (req, res) => {
const thumbnailPath = path.join(THUMBNAIL_DIR, req.params.filename);

if (!fs.existsSync(thumbnailPath)) {
return res.status(404).json({ error: '缩略图不存在' });
}

res.sendFile(thumbnailPath);
});

// 6. 删除文件
app.delete('/api/files/:filename', (req, res) => {
const userId = req.query.userId || 'anonymous';
const filePath = path.join(UPLOAD_DIR, userId, req.params.filename);
const thumbnailPath = path.join(THUMBNAIL_DIR, req.params.filename);

if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: '文件不存在' });
}

fs.unlinkSync(filePath);
if (fs.existsSync(thumbnailPath)) {
fs.unlinkSync(thumbnailPath);
}

res.json({ message: '删除成功' });
});

// 生成缩略图
async function generateThumbnail(sourcePath, filename) {
try {
const thumbnailPath = path.join(THUMBNAIL_DIR, filename);
await sharp(sourcePath)
.resize(200, 200, { fit: 'cover' })
.jpeg({ quality: 80 })
.toFile(thumbnailPath);
console.log(`缩略图生成: ${filename}`);
} catch (error) {
console.error(`缩略图生成失败: ${error.message}`);
}
}

app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: '文件大小超过限制(最大50MB)' });
}
}
res.status(500).json({ error: err.message });
});

app.listen(PORT, () => {
console.log(`文件管理系统运行在 http://localhost:${PORT}`);
});

三、实战项目六、邮件服务系统

3.1 项目概述

实现一个邮件服务系统,包含:

  • 发送邮件(文本/HTML)
  • 附件发送
  • 批量邮件
  • 邮件模板
  • 发送状态追踪

3.2 安装依赖

1
2
npm init -y
npm install express nodemailer nodemailer-express-handlebars

3.3 核心实现

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// mail-service.js
const express = require('express');
const nodemailer = require('nodemailer');
const hbs = require('nodemailer-express-handlebars');
const path = require('path');
const fs = require('fs').promises;

const app = express();
app.use(express.json());

// 创建邮件传输器(使用QQ邮箱SMTP)
const transporter = nodemailer.createTransport({
service: 'QQ',
auth: {
user: 'your_email@qq.com',
pass: 'your_smtp_auth_code' // 邮箱授权码
}
});

// 配置模板引擎
const handlebarOptions = {
viewEngine: {
extName: '.hbs',
viewPath: path.resolve('./templates'),
layoutsDir: path.resolve('./templates/layouts'),
defaultLayout: 'main'
},
viewPath: path.resolve('./templates'),
extName: '.hbs'
};

transporter.use('compile', hbs(handlebarOptions));

// 邮件发送记录
const sentEmails = [];

// 1. 发送简单邮件
app.post('/api/email/send', async (req, res) => {
try {
const { to, subject, text, html } = req.body;

const mailOptions = {
from: 'your_email@qq.com',
to,
subject,
text,
html
};

const info = await transporter.sendMail(mailOptions);

sentEmails.push({
id: info.messageId,
to,
subject,
sentAt: new Date().toISOString(),
status: 'sent'
});

res.json({
message: '邮件发送成功',
messageId: info.messageId
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// 2. 发送带附件的邮件
app.post('/api/email/send-with-attachment', async (req, res) => {
try {
const { to, subject, text, attachments } = req.body;

const mailOptions = {
from: 'your_email@qq.com',
to,
subject,
text,
attachments: attachments.map(att => ({
filename: att.filename,
content: Buffer.from(att.content, 'base64'),
contentType: att.contentType
}))
};

const info = await transporter.sendMail(mailOptions);

res.json({
message: '邮件发送成功',
messageId: info.messageId
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// 3. 使用模板发送邮件
app.post('/api/email/send-template', async (req, res) => {
try {
const { to, subject, template, context } = req.body;

const mailOptions = {
from: 'your_email@qq.com',
to,
subject,
template,
context
};

const info = await transporter.sendMail(mailOptions);

res.json({
message: '模板邮件发送成功',
messageId: info.messageId
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// 4. 批量发送邮件
app.post('/api/email/batch-send', async (req, res) => {
try {
const { recipients, subject, template, context } = req.body;
const results = [];

for (const recipient of recipients) {
try {
const mailOptions = {
from: 'your_email@qq.com',
to: recipient.email,
subject,
template,
context: { ...context, recipient }
};

const info = await transporter.sendMail(mailOptions);
results.push({
email: recipient.email,
success: true,
messageId: info.messageId
});
} catch (error) {
results.push({
email: recipient.email,
success: false,
error: error.message
});
}
}

res.json({
message: `批量发送完成:成功${results.filter(r => r.success).length}个,失败${results.filter(r => !r.success).length}个`,
results
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// 5. 获取发送记录
app.get('/api/email/sent', (req, res) => {
res.json({ emails: sentEmails });
});

// 6. 验证邮箱配置
app.get('/api/email/verify', async (req, res) => {
try {
await transporter.verify();
res.json({ status: 'ok', message: '邮箱配置有效' });
} catch (error) {
res.status(500).json({ status: 'error', message: error.message });
}
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`邮件服务运行在 http://localhost:${PORT}`);
});

邮件模板示例(templates/welcome.hbs):

1
2
3
4
5
6
7
8
9
<h1>欢迎 {{name}}!</h1>
<p>感谢您注册我们的服务。</p>
<p>您的账户信息:</p>
<ul>
<li>用户名:{{username}}</li>
<li>注册时间:{{registerDate}}</li>
</ul>
<p>点击 <a href="{{loginUrl}}">这里</a> 登录您的账户。</p>
<p>此邮件由系统自动发送,请勿回复。</p>

四、实战项目七:定时任务与爬虫系统

4.1 项目概述

实现一个定时任务管理系统,包含:

  • 定时爬虫任务
  • 数据采集与存储
  • 任务状态监控
  • 失败重试机制

4.2 安装依赖

1
2
npm init -y
npm install express node-cron axios cheerio better-sqlite3

4.3 核心实现

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
// scheduler.js
const express = require('express');
const cron = require('node-cron');
const axios = require('axios');
const cheerio = require('cheerio');
const Database = require('better-sqlite3');
const path = require('path');

const app = express();
app.use(express.json());

// 初始化数据库
const db = new Database(path.join(__dirname, 'tasks.db'));

db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL,
schedule TEXT NOT NULL,
selector TEXT,
status TEXT DEFAULT 'active',
lastRun TEXT,
lastResult TEXT,
createdAt TEXT DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS crawl_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
taskId INTEGER,
data TEXT,
crawledAt TEXT DEFAULT (datetime('now')),
FOREIGN KEY (taskId) REFERENCES tasks(id)
);

CREATE TABLE IF NOT EXISTS task_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
taskId INTEGER,
message TEXT,
level TEXT DEFAULT 'info',
createdAt TEXT DEFAULT (datetime('now')),
FOREIGN KEY (taskId) REFERENCES tasks(id)
);
`);

// 定时任务管理器
const taskManager = {
tasks: new Map(), // id -> cron job

async crawl(url, selector) {
const response = await axios.get(url, {
headers: { 'User-Agent': 'Mozilla/5.0' },
timeout: 15000
});

const $ = cheerio.load(response.data);
const results = [];

if (selector) {
$(selector).each((i, el) => {
results.push({
text: $(el).text().trim(),
html: $(el).html()
});
});
} else {
// 默认提取所有链接
$('a').each((i, el) => {
results.push({
text: $(el).text().trim(),
href: $(el).attr('href')
});
});
}

return results;
},

startTask(task) {
const job = cron.schedule(task.schedule, async () => {
const log = (level, message) => {
db.prepare('INSERT INTO task_logs (taskId, message, level) VALUES (?, ?, ?)')
.run(task.id, message, level);
};

try {
log('info', `任务开始执行`);
const results = await this.crawl(task.url, task.selector);

// 存储结果
const insert = db.prepare('INSERT INTO crawl_results (taskId, data) VALUES (?, ?)');
const insertMany = db.transaction((items) => {
for (const item of items) {
insert.run(task.id, JSON.stringify(item));
}
});
insertMany(results);

// 更新任务状态
db.prepare('UPDATE tasks SET lastRun = ?, lastResult = ? WHERE id = ?')
.run(new Date().toISOString(), `采集到${results.length}条数据`, task.id);

log('success', `任务执行成功,采集到${results.length}条数据`);
} catch (error) {
log('error', `任务执行失败: ${error.message}`);
}
});

this.tasks.set(task.id, job);
},

stopTask(taskId) {
const job = this.tasks.get(taskId);
if (job) {
job.stop();
this.tasks.delete(taskId);
}
},

restartTask(task) {
this.stopTask(task.id);
if (task.status === 'active') {
this.startTask(task);
}
}
};

// API 路由

// 1. 创建定时任务
app.post('/api/tasks', (req, res) => {
const { name, url, schedule, selector } = req.body;

const stmt = db.prepare('INSERT INTO tasks (name, url, schedule, selector) VALUES (?, ?, ?, ?)');
const result = stmt.run(name, url, schedule, selector);

const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid);
taskManager.startTask(task);

res.json({ message: '任务创建成功', task });
});

// 2. 获取所有任务
app.get('/api/tasks', (req, res) => {
const tasks = db.prepare('SELECT * FROM tasks').all();
res.json({ tasks });
});

// 3. 获取单个任务
app.get('/api/tasks/:id', (req, res) => {
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
if (!task) {
return res.status(404).json({ error: '任务不存在' });
}
res.json({ task });
});

// 4. 更新任务
app.put('/api/tasks/:id', (req, res) => {
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
if (!task) {
return res.status(404).json({ error: '任务不存在' });
}

const { name, url, schedule, selector, status } = req.body;
db.prepare('UPDATE tasks SET name=COALESCE(?,name), url=COALESCE(?,url), schedule=COALESCE(?,schedule), selector=COALESCE(?,selector), status=COALESCE(?,status) WHERE id=?')
.run(name, url, schedule, selector, status, req.params.id);

const updated = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
taskManager.restartTask(updated);

res.json({ task: updated });
});

// 5. 删除任务
app.delete('/api/tasks/:id', (req, res) => {
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
if (!task) {
return res.status(404).json({ error: '任务不存在' });
}

taskManager.stopTask(req.params.id);
db.prepare('DELETE FROM tasks WHERE id = ?').run(req.params.id);
db.prepare('DELETE FROM crawl_results WHERE taskId = ?').run(req.params.id);
db.prepare('DELETE FROM task_logs WHERE taskId = ?').run(req.params.id);

res.json({ message: '任务删除成功' });
});

// 6. 手动执行任务
app.post('/api/tasks/:id/run', async (req, res) => {
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
if (!task) {
return res.status(404).json({ error: '任务不存在' });
}

try {
const results = await taskManager.crawl(task.url, task.selector);
res.json({ message: '任务执行成功', count: results.length, results });
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// 7. 获取任务结果
app.get('/api/tasks/:id/results', (req, res) => {
const results = db.prepare('SELECT * FROM crawl_results WHERE taskId = ? ORDER BY id DESC LIMIT 100')
.all(req.params.id);
res.json({ results });
});

// 8. 获取任务日志
app.get('/api/tasks/:id/logs', (req, res) => {
const logs = db.prepare('SELECT * FROM task_logs WHERE taskId = ? ORDER BY id DESC LIMIT 50')
.all(req.params.id);
res.json({ logs });
});

// 9. 启动时恢复所有任务
const tasks = db.prepare('SELECT * FROM tasks WHERE status = "active"').all();
tasks.forEach(task => taskManager.startTask(task));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`定时任务系统运行在 http://localhost:${PORT}`);
});

五、实战项目八:命令行工具开发

5.1 项目概述

开发一个实用的命令行工具,包含:

  • 项目脚手架生成
  • 文件批量处理
  • Git 工作流自动化
  • 彩色输出与进度条

5.2 安装依赖

1
2
npm init -y
npm install commander chalk inquirer fs-extra ora

5.3 CLI 工具实现

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
// bin/cli.js
#!/usr/bin/env node

const { program } = require('commander');
const chalk = require('chalk');
const inquirer = require('inquirer');
const fs = require('fs-extra');
const path = require('path');
const ora = require('ora');
const { execSync } = require('child_process');

program
.name('devtool')
.description('开发者实用工具箱')
.version('1.0.0');

// 1. 创建项目
program
.command('create <project-name>')
.description('创建新项目')
.option('-t, --template <template>', '项目模板', 'express')
.action(async (projectName, options) => {
const spinner = ora('创建项目中...').start();

try {
const projectPath = path.resolve(process.cwd(), projectName);

if (fs.existsSync(projectPath)) {
spinner.fail(chalk.red(`目录 ${projectName} 已存在`));
return;
}

// 创建目录结构
fs.mkdirsSync(projectPath);

const template = options.template;

if (template === 'express') {
// Express 项目模板
createExpressProject(projectPath, projectName);
} else if (template === 'cli') {
// CLI 工具模板
createCliProject(projectPath, projectName);
} else if (template === 'api') {
// API 服务模板
createApiProject(projectPath, projectName);
}

spinner.succeed(chalk.green(`项目 ${projectName} 创建成功!`));

console.log(`\n${chalk.blue('下一步:')}`);
console.log(` cd ${projectName}`);
console.log(` npm install`);
console.log(` npm run dev\n`);

} catch (error) {
spinner.fail(chalk.red(`创建失败: ${error.message}`));
}
});

// 2. 文件批量处理
program
.command('batch')
.description('文件批量处理工具')
.option('-p, --path <path>', '目标目录', '.')
.option('-e, --ext <ext>', '文件扩展名', '.md')
.option('-r, --replace <old> <new>', '替换文本')
.action(async (options) => {
const spinner = ora('扫描文件中...').start();

try {
const targetDir = path.resolve(options.path);
const ext = options.ext;

if (!fs.existsSync(targetDir)) {
spinner.fail(chalk.red(`目录 ${targetDir} 不存在`));
return;
}

// 递归查找文件
const files = findFiles(targetDir, ext);
spinner.succeed(chalk.green(`找到 ${files.length}${ext} 文件`));

if (options.replace) {
const [oldStr, newStr] = options.replace;
let count = 0;

files.forEach(file => {
let content = fs.readFileSync(file, 'utf-8');
if (content.includes(oldStr)) {
content = content.replace(new RegExp(oldStr, 'g'), newStr);
fs.writeFileSync(file, content, 'utf-8');
count++;
console.log(` ${chalk.green('✓')} ${path.relative(targetDir, file)}`);
}
});

console.log(`\n${chalk.blue(`替换完成:共修改 ${count} 个文件`)}`);
} else {
// 显示文件列表
console.log(`\n${chalk.blue('文件列表:')}`);
files.forEach(file => {
console.log(` ${chalk.green('●')} ${path.relative(targetDir, file)}`);
});
}
} catch (error) {
spinner.fail(chalk.red(`处理失败: ${error.message}`));
}
});

// 3. Git 工作流
program
.command('gitflow')
.description('Git 工作流快捷命令')
.option('-m, --message <message>', '提交信息')
.option('-p, --push', '是否推送')
.action(async (options) => {
try {
// 检查是否是 git 仓库
if (!fs.existsSync('.git')) {
console.log(chalk.red('当前目录不是 Git 仓库'));
return;
}

const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: '选择操作:',
choices: [
{ name: '📦 提交所有更改', value: 'commit' },
{ name: '🔀 切换分支', value: 'branch' },
{ name: '📋 查看状态', value: 'status' },
{ name: '🧹 清理分支', value: 'clean' }
]
}
]);

switch (action) {
case 'commit':
const commitMsg = options.message || 'update';
execSync('git add -A', { stdio: 'inherit' });
execSync(`git commit -m "${commitMsg}"`, { stdio: 'inherit' });
if (options.push) {
execSync('git push', { stdio: 'inherit' });
}
console.log(chalk.green('✓ 提交成功'));
break;

case 'branch':
const { branchAction } = await inquirer.prompt([
{
type: 'list',
name: 'branchAction',
message: '选择分支操作:',
choices: ['查看分支', '创建分支', '切换分支']
}
]);

if (branchAction === '查看分支') {
execSync('git branch', { stdio: 'inherit' });
} else if (branchAction === '创建分支') {
const { branchName } = await inquirer.prompt([
{ type: 'input', name: 'branchName', message: '分支名称:' }
]);
execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' });
} else {
const { branchName } = await inquirer.prompt([
{ type: 'input', name: 'branchName', message: '分支名称:' }
]);
execSync(`git checkout ${branchName}`, { stdio: 'inherit' });
}
break;

case 'status':
execSync('git status', { stdio: 'inherit' });
break;

case 'clean':
execSync('git branch --merged', { stdio: 'inherit' });
break;
}
} catch (error) {
console.log(chalk.red(`操作失败: ${error.message}`));
}
});

// 4. 系统信息
program
.command('info')
.description('显示系统信息')
.action(() => {
console.log(`\n${chalk.blue('系统信息:')}`);
console.log(` Node.js: ${chalk.green(process.version)}`);
console.log(` Platform: ${chalk.green(process.platform)}`);
console.log(` Arch: ${chalk.green(process.arch)}`);
console.log(` CPU Cores: ${chalk.green(require('os').cpus().length)}`);
console.log(` Memory: ${chalk.green(`${(require('os').totalmem() / 1024 / 1024 / 1024).toFixed(1)} GB`)}`);
console.log(` Free Memory: ${chalk.green(`${(require('os').freemem() / 1024 / 1024 / 1024).toFixed(1)} GB`)}`);
console.log(` Home Dir: ${chalk.green(require('os').homedir())}`);
console.log(` Hostname: ${chalk.green(require('os').hostname())}`);
console.log('');
});

// 辅助函数
function findFiles(dir, ext, fileList = []) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
if (fs.statSync(filePath).isDirectory()) {
findFiles(filePath, ext, fileList);
} else if (file.endsWith(ext)) {
fileList.push(filePath);
}
});
return fileList;
}

function createExpressProject(projectPath, projectName) {
// package.json
fs.writeJSONSync(path.join(projectPath, 'package.json'), {
name: projectName,
version: '1.0.0',
description: '',
main: 'src/app.js',
scripts: {
start: 'node src/app.js',
dev: 'nodemon src/app.js'
},
dependencies: {
express: '^4.18.2',
cors: '^2.8.5',
dotenv: '^16.3.1'
},
devDependencies: {
nodemon: '^3.0.2'
}
}, { spaces: 2 });

// src 目录
fs.mkdirsSync(path.join(projectPath, 'src/routes'));
fs.mkdirsSync(path.join(projectPath, 'src/middleware'));

// src/app.js
fs.writeFileSync(path.join(projectPath, 'src/app.js'),
`const express = require('express');
const cors = require('cors');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 3000;

app.use(cors());
app.use(express.json());

// 路由
app.use('/api', require('./routes/index'));

app.listen(PORT, () => {
console.log(\`Server running on port \${PORT}\`);
});
`);

// src/routes/index.js
fs.writeFileSync(path.join(projectPath, 'src/routes/index.js'),
`const express = require('express');
const router = express.Router();

router.get('/health', (req, res) => {
res.json({ status: 'ok' });
});

module.exports = router;
`);

// .env
fs.writeFileSync(path.join(projectPath, '.env'), 'PORT=3000\n');

// .gitignore
fs.writeFileSync(path.join(projectPath, '.gitignore'),
'node_modules/\n.env\n');
}

function createCliProject(projectPath, projectName) {
fs.writeJSONSync(path.join(projectPath, 'package.json'), {
name: projectName,
version: '1.0.0',
bin: { [projectName]: 'bin/index.js' },
dependencies: {
commander: '^11.1.0',
chalk: '^4.1.2'
}
}, { spaces: 2 });

fs.mkdirsSync(path.join(projectPath, 'bin'));

fs.writeFileSync(path.join(projectPath, 'bin/index.js'),
`#!/usr/bin/env node
const { program } = require('commander');
const chalk = require('chalk');

program
.name('${projectName}')
.version('1.0.0')
.description('CLI tool');

program
.command('hello <name>')
.action((name) => {
console.log(chalk.green(\`Hello, \${name}!\`));
});

program.parse();
`);
}

function createApiProject(projectPath, projectName) {
fs.writeJSONSync(path.join(projectPath, 'package.json'), {
name: projectName,
version: '1.0.0',
main: 'src/app.js',
scripts: {
start: 'node src/app.js',
dev: 'nodemon src/app.js'
},
dependencies: {
express: '^4.18.2',
mongoose: '^7.6.3',
bcryptjs: '^2.4.3',
jsonwebtoken: '^9.0.2',
cors: '^2.8.5',
dotenv: '^16.3.1'
}
}, { spaces: 2 });

fs.mkdirsSync(path.join(projectPath, 'src/{models,routes,middleware,controllers}'));
}

program.parse();

使用方法:

1
2
3
4
5
6
7
8
9
10
11
# 创建项目
node bin/cli.js create my-api --template express

# 批量替换文件内容
node bin/cli.js batch -p ./docs -e .md -r "旧文本" "新文本"

# Git 工作流
node bin/cli.js gitflow -m "feat: 添加新功能" -p

# 查看系统信息
node bin/cli.js info

六、实战项目九:WebSocket 实时数据推送

6.1 项目概述

实现一个实时数据监控面板,包含:

  • WebSocket 实时推送
  • 数据可视化
  • 历史数据存储
  • 报警通知

6.2 核心实现

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// realtime-dashboard.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');

const app = express();
const server = http.createServer(app);
const io = new Server(server);

app.use(express.static(path.join(__dirname, 'public')));

// 模拟数据生成
class DataSimulator {
constructor() {
this.clients = new Set();
this.metrics = {
cpu: 0,
memory: 0,
disk: 0,
network: 0,
requests: 0
};
this.history = []; // 历史数据
this.historyMaxSize = 1000;

this.startSimulation();
}

startSimulation() {
// 每秒生成新数据
setInterval(() => {
this.metrics = {
cpu: this.randomValue(20, 80, this.metrics.cpu),
memory: this.randomValue(40, 70, this.metrics.memory),
disk: this.randomValue(30, 60, this.metrics.disk),
network: this.randomValue(10, 90, this.metrics.network),
requests: this.metrics.requests + Math.floor(Math.random() * 10)
};

const dataPoint = {
timestamp: Date.now(),
metrics: { ...this.metrics }
};

// 保存历史
this.history.push(dataPoint);
if (this.history.length > this.historyMaxSize) {
this.history.shift();
}

// 推送数据
io.emit('metrics-update', dataPoint);

// 检查报警条件
this.checkAlerts(dataPoint);

}, 1000);
}

randomValue(min, max, current) {
const trend = (Math.random() - 0.5) * 10;
let value = (current || (min + max) / 2) + trend;
return Math.max(min, Math.min(max, value));
}

checkAlerts(dataPoint) {
const { metrics } = dataPoint;

// CPU 报警
if (metrics.cpu > 75) {
io.emit('alert', {
type: 'warning',
message: `CPU 使用率过高: ${metrics.cpu.toFixed(1)}%`,
timestamp: Date.now()
});
}

// 内存报警
if (metrics.memory > 80) {
io.emit('alert', {
type: 'danger',
message: `内存使用率过高: ${metrics.memory.toFixed(1)}%`,
timestamp: Date.now()
});
}
}

getHistory(hours = 1) {
const cutoff = Date.now() - hours * 60 * 60 * 1000;
return this.history.filter(d => d.timestamp > cutoff);
}

getCurrentMetrics() {
return {
timestamp: Date.now(),
metrics: { ...this.metrics }
};
}
}

const simulator = new DataSimulator();

// Socket.io 事件
io.on('connection', (socket) => {
console.log('客户端连接');

// 发送当前数据
socket.emit('current-data', simulator.getCurrentMetrics());

// 发送历史数据
socket.emit('history-data', simulator.getHistory(1));

// 请求历史数据
socket.on('request-history', ({ hours }) => {
socket.emit('history-data', simulator.getHistory(hours || 1));
});

socket.on('disconnect', () => {
console.log('客户端断开');
});
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`实时数据监控面板运行在 http://localhost:${PORT}`);
});

💡 小结:通过这 5 个进阶实战项目,你已经掌握了 Node.js 在实时通信、文件管理、邮件服务、定时任务、CLI 工具、数据监控等领域的实战技能。每个项目都可以作为独立模块集成到实际业务中。