引言: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)把结果拿回来。
// 这就是 Node.js 非阻塞的精髓
const fs = require('fs');
// 传统阻塞式(Node.js 里千万别这么写!)
// const data = fs.readFileSync('huge-file.txt');
// console.log('文件读完了'); // 这期间服务器啥也干不了
// 正确的非阻塞式
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 格式,你得先搞懂这个。
// math.js - 导出模块
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// 可以导出多个
module.exports = { add, multiply };
// 也可以这样写
exports.subtract = (a, b) => a - b;
// app.js - 导入模块
const math = require('./math.js');
const { add, multiply } = require('./math.js');
// 解构导入
console.log(math.add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
注意 require 是同步的,而且 Node.js 会缓存模块。第一次 require 会执行整个文件,后面再 require 同一个文件直接返回缓存。这个特性有时候会让你踩坑——比如你想动态重新加载配置,结果发现改了文件没生效。
npm:包管理器的江湖地位
说实话,npm 生态是 Node.js 最大的杀手锏。你想干啥几乎都能找到现成的包,有时候我都觉得 npm 上的包是不是太多了(毕竟有 left-pad 这种几行代码也发包的)。但不得不承认,这种"拿来主义"让开发效率起飞。
// package.json - 项目的身份证
{
"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"
几个小建议:
- 锁定版本:别用
*或者latest,哪天作者发个 break change 你就哭了。package-lock.json一定要提交到 git。 - 区分 dependencies 和 devDependencies:生产环境不需要测试工具,减小部署体积。
- 定期审计:
npm audit能帮你发现漏洞,npm outdated看看哪些包该升级了。
全局对象和浏览器不一样
前端有 window,Node.js 有 global。但有些 API 两边都有,比如 console、setTimeout,有些只有 Node.js 有:
// __dirname - 当前文件所在目录的绝对路径
console.log(__dirname); // /Users/xxx/project/src
// __filename - 当前文件的绝对路径
console.log(__filename); // /Users/xxx/project/src/app.js
// process - 进程信息,超级常用
console.log(process.env.NODE_ENV); // 环境变量
console.log(process.argv); // 命令行参数
// Buffer - 处理二进制数据
const buf = Buffer.from('hello world', 'utf8');
console.log(buf); // <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>
避坑提醒:别在 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();
// 中间件 1:日志记录
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method}${req.url}`);
next(); // 必须调用 next,否则请求卡在这里
});
// 中间件 2:解析 JSON body
app.use(express.json());
// 中间件 3:自定义错误处理(要放在最后)
app.use((err, req, res, next) => {
console.error('出大事了:', err.stack);
res.status(500).json({ error: '服务器抽风了' });
});
// 路由
app.get('/users', (req, res) => {
// req.query 获取查询参数 ?page=1&limit=10
const { page = 1, limit = 10 } = req.query;
res.json({
users: [{ id: 1, name: '张三' }],
page: parseInt(page),
limit: parseInt(limit)
});
});
app.(, {
{ name, email } = req.;
(!name || !email) {
res.().({ : });
}
newUser = { : .(), name, email };
res.().(newUser);
});
app.(, {
userId = req..;
res.({ : userId, : + userId });
});
app.(, {
.();
});
Express 的坑:
- 回调地狱:虽然可以用 async/await,但错误处理很蛋疼。如果你在一个 async 路由里抛错,不处理的话进程会直接崩。
// 错误示范:这样写会崩!
app.get('/bad', async (req, res) => {
const data = await someAsyncOperation();
// 如果这里报错,进程崩溃
res.json(data);
});
// 正确姿势:包个 try-catch,或者用 express-async-errors 包
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();
// 中间件 1:日志
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`);
});
// 中间件 2:错误处理(Koa 的错误处理比较优雅)
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = { error: err.message };
// 触发应用级错误事件
app.(, err, ctx);
}
});
app.(());
router.(, (ctx) => {
ctx. = { : [] };
});
router.(, (ctx) => {
{ name } = ctx..;
(!name) {
ctx.(, );
}
ctx. = ;
ctx. = { : .(), name };
});
app.(router.());
app.();
洋葱模型是啥意思?就是中间件的执行顺序像剥洋葱:先执行第一个中间件的前半部分,然后进入第二个,再进入第三个…到底后再一层层返回。
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';
});
// 输出顺序:
// 1-开始
// 2-开始
// 3-响应
// 2-结束
// 1-结束
这个特性特别适合做统计耗时、设置响应头这类需要在请求前后都执行的操作。
Koa 的坑:
- 生态比 Express 小,很多功能要装第三方中间件(比如路由得装
@koa/router)。 - 对 ES6+ 语法依赖强,老 Node 版本可能跑不了。
Fastify:性能党的最爱
如果你在乎性能,选 Fastify。它用了 JSON Schema 做序列化,路由查找更快,而且天生支持异步。benchmarks 里经常能看到它比 Express 快 2-3 倍。
const fastify = require('fastify')({ logger: true }); // 内置日志,省事儿
// 声明路由和 schema(Fastify 推荐用 schema 验证)
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: , : }
}
}
}
}, (request, reply) => {
{ name, email } = request.;
newUser = { : .(), name, email };
reply.();
newUser;
});
fastify.( {
fastify..(error);
reply.().({ : });
});
= () => {
{
fastify.();
} (err) {
fastify..(err);
process.();
}
};
();
Fastify 的坑:
- 生态相对小,有些特定功能可能找不到插件。
- schema 验证虽然爽,但写起来有点啰嗦。
我的建议
- 新手/快速原型:Express,文档多,stackoverflow 上答案多。
- 追求代码优雅:Koa,async/await 写起来舒服,错误处理好。
- 性能敏感/微服务:Fastify,快是真的快。
JS 写后端:爽点和痛点都很真实
爽在哪?
1. 语言统一,心智负担小
前端写 TypeScript,后端写 Java,你得在两种语法风格之间来回切换,很容易精神分裂。全栈 JS 的话,类型定义、工具函数、甚至验证逻辑都可以前后端共享。
// shared/validation.js - 前后端共用
exports.validateEmail = (email) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};
exports.validatePassword = (password) => {
// 至少 8 位,包含大小写和数字
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(password);
};
2. async/await 让异步代码能看了
早期的 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);
}
}
3. npm 生态太丰富了
你想做啥都有现成的包:
- 数据库:mongoose(MongoDB)、sequelize(SQL)、prisma(新一代 ORM)
- 认证:passport.js、jsonwebtoken
- 验证:joi、yup、zod
- 测试:jest、mocha、supertest
- 部署:pm2、dockerode
痛在哪?
1. CPU 密集型任务直接拉胯
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);
// 如果 n=40,服务器卡好几秒
res.json({ result });
});
解决方案:
- 用Worker Threads(Node.js 10.5+引入的真正多线程)
- 把计算任务扔到其他服务(比如 Python 服务、云函数)
- 用 C++ 写 Addon(门槛高,但性能最好)
// 用 Worker Threads 的正确姿势
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);
// 创建 Worker 线程
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 {
// Worker 线程
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
const result = fib(workerData);
parentPort.(result);
}
2. 内存泄漏排查像捉鬼
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);
});
// 改进:用 LRU 缓存,限制大小
const LRU = require('lru-cache');
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 5 }); // 最多 500 条,5 分钟过期
排查工具:
node --inspect+ Chrome DevToolsheapdump包生成堆快照clinic.js一站式诊断
3. 单线程崩溃即全场结束
如果某个请求抛了未捕获的异常,整个 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 版)
// src/app.js
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()); // 设置各种 HTTP 头防止攻击
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true
}));
// 性能优化
app.use(compression()); // gzip 压缩响应
// 解析请求体
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 请求日志
app.use(() => {
logger.();
();
});
app.(, {
res.({
: ,
: ().(),
: process.()
});
});
app.(, routes);
app.( {
res.().({ : });
});
app.(errorHandler);
. = app;
// src/middlewares/errorHandler.js
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 为例)
// src/config/database.js
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);
}
};
// 优雅关闭
= () => {
mongoose..();
logger.();
};
. = { connectDB, disconnectDB };
// src/models/User.js
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:
},
: ,
:
}, {
: ,
: { : },
: { : }
});
userSchema.().( () {
{
: .,
: .,
: .,
: .,
: .
};
});
userSchema.(, () {
(!.()) ();
{
salt = bcrypt.();
. = bcrypt.(., salt);
();
} (error) {
(error);
}
});
userSchema.. = () {
bcrypt.(candidatePassword, .);
};
userSchema.. = () {
.({ email }).();
};
. = mongoose.(, userSchema);
3. JWT 认证(别乱用,有讲究)
// src/middlewares/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// 生成 Token
const generateToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
issuer: 'my-api',
audience: 'my-client'
}
);
};
// 验证 Token 中间件
const authenticate = async (req, res, next) => {
try {
// 从 Header 获取 token
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供认证令牌' });
}
const token = authHeader.substring(7);
// 验证 token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
user = .(decoded.);
(!user) {
res.().({ : });
}
req. = user;
();
} (error) {
(error. === ) {
res.().({ : });
}
(error. === ) {
res.().({ : });
}
(error);
}
};
= () => {
{
(!roles.(req..)) {
res.().({ : });
}
();
};
};
. = { generateToken, authenticate, authorize };
// src/controllers/userController.js
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 });
// 生成 token
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;
// 查询用户(带上密码字段)
user = .(email);
(!user) {
res.().({ : });
}
isValid = user.(password);
(!isValid) {
res.().({ : });
}
user. = ();
user.();
token = (user.);
res.({
: ,
token,
: user.
});
} (error) {
(error);
}
};
. = (req, res) => {
res.({ : req.. });
};
4. 文件上传(别信那些 buffer 拼接的教程)
网上很多教程教你用 req.on('data', chunk => data.push(chunk)) 这种方式处理文件上传,这在生产环境就是找死。大文件会撑爆内存,而且代码复杂。用 multer 或者 formidable 才是正道。
// src/controllers/uploadController.js
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 = ``;
(, uniqueName);
}
});
= () => {
allowedTypes = [, , , ];
(allowedTypes.(file.)) {
(, );
} {
( (), );
}
};
upload = ({
storage,
fileFilter,
: {
: * * ,
:
}
});
. = {
{
uploadMiddleware = upload.(fieldName);
(req, res, {
(err multer.) {
(err. === ) {
res.().({ : });
}
res.().({ : err. });
} (err) {
res.().({ : err. });
}
();
});
};
};
. = (req, res, next) => {
{
(!req.) {
res.().({ : });
}
fileUrl = ;
res.({
: ,
: {
: req..,
: req..,
: req..,
: req..,
: fileUrl
}
});
} (error) {
(error);
}
};
. = {
};
5. 日志记录(别只会 console.log)
生产环境用 console.log 就是灾难,没法分级、没法持久化、没法追踪。用 winston 或者 pino。
// src/utils/logger.js
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..({
: path.(__dirname, ),
: ,
: ,
:
}),
winston..({
: path.(__dirname, ),
: ,
:
})
]
});
(process.. !== ) {
logger.( winston..({ : consoleFormat }));
}
logger..( winston..({
: path.(__dirname, )
}));
. = logger;
6. WebSocket 实时通信
聊天室、实时通知、股票行情这些场景得用 WebSocket。socket.io 是首选,支持自动重连、房间管理、命名空间等高级功能。
// src/app.js(扩展支持 WebSocket)
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']
}
});
// 中间件:验证 JWT
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;
();
} (err) {
( ( + err.));
}
});
io.(, {
logger.();
socket.();
socket.();
socket.().(, {
: socket.,
: socket.,
: ()
});
socket.(, (data) => {
{
{ content, room = } = data;
.({
: socket.,
content,
room,
: ()
}).( logger.(, err));
io.(room).(, {
: .(),
: { : socket., : socket. },
content,
: ()
});
} (error) {
socket.(, { : });
}
});
socket.(, (data) => {
{ toUserId, content } = data;
io.().(, {
: { : socket., : socket. },
content,
: ()
});
socket.(, {
: toUserId,
content,
: ()
});
});
socket.(, {
logger.();
socket.().(, {
: socket.,
: socket.
});
});
});
. = server;
7. 入口文件和启动配置
// src/index.js
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();
// 再启动 HTTP 服务
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.();
});
( {
logger.();
process.();
}, );
};
process.(, ());
process.(, ());
} (error) {
logger.(, error);
process.();
}
};
();
# .env.example(提交到 git,不含真实密钥)
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. 先看进程还在不在
# 查看 Node 进程
ps aux | grep node
# 用 PM2 的话
pm2 list
pm2 logs
pm2 monit # 实时查看 CPU 和内存
如果进程没了,看日志找崩溃原因:
# 查看最后 100 行错误日志
tail -n 100 logs/error.log
# 查看 PM2 错误日志
pm2 logs --err
2. 内存泄漏排查
如果内存一直涨不释放,大概率是泄漏。
# 生成堆快照(需要在代码里加 heapdump)
kill -USR2 <pid>
# 会在当前目录生成 heapdump-xxx.heapsnapshot 文件
然后用 Chrome DevTools 分析:
- 打开 Chrome -> DevTools -> Memory
- Load 刚才的快照文件
- 看"Retained Size"最大的对象,找到泄漏源
或者用 clinic.js 做全方位诊断:
npm install -g clinic
clinic doctor -- node src/index.js # 会自动生成报告,指出 CPU、内存、事件循环延迟问题
3. 性能瓶颈分析
接口慢?用 0x 生成火焰图:
npm install -g 0x
0x node src/index.js # 访问 http://localhost:3000 压测一下
# Ctrl+C 后会在目录生成火焰图 HTML,看哪个函数占用最多 CPU
4. 关键救命代码
// 在 app.js 顶部加上,捕获所有未处理的错误
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`
});
// 如果内存超过阈值,主动重启(配合 PM2)
if (usage.heapUsed > 1024 * 1024 * 1024) { // 1GB
logger.error('内存使用过高,准备重启');
process.();
}
}, );
前端老铁写后端的进阶实践
1. 统一代码风格
前后端都用 ESLint + Prettier,配置保持一致:
// .eslintrc.js(前后端通用)
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 全栈
别犹豫,上 TS!类型安全能避免很多低级错误:
// src/types/index.ts
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;
};
}
// src/controllers/userController.ts
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"]
# docker-compose.yml
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 根据注释生成文档:
/**
* @swagger
* /api/users:
* get:
* summary: 获取用户列表
* tags: [Users]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* responses:
* 200:
* description: 成功
* content:
* application/json:
* schema:
* type: object
* properties:
* users:
* type: array
*/
app.get('/api/users', userController.getUsers);
5. 环境变量管理
别到处写 process.env.XXX,集中管理:
// src/config/index.js
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 做参数验证,别让脏数据进数据库:
// src/middlewares/validator.js
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({
: .().().(),
: .().().(),
: .().().().()
}),
: .({
: .().().()
})
});
router.(, (userSchema), userController.);
7. 优雅关机信号处理
前面代码里提过,但值得再强调。Docker 部署、PM2 重启时都会发 SIGTERM 信号,这时候得把正在处理的请求完成再退出,否则用户会收到 502 错误。
// 在 server.listen 回调里加
const gracefulShutdown = (signal) => {
console.log(`收到${signal},开始优雅关机...`);
server.close(() => {
console.log('HTTP 服务器已关闭');
// 关闭数据库连接等...
process.exit(0);
});
// 强制退出保险
setTimeout(() => {
console.error('强制退出');
process.exit(1);
}, 30000); // 30 秒超时
};
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,去征服服务器的星辰大海。🚀


