前置配置
ES6 模块化配置
Node.js 从 v12.17.0 开始支持原生 ES 模块(ESM)。以下是两种主要的实现方案:
1. 原生 ESM 方案(推荐)
json
// package.json
{
"type": "module"
}使用该方案后:
- 可以使用
import/export语法 - 文件扩展名使用
.mjs或.js - 可以使用
import.meta.url等新特性 - 无需额外配置和依赖
2. Babel 方案
json
// package.json
{
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"@babel/cli": "^7.23.0",
"@babel/core": "^7.23.0",
"@babel/preset-env": "^7.22.20"
}
}需要创建 .babelrc 配置:
json
{
"presets": ["@babel/preset-env"]
}日志管理配置
morgan 日志中间件
morgan 提供多种预定义的日志格式:
tiny: 最小输出dev: 开发环境友好的彩色输出combined: Apache 标准格式common: Apache 通用格式short: 较短的输出
js
import express from 'express';
import morgan from 'morgan';
const app = express();
// 开发环境使用
app.use(morgan('dev'));
// 生产环境使用
if (process.env.NODE_ENV === 'production') {
// 将日志写入文件
const accessLogStream = fs.createWriteStream(
path.join(__dirname, 'access.log'),
{ flags: 'a' }
);
app.use(morgan('combined', { stream: accessLogStream }));
}winston 日志系统
winston 支持多种日志级别和传输方式,建议按环境配置:
js
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: { service: 'api-service' },
transports: [
// 开发环境: 控制台彩色输出
process.env.NODE_ENV !== 'production'
? new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
: null,
// 生产环境: 文件输出
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880,
maxFiles: 5,
})
].filter(Boolean)
});
export default logger;路由与分层架构
现代后端采用三层架构模式,在 Express 项目中对应如下:
- 表现层/路由层(Routes + Controllers)
- 位置:
routes/和controllers/目录 - 职责:
- 处理 HTTP 请求和响应
- 参数验证和清洗
- 权限控制
- 调用业务层服务
- 格式化返回结果
- 业务逻辑层(Service)
- 位置:
services/目录 - 职责:
- 实现具体的业务逻辑
- 组合和编排多个领域对象
- 处理事务(特别是涉及多表操作时)
- 不直接处理 HTTP 相关的内容
- 可被多个控制器复用
- 数据访问层(Models)
- 位置:
models/目录 - 职责:
- 定义数据模型和结构
- 封装数据库操作
- 提供数据的 CRUD 接口
- 处理数据的持久化
- 确保数据的一致性
示例结构:
project/
├── routes/ # 路由定义
├── controllers/ # 请求处理和响应
├── services/ # 业务逻辑
├── models/ # 数据模型和访问
└── middlewares/ # 中间件(用于横切关注点)最佳实践:
- 保持单一职责原则,每一层只负责自己的任务
- 层与层之间通过接口通信,避免跨层调用
- 依赖方向:Controller -> Service -> Model
- 使用依赖注入来降低耦合度
响应处理
统一响应格式
建议创建统一的响应工具类:
js
// utils/responseHandler.js
export class ResponseHandler {
static success(res, data = null, message = "success") {
return res.json({
code: 200,
message,
data
});
}
static error(res, message = "error", code = 400, data = null) {
return res.status(code).json({
code,
message,
data
});
}
}使用示例:
js
import { ResponseHandler } from '../utils/responseHandler.js';
// 成功响应
ResponseHandler.success(res, userData, "获取用户信息成功");
// 错误响应
ResponseHandler.error(res, "用户未找到", 404);响应方法说明
- res.json() vs res.send()
res.json(): 专用于返回 JSON 数据- 自动设置
Content-Type: application/json - 自动调用
JSON.stringify() - 推荐用于 API 接口响应
- 自动设置
res.send(): 通用响应方法- 支持多种数据类型(字符串、Buffer、对象等)
- 自动识别 Content-Type
- 适合返回非 JSON 数据
- 其他常用响应方法
res.sendFile(): 发送文件res.render(): 渲染模板res.redirect(): URL 重定向res.download(): 触发文件下载
Express + Node.js 全栈项目目录示例
目录结构
my-app/
├── app.js
├── bin/
│ └── www
├── config/
│ ├── db.js
│ └── env.config.js
├── controllers/
│ ├── authController.js
│ └── userController.js
├── models/
│ ├── User.js
│ └── Auth.js
├── routes/
│ ├── authRoutes.js
│ └── userRoutes.js
├── services/
│ ├── authService.js
│ └── userService.js
├── middlewares/
│ ├── authMiddleware.js
│ └── errorMiddleware.js
├── public/
│ ├── css/
│ │ └── main.css
│ ├── js/
│ │ └── main.js
│ └── images/
│ └── logo.png
├── views/
│ ├── layouts/
│ │ └── main.handlebars
│ ├── partials/
│ │ └── header.handlebars
│ └── pages/
│ └── home.handlebars
├── utils/
│ ├── logger.js
│ └── validators.js
├── package.json
├── package-lock.json
└── README.md目录说明
- app.js:应用程序的入口文件,初始化 Express 应用并设置中间件、路由等。
- bin/www:启动脚本,用于启动应用。
- config/:配置文件,如数据库连接、环境变量等。
- controllers/:控制器文件,处理业务逻辑。
- models/:模型文件,定义数据库模型。
- routes/:路由文件,定义路由和控制器的映射。
- middlewares/:中间件文件,处理请求和响应的预处理和后处理。
- public/:静态文件目录,存放 CSS、JavaScript、图片等静态资源。
- views/:视图文件目录,存放模板文件。
- utils/:工具文件,存放通用的工具函数和辅助函数。
- package.json:项目依赖和脚本配置文件。
- package-lock.json:锁定依赖版本的文件。
- README.md:项目文档文件。
代码示例
- app.js
js
const express = require('express');
const path = require('path');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const cors = require('cors');
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes');
const app = express();
// Middleware
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(cors());
app.use(express.static(path.join(__dirname, 'public')));
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
// Error handling middleware
app.use(require('./middlewares/errorMiddleware'));
module.exports = app;- bin/www
js
#!/usr/bin/env node
/**
* Module dependencies.
*/
const app = require('../app');
const debug = require('debug')('my-app:server');
const http = require('http');
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
const port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
const addr = server.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}- config/
db.js
js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB connected');
} catch (err) {
console.error(err.message);
process.exit(1);
}
};
module.exports = connectDB;env.config.js
js
require('dotenv').config();
module.exports = {
MONGO_URI: process.env.MONGO_URI,
JWT_SECRET: process.env.JWT_SECRET,
PORT: process.env.PORT || 3000,
};- controllers/authController.js:控制器文件,处理业务逻辑
js
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.register = async (req, res) => {
const { name, email, password } = req.body;
try {
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: 'User already exists' });
}
user = new User({
name,
email,
password,
});
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
await user.save();
const payload = {
user: {
id: user.id,
},
};
jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '1h' },
(err, token) => {
if (err) throw err;
res.json({ token });
}
);
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
};- models/User.js: 模型文件,定义数据库模型。
js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
date: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('User', UserSchema);- routes/authRoutes.js: 路由文件,定义路由和控制器的映射。
js
const express = require('express');
const router = express.Router();
const { register, login } = require('../controllers/authController');
const { protect } = require('../middlewares/authMiddleware');
router.post('/register', register);
router.post('/login', login);
module.exports = router;- middlewares/authMiddleware.js: 中间件文件,处理请求和响应的预处理和后处理。
js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const protect = async (req, res, next) => {
const token = req.header('x-auth-token');
if (!token) {
return res.status(401).json({ msg: 'No token, authorization denied' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.user.id).select('-password');
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
};
module.exports = { protect };- views/: 视图文件目录,存放模板文件。
layouts/main.handlebars
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
{{{body}}}
<script src="/js/main.js"></script>
</body>
</html>pages/home.handlebars
html
{{> partials/header }}
<main>
<h1>Welcome to My App</h1>
<p>This is the home page.</p>
</main>
{{> partials/footer }}