跳到主要内容JavaScript 全栈开发实战指南:Node.js 后端入门与避坑 | 极客日志JavaScriptNode.js大前端
JavaScript 全栈开发实战指南:Node.js 后端入门与避坑
综述由AI生成JavaScript 全栈开发的基础与实践。内容涵盖 Node.js 运行环境原理、CommonJS 模块规范及 npm 包管理。对比了 Express、Koa 和 Fastify 三大框架的优缺点及适用场景。详细讲解了后端开发中的爽点与痛点,包括异步编程、CPU 密集型任务处理、内存泄漏排查等。实战部分演示了从零搭建 REST API,涉及项目结构、数据库连接(MongoDB)、JWT 认证、文件上传、日志记录及 WebSocket 通信。最后提供了线上故障排查思路及 TypeScript、Docker 部署等进阶技巧,帮助前端开发者平滑过渡到后端开发。
moshang256 浏览 引言:JavaScript 服务端开发能力解析
说实话,第一次听说要用 JavaScript 写后端的时候,内心是拒绝的。那时候刚把 Vue 的响应式原理啃明白,正觉得自己在前端领域已经是个"人物"了,结果老大甩过来一句:"这个后台管理系统,你顺便把接口也写了吧。"
我当时的表情大概是:😱
"我?写后端?我只会写 console.log 啊!"
但硬着头皮上了之后才发现,原来每天写的那些 JavaScript,换个地方跑居然就能操作数据库、读写文件、甚至搞网络通信了。这感觉就像是你一直以为自己的自行车只能在小区里骑,结果有人告诉你它其实能上高速,还能飙到 120 码。
Node.js 这玩意儿,说白了就是让 JavaScript 脱了浏览器的"紧身衣",到服务器上撒欢儿。你不用再学什么 PHP、Java、Go(虽然这些都很牛逼),就拿着你熟悉的 JS 语法,就能搭个完整的服务端应用。这不是什么"前端入侵后端"的阴谋论,这是技术发展的自然结果——JavaScript 早就不是当年那个只能在浏览器里弹个 alert 的小脚本了。
所以这篇文章,咱们就聊聊怎么把前端的那套 JS 思维,平滑过渡到后端开发。不搞那些虚头八脑的概念,全是实战干货,还有踩过的坑(血淋淋的那种)。看完你会发现,写后端其实跟写前端差不多,都是接收请求、处理数据、返回响应,只不过场景从浏览器换到了服务器而已。
JavaScript 服务器端运行历史
很多人到现在还觉得这事儿挺魔幻的:JavaScript 不是浏览器里的语言吗?怎么就能跑服务器上了?
这事儿得感谢一个叫 Ryan Dahl 的哥们儿。2009 年,这大哥看着 Apache 服务器那种"一个请求一个线程"的玩法,觉得太特么浪费了。你想啊,传统服务器处理请求就像去银行柜台办业务,每个客户都得占一个窗口,哪怕你只是在填表(I/O 等待),那个窗口也得一直等着你。银行得开多少窗口才能扛住双十一的流量?
Ryan Dahl 的思路很清奇:既然 JavaScript 在浏览器里处理用户点击、网络请求这些异步事件玩得挺溜,那把它搬到服务器上,用事件驱动的方式处理 HTTP 请求,岂不是美哉?于是他把 Chrome 的 V8 引擎(就是那个让 JS 跑得飞快的引擎)单独抠出来,加上事件循环、非阻塞 I/O,搞出了 Node.js。
这里有个核心概念得整明白:事件循环(Event Loop)。别被这名字吓到,其实跟你前端写的 setTimeout、Promise.then 是一个道理。Node.js 里面,所有的 I/O 操作(读文件、查数据库、网络请求)都是异步的,不会阻塞主线程。当一个请求进来需要查数据库时,Node.js 不会傻等着,而是把这个任务扔给后台线程,自己继续处理下一个请求。等数据库查完了,再通过回调函数(或者现在的 async/await)把结果拿回来。
const fs = require('fs');
fs.readFile('huge-file.txt', (err, data) => {
if (err) throw err;
console.log('文件读完了,数据长度:', data.length);
});
console.log('这行会先执行,因为读文件是异步的');
看到没?代码不会卡在 readFile 那里,而是继续往下走。这种机制让 Node.js 用单线程就能处理成千上万个并发连接,跟 Nginx 是一个路数的。当然,代价就是你不能在 Node.js 里做 CPU 密集型计算,比如图像处理、复杂数学运算,这会阻塞事件循环,导致所有请求都卡住。后面我们会详细说怎么解决这个问题。
Node.js 运行时环境详解
很多人搞不清 Node.js 和 JavaScript 的关系,以为 Node.js 是一门新语言。其实啊,Node.js 就是一个运行时环境(Runtime),就像浏览器是 JavaScript 的运行时一样。它提供了 V8 引擎(执行 JS 代码)加上一堆 C++ 写的底层模块(文件系统、网络、进程管理等),让 JS 能在服务器上干活。
CommonJS:模块化的老祖宗
前端同学现在都用 ES Module(import/export),但 Node.js 从诞生起用的是CommonJS规范。虽然现在新版本也支持 ESM 了,但 npm 上 99% 的包还是 CommonJS 格式,你得先搞懂这个。
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
exports.subtract = (a, b) => a - b;
const math = require('./math.js');
const { add, multiply } = require('./math.js');
console.log(math.add(2, 3));
console.log(multiply(4, 5));
注意 require 是同步的,而且 Node.js 会缓存模块。第一次 require 会执行整个文件,后面再 require 同一个文件直接返回缓存。这个特性有时候会让你踩坑——比如你想动态重新加载配置,结果发现改了文件没生效。
npm:包管理器的江湖地位
说实话,npm 生态是 Node.js 最大的杀手锏。你想干啥几乎都能找到现成的包,有时候我都觉得 npm 上的包是不是太多了(毕竟有 left-pad 这种几行代码也发包的)。但不得不承认,这种"拿来主义"让开发效率起飞。
{
"name": "my-awesome-api",
"version": "1.0.0",
"description": "一个牛逼的 API 服务",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.0",
"mongoose": "^6.0.0",
"jsonwebtoken": "^8.5.0"
},
"devDependencies": {
"nodemon": "^2.0.0",
"jest": "^28.0.0"
}
}
- 锁定版本:别用
* 或者 latest,哪天作者发个 break change 你就哭了。package-lock.json 一定要提交到 git。
- 区分 dependencies 和 devDependencies:生产环境不需要测试工具,减小部署体积。
- 定期审计:
npm audit 能帮你发现漏洞,npm outdated 看看哪些包该升级了。
全局对象和浏览器不一样
前端有 window,Node.js 有 global。但有些 API 两边都有,比如 console、setTimeout,有些只有 Node.js 有:
console.log(__dirname);
console.log(__filename);
console.log(process.env.NODE_ENV);
console.log(process.argv);
const buf = Buffer.from('hello world', 'utf8');
console.log(buf);
避坑提醒:别在 Node.js 里用 window 或者 document,会报错。如果你写了 if (typeof window !== 'undefined'),说明你的代码想同时跑在浏览器和 Node.js 里,这种同构代码要小心处理。
Express、Koa、Fastify:选框架就像选对象
Node.js 裸写 HTTP 服务其实挺麻烦的,得手动处理路由、中间件、错误处理。所以大家都用框架,主要是这三个:Express(老牌稳重)、Koa(清新脱俗)、Fastify(性能怪兽)。我三个都用过,给你掰扯掰扯。
Express:老大哥还是稳
Express 从 2010 年就有了,生态最丰富,文档最齐全,初学者首选。它的设计理念很简单:中间件(Middleware)堆叠。
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method}${req.url}`);
next();
});
app.use(express.json());
app.use((err, req, res, next) => {
console.error('出大事了:', err.stack);
res.status(500).json({ error: '服务器抽风了' });
});
app.get('/users', (req, res) => {
const { page = 1, limit = 10 } = req.query;
res.json({
users: [{ id: 1, name: '张三' }],
page: parseInt(page),
limit: parseInt(limit)
});
});
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: '名字和邮箱必填' });
}
const newUser = { id: Date.now(), name, email };
res.status(201).json(newUser);
});
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
res.json({ id: userId, name: '用户' + userId });
});
app.listen(3000, () => {
console.log('服务器跑在 http://localhost:3000');
});
- 回调地狱:虽然可以用 async/await,但错误处理很蛋疼。如果你在一个 async 路由里抛错,不处理的话进程会直接崩。
app.get('/bad', async (req, res) => {
const data = await someAsyncOperation();
res.json(data);
});
app.get('/good', async (req, res, next) => {
try {
const data = await someAsyncOperation();
res.json(data);
} catch (err) {
next(err);
}
});
- 中间件顺序很重要:如果你先写了路由再写
express.json(),POST 请求会拿不到 body。
Koa:洋葱模型,真香警告
Koa 是 Express 原班人马搞的,号称"下一代 Web 框架"。最大的区别是用了 ES6 的 Generator(后来改成 async/await),并且引入了洋葱模型的中间件执行机制。
const Koa = require('koa');
const Router = require('@koa/router');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router();
app.use(async (ctx, next) => {
const start = Date.now();
console.log(`--> ${ctx.method}${ctx.url}`);
await next();
const ms = Date.now() - start;
console.log(`<-- ${ctx.method}${ctx.url} - ${ms}ms`);
});
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = { error: err.message };
app.emit('error', err, ctx);
}
});
app.use(bodyParser());
router.get('/users', async (ctx) => {
ctx.body = { users: [] };
});
router.post('/users', async (ctx) => {
const { name } = ctx.request.body;
if (!name) {
ctx.throw(400, '名字必填');
}
ctx.status = 201;
ctx.body = { id: Date.now(), name };
});
app.use(router.routes());
app.listen(3000);
洋葱模型是啥意思?就是中间件的执行顺序像剥洋葱:先执行第一个中间件的前半部分,然后进入第二个,再进入第三个…到底后再一层层返回。
app.use(async (ctx, next) => {
console.log('1-开始');
await next();
console.log('1-结束');
});
app.use(async (ctx, next) => {
console.log('2-开始');
await next();
console.log('2-结束');
});
app.use(async (ctx) => {
console.log('3-响应');
ctx.body = 'Hello';
});
这个特性特别适合做统计耗时、设置响应头这类需要在请求前后都执行的操作。
- 生态比 Express 小,很多功能要装第三方中间件(比如路由得装
@koa/router)。
- 对 ES6+ 语法依赖强,老 Node 版本可能跑不了。
Fastify:性能党的最爱
如果你在乎性能,选 Fastify。它用了 JSON Schema 做序列化,路由查找更快,而且天生支持异步。benchmarks 里经常能看到它比 Express 快 2-3 倍。
const fastify = require('fastify')({ logger: true });
fastify.get('/users', {
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer', default: 1 },
limit: { type: 'integer', default: 10 }
}
},
response: {
200: {
type: 'object',
properties: {
users: { type: 'array' }
}
}
}
}
}, async (request, reply) => {
const { page, limit } = request.query;
return { users: [], page, limit };
});
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' }
}
}
}
}, async (request, reply) => {
const { name, email } = request.body;
const newUser = { id: Date.now(), name, email };
reply.status(201);
return newUser;
});
fastify.setErrorHandler((error, request, reply) => {
fastify.log.error(error);
reply.status(500).send({ error: '搞砸了' });
});
const start = async () => {
try {
await fastify.listen(3000);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
- 生态相对小,有些特定功能可能找不到插件。
- schema 验证虽然爽,但写起来有点啰嗦。
我的建议
- 新手/快速原型:Express,文档多,stackoverflow 上答案多。
- 追求代码优雅:Koa,async/await 写起来舒服,错误处理好。
- 性能敏感/微服务:Fastify,快是真的快。
JS 写后端:爽点和痛点都很真实
爽在哪?
前端写 TypeScript,后端写 Java,你得在两种语法风格之间来回切换,很容易精神分裂。全栈 JS 的话,类型定义、工具函数、甚至验证逻辑都可以前后端共享。
exports.validateEmail = (email) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};
exports.validatePassword = (password) => {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(password);
};
早期的 Node.js 全是回调函数,三层嵌套下来代码横向发展,被称为"回调地狱"。现在有了 async/await,写起来跟同步代码差不多:
getUser(userId, (err, user) => {
if (err) return handleError(err);
getOrders(user.id, (err, orders) => {
if (err) return handleError(err);
getProducts(orders[0].id, (err, products) => {
if (err) return handleError(err);
});
});
});
async function getUserData(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const products = await getProducts(orders[0].id);
return products;
} catch (err) {
handleError(err);
}
}
- 数据库:mongoose(MongoDB)、sequelize(SQL)、prisma(新一代 ORM)
- 认证:passport.js、jsonwebtoken
- 验证:joi、yup、zod
- 测试:jest、mocha、supertest
- 部署:pm2、dockerode
痛在哪?
Node.js 是单线程的,如果你让它算斐波那契数列、处理图片、视频转码,整个服务器都会卡住,其他请求都等着。
app.get('/fibonacci/:n', (req, res) => {
const n = parseInt(req.params.n);
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
const result = fib(n);
res.json({ result });
});
- 用Worker Threads(Node.js 10.5+引入的真正多线程)
- 把计算任务扔到其他服务(比如 Python 服务、云函数)
- 用 C++ 写 Addon(门槛高,但性能最好)
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const express = require('express');
const app = express();
if (isMainThread) {
app.get('/fibonacci/:n', async (req, res) => {
const n = parseInt(req.params.n);
const worker = new Worker(__filename, { workerData: n });
worker.on('message', (result) => {
res.json({ result });
});
worker.on('error', (err) => {
res.status(500).json({ error: err.message });
});
});
app.listen(3000);
} else {
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
const result = fib(workerData);
parentPort.postMessage(result);
}
Node.js 的内存管理是自动的(V8 的垃圾回收),但写不好还是会泄漏。比如全局变量缓存了大数据、事件监听器没移除、闭包持有引用等。
const cache = {};
app.get('/data/:id', async (req, res) => {
const id = req.params.id;
if (cache[id]) {
return res.json(cache[id]);
}
const data = await fetchFromDatabase(id);
cache[id] = data;
res.json(data);
});
const LRU = require('lru-cache');
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 5 });
node --inspect + Chrome DevTools
heapdump 包生成堆快照
clinic.js 一站式诊断
如果某个请求抛了未捕获的异常,整个 Node.js 进程会崩溃,所有正在处理的请求都完蛋。
app.get('/crash', (req, res) => {
throw new Error('我炸了');
});
process.on('uncaughtException', (err) => {
console.error('未捕获的异常:', err);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
});
注意:uncaughtException 是最后的手段,别指望靠它维持服务运行。正确的做法是用 PM2 等进程管理器,崩溃后自动重启。
实战:从零搭一个能用的后端服务
光说不练假把式,咱们搞个完整的 REST API,包含数据库、认证、文件上传、日志等实际功能。
项目结构
my-api/
├── src/
│ ├── config/
│ │ ├── database.js
│ │ └── auth.js
│ ├── controllers/
│ │ ├── userController.js
│ │ └── uploadController.js
│ ├── middlewares/
│ │ ├── auth.js
│ │ ├── errorHandler.js
│ │ └── validator.js
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ ├── index.js
│ │ ├── user.js
│ │ └── upload.js
│ ├── utils/
│ │ ├── logger.js
│ │ └── response.js
│ └── app.js
├── uploads/
├── logs/
├── tests/
├── .env
├── .env.example
└── package.json
1. 基础搭建(Express 版)
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const routes = require('./routes');
const errorHandler = require('./middlewares/errorHandler');
const logger = require('./utils/logger');
const app = express();
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true
}));
app.use(compression());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use((req, res, next) => {
logger.info(`${req.method}${req.url} - ${req.ip}`);
next();
});
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
app.use('/api', routes);
app.use((req, res) => {
res.status(404).json({ error: '接口不存在' });
});
app.use(errorHandler);
module.exports = app;
const logger = require('../utils/logger');
module.exports = (err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
body: req.body,
user: req.user?.id
});
const isDev = process.env.NODE_ENV === 'development';
res.status(err.status || 500).json({
error: err.message || '服务器内部错误',
...(isDev && { stack: err.stack })
});
};
2. 数据库连接(以 MongoDB 为例)
const mongoose = require('mongoose');
const logger = require('../utils/logger');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000
});
logger.info(`MongoDB 连接成功:${conn.connection.host}`);
mongoose.connection.on('error', (err) => {
logger.error('MongoDB 连接错误:', err);
});
mongoose.connection.on('disconnected', () => {
logger.warn('MongoDB 连接断开');
});
} catch (error) {
logger.error('MongoDB 连接失败:', error);
process.exit(1);
}
};
const disconnectDB = async () => {
await mongoose.connection.close();
logger.info('MongoDB 连接已关闭');
};
module.exports = { connectDB, disconnectDB };
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, '邮箱必填'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, '邮箱格式不正确']
},
password: {
type: String,
required: [true, '密码必填'],
minlength: [6, '密码至少 6 位'],
select: false
},
name: {
type: String,
required: [true, '姓名必填'],
trim: true,
maxlength: [20, '姓名不能超过 20 字符']
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
avatar: String,
lastLogin: Date
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
userSchema.virtual('profile').get(function () {
return {
id: this._id,
name: this.name,
email: this.email,
role: this.role,
avatar: this.avatar
};
});
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
userSchema.methods.comparePassword = async function (candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
userSchema.statics.findByEmail = function (email) {
return this.findOne({ email }).select('+password');
};
module.exports = mongoose.model('User', userSchema);
3. JWT 认证(别乱用,有讲究)
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const generateToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
issuer: 'my-api',
audience: 'my-client'
}
);
};
const authenticate = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供认证令牌' });
}
const token = authHeader.substring(7);
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: '用户不存在' });
}
req.user = user;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已过期' });
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ error: '无效令牌' });
}
next(error);
}
};
const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
};
module.exports = { generateToken, authenticate, authorize };
const User = require('../models/User');
const { generateToken } = require('../middlewares/auth');
exports.register = async (req, res, next) => {
try {
const { email, password, name } = req.body;
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(409).json({ error: '邮箱已被注册' });
}
const user = await User.create({ email, password, name });
const token = generateToken(user._id);
res.status(201).json({
message: '注册成功',
token,
user: user.profile
});
} catch (error) {
next(error);
}
};
exports.login = async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await User.findByEmail(email);
if (!user) {
return res.status(401).json({ error: '邮箱或密码错误' });
}
const isValid = await user.comparePassword(password);
if (!isValid) {
return res.status(401).json({ error: '邮箱或密码错误' });
}
user.lastLogin = new Date();
await user.save();
const token = generateToken(user._id);
res.json({
message: '登录成功',
token,
user: user.profile
});
} catch (error) {
next(error);
}
};
exports.getProfile = async (req, res) => {
res.json({ user: req.user.profile });
};
4. 文件上传(别信那些 buffer 拼接的教程)
网上很多教程教你用 req.on('data', chunk => data.push(chunk)) 这种方式处理文件上传,这在生产环境就是找死。大文件会撑爆内存,而且代码复杂。用 multer 或者 formidable 才是正道。
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const uploadDir = path.join(__dirname, '../../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const dateDir = new Date().toISOString().split('T')[0];
const fullPath = path.join(uploadDir, dateDir);
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
}
cb(null, fullPath);
},
filename: (req, file, cb) => {
const uniqueName = `${uuidv4()}${path.extname(file.originalname)}`;
cb(null, uniqueName);
}
});
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error(`不支持的文件类型:${file.mimetype},只允许图片`), false);
}
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024,
files: 1
}
});
exports.uploadSingle = (fieldName) => {
return (req, res, next) => {
const uploadMiddleware = upload.single(fieldName);
uploadMiddleware(req, res, (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: '文件大小超过 5MB 限制' });
}
return res.status(400).json({ error: err.message });
} else if (err) {
return res.status(400).json({ error: err.message });
}
next();
});
};
};
exports.handleUpload = async (req, res, next) => {
try {
if (!req.file) {
return res.status(400).json({ error: '没有上传文件' });
}
const fileUrl = `/uploads/${path.relative(uploadDir, req.file.path)}`;
res.json({
message: '上传成功',
file: {
originalName: req.file.originalname,
filename: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype,
url: fileUrl
}
});
} catch (error) {
next(error);
}
};
exports.cleanupTempFiles = () => {
};
5. 日志记录(别只会 console.log)
生产环境用 console.log 就是灾难,没法分级、没法持久化、没法追踪。用 winston 或者 pino。
const winston = require('winston');
const path = require('path');
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
);
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'HH:mm:ss' }),
winston.format.printf(({ level, message, timestamp, stack }) => {
return `${timestamp} [${level}]: ${stack || message}`;
})
);
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
defaultMeta: { service: 'my-api' },
transports: [
new winston.transports.File({
filename: path.join(__dirname, '../../logs/error.log'),
level: 'error',
maxsize: 5242880,
maxFiles: 5
}),
new winston.transports.File({
filename: path.join(__dirname, '../../logs/combined.log'),
maxsize: 5242880,
maxFiles: 5
})
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({ format: consoleFormat }));
}
logger.exceptions.handle(new winston.transports.File({
filename: path.join(__dirname, '../../logs/exceptions.log')
}));
module.exports = logger;
6. WebSocket 实时通信
聊天室、实时通知、股票行情这些场景得用 WebSocket。socket.io 是首选,支持自动重连、房间管理、命名空间等高级功能。
const http = require('http');
const { Server } = require('socket.io');
const expressApp = express();
const server = http.createServer(expressApp);
const io = new Server(server, {
cors: {
origin: process.env.CLIENT_URL,
methods: ['GET', 'POST']
}
});
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) throw new Error('认证失败');
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId);
if (!user) throw new Error('用户不存在');
socket.userId = user._id;
socket.userName = user.name;
next();
} catch (err) {
next(new Error('认证失败:' + err.message));
}
});
io.on('connection', (socket) => {
logger.info(`用户连接:${socket.userName} (${socket.userId})`);
socket.join(`user:${socket.userId}`);
socket.join('general');
socket.to('general').emit('user:joined', {
userId: socket.userId,
userName: socket.userName,
time: new Date()
});
socket.on('message:send', async (data) => {
try {
const { content, room = 'general' } = data;
Message.create({
sender: socket.userId,
content,
room,
createdAt: new Date()
}).catch(err => logger.error('保存消息失败:', err));
io.to(room).emit('message:received', {
id: Date.now(),
sender: { id: socket.userId, name: socket.userName },
content,
time: new Date()
});
} catch (error) {
socket.emit('error', { message: '发送失败' });
}
});
socket.on('message:private', async (data) => {
const { toUserId, content } = data;
io.to(`user:${toUserId}`).emit('message:private', {
from: { id: socket.userId, name: socket.userName },
content,
time: new Date()
});
socket.emit('message:private:sent', {
to: toUserId,
content,
time: new Date()
});
});
socket.on('disconnect', () => {
logger.info(`用户断开:${socket.userName}`);
socket.to('general').emit('user:left', {
userId: socket.userId,
userName: socket.userName
});
});
});
module.exports = server;
7. 入口文件和启动配置
require('dotenv').config();
const server = require('./app');
const { connectDB, disconnectDB } = require('./config/database');
const logger = require('./utils/logger');
const PORT = process.env.PORT || 3000;
const startServer = async () => {
try {
await connectDB();
const httpServer = server.listen(PORT, () => {
logger.info(`服务器运行在端口 ${PORT},环境:${process.env.NODE_ENV}`);
});
const gracefulShutdown = (signal) => {
logger.info(`${signal} 信号接收,开始优雅关机...`);
httpServer.close(async () => {
logger.info('HTTP 服务器已关闭');
await disconnectDB();
logger.info('数据库连接已关闭');
process.exit(0);
});
setTimeout(() => {
logger.error('强制关机');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
} catch (error) {
logger.error('启动失败:', error);
process.exit(1);
}
};
startServer();
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/myapp
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_EXPIRES_IN=7d
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
LOG_LEVEL=debug
线上崩了?排查思路甩给你
不管你本地测得多完美,上线后总会遇到各种问题。502、504、内存爆炸、CPU100%…这时候别慌,按这个流程排查。
1. 先看进程还在不在
ps aux | grep node
pm2 list
pm2 logs
pm2 monit
tail -n 100 logs/error.log
pm2 logs --err
2. 内存泄漏排查
- 打开 Chrome -> DevTools -> Memory
- Load 刚才的快照文件
- 看"Retained Size"最大的对象,找到泄漏源
npm install -g clinic
clinic doctor -- node src/index.js
3. 性能瓶颈分析
npm install -g 0x
0x node src/index.js
4. 关键救命代码
process.on('uncaughtException', (err) => {
logger.error('未捕获异常:', err);
setTimeout(() => {
process.exit(1);
}, 1000);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('未处理的 Promise 拒绝:', reason);
});
setInterval(() => {
const usage = process.memoryUsage();
logger.info('内存使用:', {
rss: `${(usage.rss / 1024 / 1024).toFixed(2)} MB`,
heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`
});
if (usage.heapUsed > 1024 * 1024 * 1024) {
logger.error('内存使用过高,准备重启');
process.exit(1);
}
}, 60000);
前端老铁写后端的进阶实践
1. 统一代码风格
前后端都用 ESLint + Prettier,配置保持一致:
module.exports = {
root: true,
env: {
node: true,
es2021: true
},
extends: ['eslint:recommended'],
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'prefer-const': 'error',
'no-var': 'error'
}
};
2. TypeScript 全栈
export interface IUser {
id: string;
email: string;
name: string;
role: 'user' | 'admin';
createdAt: Date;
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
meta?: {
page: number;
limit: number;
total: number;
};
}
import { Request, Response, NextFunction } from 'express';
import { IUser, ApiResponse } from '../types';
export const getUser = async (
req: Request<{ id: string }>,
res: Response<ApiResponse<IUser>>,
next: NextFunction
): Promise<void> => {
try {
const user = await User.findById(req.params.id);
if (!user) {
res.status(404).json({ success: false, error: '用户不存在' });
return;
}
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
};
3. Docker 一键部署
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# 先复制依赖文件,利用缓存层
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
# 非 root 用户运行,安全
USER node
CMD ["node", "src/index.js"]
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- MONGODB_URI=mongodb://mongo:27017/myapp
depends_on:
- mongo
restart: unless-stopped
mongo:
image: mongo:6
volumes:
- mongo-data:/data/db
restart: unless-stopped
volumes:
mongo-data:
4. API 文档自动生成
用 swagger-jsdoc 根据注释生成文档:
app.get('/api/users', userController.getUsers);
5. 环境变量管理
别到处写 process.env.XXX,集中管理:
require('dotenv').config();
const config = {
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT, 10) || 3000,
database: {
uri: process.env.MONGODB_URI,
options: {
maxPoolSize: 10
}
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
}
};
config.validate = () => {
const required = ['JWT_SECRET', 'MONGODB_URI'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`缺少环境变量:${missing.join(', ')}`);
}
};
config.validate();
module.exports = config;
6. 请求验证中间件
用 joi 或 zod 做参数验证,别让脏数据进数据库:
const Joi = require('joi');
const validate = (schema) => {
return (req, res, next) => {
const { error, value } = schema.validate(
{
body: req.body,
query: req.query,
params: req.params
},
{ abortEarly: false }
);
if (error) {
const errors = error.details.map(d => ({
field: d.path.join('.'),
message: d.message
}));
return res.status(400).json({ errors });
}
req.body = value.body;
req.query = value.query;
req.params = value.params;
next();
};
};
const userSchema = Joi.object({
body: Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
age: Joi.number().integer().min(0).optional()
}),
params: Joi.object({
id: Joi.string().hex().length(24)
})
});
router.post('/users', validate(userSchema), userController.create);
7. 优雅关机信号处理
前面代码里提过,但值得再强调。Docker 部署、PM2 重启时都会发 SIGTERM 信号,这时候得把正在处理的请求完成再退出,否则用户会收到 502 错误。
const gracefulShutdown = (signal) => {
console.log(`收到${signal},开始优雅关机...`);
server.close(() => {
console.log('HTTP 服务器已关闭');
process.exit(0);
});
setTimeout(() => {
console.error('强制退出');
process.exit(1);
}, 30000);
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
总结
写到这里,我突然想起三年前那个听说要写后端就腿软的自己。那时候觉得服务器是另一个世界,是 Java 和 Python 的天下,JavaScript 就该老老实实待在浏览器里操作 DOM。
但现在呢?我用 Node.js 写了几十个服务,从内部工具到日活百万的 API,从简单的 CRUD 到复杂的实时通信系统。JavaScript 还是那个 JavaScript,只是它跑的地方变了,能做的事情变多了。
当然,别以为会写 fetch 就懂后端了。后端的水很深,数据库优化、缓存策略、分布式事务、微服务治理…这些够你学一辈子的。但也别被"后端"俩字吓退,Node.js 已经帮你把门槛降得很低了——你不需要学新语言,就能开始写服务端代码。
最重要的是开始写。先搭个简单的 REST API,然后加上数据库,再搞个认证,慢慢你会发现,原来那些看起来高大上的后端概念,其实就是这么回事儿。
JavaScript 早就不只是浏览器的玩具了。你的下一行代码,可能就在服务器上偷偷改变着某个用户的体验,支撑着某个产品的核心业务。
去吧,用你熟悉的 JS,去征服服务器的星辰大海。🚀
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online