Skip to content

前置配置 ​

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 项目中对应如下:

  1. 表现层/路由层(Routes + Controllers)
  • 位置:routes/ 和 controllers/ 目录
  • 职责:
    • 处理 HTTP 请求和响应
    • 参数验证和清洗
    • 权限控制
    • 调用业务层服务
    • 格式化返回结果
  1. 业务逻辑层(Service)
  • 位置:services/ 目录
  • 职责:
    • 实现具体的业务逻辑
    • 组合和编排多个领域对象
    • 处理事务(特别是涉及多表操作时)
    • 不直接处理 HTTP 相关的内容
    • 可被多个控制器复用
  1. 数据访问层(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);

响应方法说明 ​

  1. res.json() vs res.send()
  • res.json(): 专用于返回 JSON 数据

    • 自动设置 Content-Type: application/json
    • 自动调用 JSON.stringify()
    • 推荐用于 API 接口响应
  • res.send(): 通用响应方法

    • 支持多种数据类型(字符串、Buffer、对象等)
    • 自动识别 Content-Type
    • 适合返回非 JSON 数据
  1. 其他常用响应方法
  • 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

目录说明 ​

  1. app.js:应用程序的入口文件,初始化 Express 应用并设置中间件、路由等。
  2. bin/www:启动脚本,用于启动应用。
  3. config/:配置文件,如数据库连接、环境变量等。
  4. controllers/:控制器文件,处理业务逻辑。
  5. models/:模型文件,定义数据库模型。
  6. routes/:路由文件,定义路由和控制器的映射。
  7. middlewares/:中间件文件,处理请求和响应的预处理和后处理。
  8. public/:静态文件目录,存放 CSS、JavaScript、图片等静态资源。
  9. views/:视图文件目录,存放模板文件。
  10. utils/:工具文件,存放通用的工具函数和辅助函数。
  11. package.json:项目依赖和脚本配置文件。
  12. package-lock.json:锁定依赖版本的文件。
  13. README.md:项目文档文件。

代码示例 ​

  1. 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;
  1. 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);
}
  1. 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,
};
  1. 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');
  }
};
  1. 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);
  1. 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;
  1. 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 };
  1. 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 }}