Skip to content

Node.js 编码规约

前言

Node.js 规约主要包含编码风格、安全规约、最佳实践等几个部分,目的是给业务同学提供研发过程中的实质性规范和指导。其中编码风格 follow eslint-config-egg

版本兼容性

本规范适用于 Node.js >= 14.0.0 版本,推荐使用 LTS 版本。

核心原则

  1. 安全第一:确保应用安全,避免常见安全漏洞
  2. 性能优先:关注应用性能,避免阻塞操作
  3. 可维护性:编写清晰、可读、可维护的代码
  4. 错误处理:完善的错误处理和日志记录
  5. 测试覆盖:保证代码质量和稳定性

1 编码风格

javascript
// bad
const { Buffer } = require('buffer');
const b = Buffer.alloc(16);
// good
const b = Buffer.alloc(16);

// bad
const { URL } = require('url');
const u = new URL(s);
// good
const u = new URL(s);

// bad
const { URLSearchParams } = require('url');
const u = new URLSearchParams(s);
// good
const u = new URLSearchParams(s);

// bad
const { TextEncoder } = require('util');
const u = new TextEncoder(s);
// good
const u = new TextEncoder(s);

// bad
const { TextDecoder } = require('util');
const u = new TextDecoder(s);
// good
const u = new TextDecoder(s);

// bad
const process = require('process');
process.exit(0);
// good
process.exit(0);

// bad
const console = require('console');
console.log('hello');
// good
console.log('hello');

Node.js 从 v11.14.0 开始支持 require('dns').promisesrequire('fs').promises API。

javascript
// bad
const dns = require('dns');
const fs = require('fs');

function lookup(hostname) {
  dns.lookup(hostname, (error, address, family) => {
    // ...
  });
}

function readData(filePath) {
  fs.readFile(filePath, 'utf8', (error, content) => {
    // ...
  });
}

// good
const { promises: dns } = require('dns');
const { promises: fs } = require('fs');

async function lookup(hostname) {
  const { address, family } = await dns.lookup(hostname);
  // ...
}

async function readData(filePath) {
  const content = await fs.readFile(filePath, 'utf8');
  // ...
}
  • 1.3【推荐】如无特殊需求,模块引用声明放在文件顶端,注意引用顺序。eslint: import/order

如无特殊需求(如动态 require),模块引用声明需要放在文件顶端。引用顺序如无特殊需求,按以下顺序来引入依赖:node 内置模块、npm 包、本地文件或其他,几类文件代码块之间各空一行,每类文件代码块中的引用顺序按照字典排序,如有解构引用情况,字典序以解构的第一个为准,解构内部按照字典排序。

javascript
// bad
const Car = require('./models/car');
const moment = require('moment');
const mongoose = require('mongoose');
const fs = require('fs');
const http = require('http');
const { Foo, Bar } = require('tool');
const note = require('note');

// good
const fs = require('fs');
const http = require('http');

const { Bar, Foo } = require('tool');
const moment = require('moment');
const mongoose = require('mongoose');
const note = require('note');

const Car = require('./models/car');

// bad
import Car from './models/car';
import moment from 'moment';
import mongoose from 'mongoose';
import fs from 'fs';
import http from 'http';
import { Foo, Bar } from 'tool';
import note from 'note';

// good
import fs from 'fs';
import http from 'http';

import { Bar, Foo } from 'tool';
import moment from 'moment';
import mongoose from 'mongoose';
import note from 'note';

import Car from './models/car';
  • 1.4【推荐】抛出异常时,使用原生 Error 对象。eslint: no-throw-literal
javascript
// bad
throw 'error';

throw 0;

throw undefined;

throw null;

const err = new Error();
throw 'an ' + err;

const err = new Error();
throw `${err}`

// good
throw new Error();

throw new Error('error');

const err = new Error('error');
throw err;

try {
  throw new Error('error');
} catch (err) {
  throw err;
}
  • 1.5【推荐】线上环境尽量不要使用 fs/child_process 模块的 sync 方法,如 fs.readFileSync()cp.execSync() 等。

这样会阻塞 Node.js 应用的进程,导致不能继续处理新的请求,或当前正在处理的请求超时。推荐使用 require('fs').promises 方式或使用 mz

javascript
// bad
const fs = require('fs');

function test() {
  fs.readFileSync('./somefile', 'utf-8');
}

// good
const { promises: fs } = require('fs');

async function test() {
  await fs.readFile('./somefile', 'utf-8');
}

// good
const fs = require('mz/fs');

async function test() {
  await fs.readFile('./somefile', 'utf-8');
}
  • 1.6【推荐】使用 ES6+ 语法特性,保持代码现代化。
javascript
// bad
var name = 'john';
var items = [1, 2, 3];
var doubled = items.map(function(item) {
  return item * 2;
});

// good
const name = 'john';
const items = [1, 2, 3];
const doubled = items.map(item => item * 2);

// bad - 使用 for 循环
for (var i = 0; i < items.length; i++) {
  console.log(items[i]);
}

// good - 使用 for...of 或数组方法
for (const item of items) {
  console.log(item);
}

// or
items.forEach(item => console.log(item));
  • 1.7【推荐】使用模板字符串而不是字符串拼接。
javascript
// bad
const greeting = 'Hello, ' + name + '! You have ' + count + ' messages.';

// good
const greeting = `Hello, ${name}! You have ${count} messages.`;

// bad - 多行字符串
const multiLine = 'Line 1
' +
  'Line 2
' +
  'Line 3';

// good
const multiLine = `Line 1
Line 2
Line 3`;
  • 1.8【推荐】使用解构赋值简化代码。
javascript
// bad
function getUserInfo(user) {
  const name = user.name;
  const age = user.age;
  const email = user.email;
  return { name, age, email };
}

// good
function getUserInfo({ name, age, email }) {
  return { name, age, email };
}

// bad - 数组赋值
const first = arr[0];
const second = arr[1];

// good
const [first, second] = arr;
  • 1.9【推荐】使用对象简写。
javascript
// bad
const obj = {
  name: name,
  age: age,
  greet: function() {
    return `Hello, ${this.name}`;
  }
};

// good
const obj = {
  name,
  age,
  greet() {
    return `Hello, ${this.name}`;
  }
};
  • 1.10【推荐】避免使用 var,优先使用 const,需要重新赋值时使用 let
javascript
// bad
var name = 'john';
var count = 0;

// good
const name = 'john';
let count = 0;

// bad - 函数内声明
function foo() {
  if (true) {
    var bar = 1;
  }
  console.log(bar); // 1
}

// good - 块级作用域
function foo() {
  if (true) {
    const bar = 1;
  }
  // console.log(bar); // ReferenceError
}

2 安全规约

  • 2.1【强制】在客户端隐藏错误详情。

错误提示有可能会暴露出敏感的系统信息,容易被利用去做进一步的攻击。

  • 2.2【强制】隐藏或伪造技术栈和框架标识。

隐藏或伪造 X-Powered-By 响应头,应用广泛的框架多有公开的漏洞,防止标识露出被恶意利用。

  • 2.3【强制】JSONP 跨域接口必须严格校验访问来源。

配置域名白名单,防止通过 JSONP 接口获取到敏感信息的风险。

  • 2.4【强制】禁止使用从参数或明文 cookie 中获取的用户标识进行敏感信息查询输出。

防止未授权访问/越权访问。

  • 2.5【强制】防止 SQL 注入。

含有用户输入内容的 SQL 语句必须使用预编译模式。若用户输入无法使用预编译模式(输入为表名/字段名等内容),需要对用户输入进行转义/过滤之后再拼接到 SQL 中。

  • 2.6【推荐】定期检查过期依赖和依赖漏洞升级。

检测依赖,对于有漏洞或者过期的依赖要及时升级或替换。

  • 2.7【推荐】用户上传文件不允许至服务器本地,需要上传到 OSS 等服务。

任意文件上传漏洞,防止用户上传恶意文件,入侵服务器。

  • 2.8【推荐】服务端 URL 重定向需要设置白名单。

若需要对用户输入内容作为目标 URL 进行重定向,需要对其进行域名白名单校验,不允许跳转至白名单外的域名。

  • 2.9【推荐】对接口入参严格校验。

使用 jsonschemajoi 校验入参,减少意外输入造成的程序报错或崩溃,同时也能减少脏数据形成。

javascript
// 使用 Joi 进行参数校验
const Joi = require('joi');

const userSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(6).pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(),
  age: Joi.number().integer().min(0).max(120)
});

app.post('/users', async (req, res) => {
  try {
    const { error, value } = userSchema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: error.details[0].message });
    }
    // 使用校验后的 value
    const user = await createUser(value);
    res.json(user);
  } catch (err) {
    next(err);
  }
});
  • 2.10【强制】防止 NoSQL 注入。

使用参数化查询和输入验证,防止 NoSQL 注入攻击。

javascript
// bad - 容易受到 NoSQL 注入
app.get('/users', async (req, res) => {
  const { username } = req.query;
  const users = await User.find({ username }); // 危险
  res.json(users);
});

// good - 使用类型检查和转义
app.get('/users', async (req, res) => {
  const { username } = req.query;
  
  // 输入验证
  if (typeof username !== 'string' || !/^[a-zA-Z0-9_]{3,30}$/.test(username)) {
    return res.status(400).json({ error: 'Invalid username' });
  }
  
  const users = await User.find({ 
    username: username.trim(),
    isActive: true
  });
  res.json(users);
});
  • 2.11【强制】安全处理文件上传。

限制文件类型、大小,扫描恶意内容,使用安全的文件存储位置。

javascript
const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
  destination: '/tmp/uploads', // 临时目录,不是可执行目录
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    const filename = `${Date.now()}${ext}`;
    cb(null, filename);
  }
});

const fileFilter = (req, file, cb) => {
  const allowedTypes = /jpeg|jpg|png|gif|pdf|doc|docx/;
  const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
  const mimetype = allowedTypes.test(file.mimetype);
  
  if (mimetype && extname) {
    return cb(null, true);
  } else {
    cb(new Error('Invalid file type'));
  }
};

const upload = multer({
  storage,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB 限制
  fileFilter
});

app.post('/upload', upload.single('file'), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded' });
  }
  
  try {
    // 扫描文件内容(使用 ClamAV 等)
    // 上传到安全的云存储(OSS、S3)
    const secureUrl = await uploadToCloud(req.file.path);
    
    // 删除临时文件
    await fs.unlink(req.file.path);
    
    res.json({ url: secureUrl });
  } catch (error) {
    res.status(500).json({ error: 'Upload failed' });
  }
});
  • 2.12【推荐】使用 HTTPS 和安全头。
javascript
const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

// 设置安全相关响应头
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});

3 最佳实践

  • 3.1【推荐】应用不应该有状态。

使用外部数据存储。保证即使结束某个应用实例也不会影响数据和服务。

  • 3.2【推荐】尽量不要用 Node.js 应用去托管前端静态文件。

应该把前端静态文件放到 CDN,当静态文件的访问量很大的时候,可能会阻塞其他服务的执行。

  • 3.3【推荐】把 CPU 密集型任务委托给反向代理。

Node.js 应用不合适做 CPU 密集型任务(例如 gzip,SSL),请尽量把这类任务代理给 nginx 或其他服务。

  • 3.4【推荐】使用 async/await,尽量避免使用回调函数。

async/await 可以让你的代码看起来更简洁,可以规避掉回调地狱的问题,并且使异常处理也变得清晰简单。

  • 3.5【推荐】使用 util.promisify 处理回调函数,使其返回 Promise
javascript
const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);

async function callStat() {
  const stats = await stat('.');
  console.log(`This directory is owned by ${stats.uid}`);
}
  • 3.6【推荐】使用 Node.js 原生 Promise,而不是三方库如 bluebird

  • 3.7【推荐】在类方法中返回 this 方便链式调用。

javascript
class Jedi {
  jump() {
    this.jumping = true;
    return this;
  }

  setHeight(height) {
    this.height = height;
    return this;
  }
}

const luke = new Jedi();

luke.jump()
  .setHeight(20);

阿里云 Node.js 性能平台提供 Node.js 应用性能监控、管理及报警,性能快照远程截取与调优, 安全与依赖更新提示,异常日志与慢 HTTP 日志等功能,能有效帮助开发者监控和排查 Node.js 应用性能问题。

  • 3.9【推荐】实现统一的错误处理机制。
javascript
// 错误处理中间件
function errorHandler(err, req, res, next) {
  // 记录错误日志
  console.error('Error:', err);
  
  // 根据环境返回不同的错误信息
  const isDev = process.env.NODE_ENV === 'development';
  
  res.status(err.status || 500).json({
    message: err.message || 'Internal Server Error',
    ...(isDev && { stack: err.stack })
  });
}

// 异步错误处理包装器
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// 使用示例
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    const error = new Error('User not found');
    error.status = 404;
    throw error;
  }
  res.json(user);
}));

app.use(errorHandler);
  • 3.10【推荐】使用环境变量管理配置。
javascript
// config/config.js
require('dotenv').config();

const config = {
  port: process.env.PORT || 3000,
  database: {
    host: process.env.DB_HOST || 'localhost',
    port: process.env.DB_PORT || 5432,
    name: process.env.DB_NAME,
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD
  },
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: process.env.REDIS_PORT || 6379,
    password: process.env.REDIS_PASSWORD
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '24h'
  }
};

// 验证必需的环境变量
const requiredEnvVars = ['DB_NAME', 'DB_USERNAME', 'DB_PASSWORD', 'JWT_SECRET'];
const missingEnvVars = requiredEnvVars.filter(varName => !process.env[varName]);

if (missingEnvVars.length > 0) {
  throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
}

module.exports = config;
  • 3.11【推荐】实现优雅关闭。
javascript
const server = require('http').createServer(app);

const gracefulShutdown = (signal) => {
  console.log(`Received ${signal}, starting graceful shutdown`);
  
  server.close(() => {
    console.log('HTTP server closed');
    
    // 关闭数据库连接
    mongoose.connection.close(() => {
      console.log('MongoDB connection closed');
      process.exit(0);
    });
  });
  
  // 强制关闭超时
  setTimeout(() => {
    console.error('Could not close connections in time, forcefully shutting down');
    process.exit(1);
  }, 10000);
};

// 监听关闭信号
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

// 处理未捕获的异常
process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  process.exit(1);
});
  • 3.12【推荐】使用日志管理工具。
javascript
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'user-service' },
  transports: [
    new winston.transports.File({ 
      filename: 'logs/error.log', 
      level: 'error' 
    }),
    new winston.transports.File({ 
      filename: 'logs/combined.log' 
    })
  ]
});

// 开发环境添加控制台输出
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

// 使用日志中间件
app.use((req, res, next) => {
  logger.info(`${req.method} ${req.url}`, {
    ip: req.ip,
    userAgent: req.get('User-Agent')
  });
  next();
});

module.exports = logger;

4. 性能优化

  • 4.1【推荐】使用缓存减少数据库查询。
javascript
const Redis = require('redis');
const client = Redis.createClient();

// 缓存装饰器
function cache(ttl = 300) {
  return function(target, propertyKey, descriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args) {
      const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
      
      // 尝试从缓存获取
      const cached = await client.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }
      
      // 执行原方法
      const result = await originalMethod.apply(this, args);
      
      // 缓存结果
      await client.setex(cacheKey, ttl, JSON.stringify(result));
      
      return result;
    };
    
    return descriptor;
  };
}

// 使用示例
class UserService {
  @cache(600) // 缓存10分钟
  async getUserById(id) {
    return await User.findById(id);
  }
}
  • 4.2【推荐】使用连接池。
javascript
// 数据库连接池
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'myapp',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
  acquireTimeout: 60000,
  timeout: 60000,
  reconnect: true
});

// 使用连接池
async function query(sql, params) {
  const connection = await pool.getConnection();
  try {
    const [rows] = await connection.execute(sql, params);
    return rows;
  } finally {
    connection.release();
  }
}
  • 4.3【推荐】使用流处理大文件。
javascript
const fs = require('fs');
const { pipeline } = require('stream');
const { promisify } = require('util');

const pipelineAsync = promisify(pipeline);

app.get('/download-large-file', async (req, res) => {
  const filePath = '/path/to/large/file.pdf';
  
  try {
    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', 'attachment; filename="file.pdf"');
    
    await pipelineAsync(
      fs.createReadStream(filePath),
      res
    );
  } catch (error) {
    if (!res.headersSent) {
      res.status(500).json({ error: 'Download failed' });
    }
  }
});

5. 测试规范

  • 5.1【推荐】编写单元测试。
javascript
// user.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const UserService = require('../services/userService');

describe('UserService', () => {
  let userService;
  let userModelStub;
  
  beforeEach(() => {
    userModelStub = sinon.stub(User);
    userService = new UserService(userModelStub);
  });
  
  afterEach(() => {
    sinon.restore();
  });
  
  describe('getUserById', () => {
    it('should return user when found', async () => {
      const mockUser = { id: 1, name: 'John' };
      userModelStub.findById.resolves(mockUser);
      
      const result = await userService.getUserById(1);
      
      expect(result).to.deep.equal(mockUser);
      expect(userModelStub.findById).to.have.been.calledWith(1);
    });
    
    it('should throw error when user not found', async () => {
      userModelStub.findById.resolves(null);
      
      try {
        await userService.getUserById(999);
        expect.fail('Should have thrown error');
      } catch (error) {
        expect(error.message).to.equal('User not found');
      }
    });
  });
});
  • 5.2【推荐】编写集成测试。
javascript
// app.test.js
const request = require('supertest');
const app = require('../app');
const { expect } = require('chai');

describe('API Integration Tests', () => {
  describe('GET /api/users', () => {
    it('should return list of users', async () => {
      const response = await request(app)
        .get('/api/users')
        .expect(200);
      
      expect(response.body).to.be.an('array');
      expect(response.body).to.have.lengthOf(0); // 初始状态
    });
  });
  
  describe('POST /api/users', () => {
    it('should create new user', async () => {
      const userData = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123'
      };
      
      const response = await request(app)
        .post('/api/users')
        .send(userData)
        .expect(201);
      
      expect(response.body).to.have.property('id');
      expect(response.body.name).to.equal(userData.name);
      expect(response.body).to.not.have.property('password'); // 不返回密码
    });
  });
});

6. TypeScript 支持

  • 6.1【推荐】在 Node.js 项目中使用 TypeScript。
typescript
// types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
}

// services/userService.ts
import { User, CreateUserRequest } from '../types/user';

export class UserService {
  async createUser(userData: CreateUserRequest): Promise<User> {
    // 创建用户逻辑
    const user = await User.create(userData);
    return user;
  }
  
  async getUserById(id: number): Promise<User | null> {
    return await User.findById(id);
  }
}

// controllers/userController.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/userService';

export class UserController {
  private userService: UserService;
  
  constructor() {
    this.userService = new UserService();
  }
  
  public createUser = async (
    req: Request,
    res: Response,
    next: NextFunction
  ): Promise<void> => {
    try {
      const userData: CreateUserRequest = req.body;
      const user = await this.userService.createUser(userData);
      res.status(201).json(user);
    } catch (error) {
      next(error);
    }
  };
}
  • 6.2【推荐】配置 TypeScript 编译选项。
json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

7. 项目结构规范

  • 7.1【推荐】使用分层架构。
src/
├── controllers/          # 控制器层
│   ├── userController.ts
│   └── authController.ts
├── services/            # 业务逻辑层
│   ├── userService.ts
│   └── authService.ts
├── models/              # 数据模型层
│   ├── User.ts
│   └── index.ts
├── middleware/          # 中间件
│   ├── auth.ts
│   ├── validation.ts
│   └── errorHandler.ts
├── routes/              # 路由定义
│   ├── user.ts
│   └── index.ts
├── utils/               # 工具函数
│   ├── logger.ts
│   ├── validation.ts
│   └── crypto.ts
├── config/              # 配置文件
│   ├── database.ts
│   └── app.ts
├── types/               # TypeScript 类型定义
│   ├── user.ts
│   └── common.ts
├── tests/               # 测试文件
│   ├── unit/
│   └── integration/
├── app.ts               # 应用入口
└── server.ts            # 服务器启动
  • 7.2【推荐】模块化和可复用性。
typescript
// utils/response.ts
export interface ApiResponse<T = any> {
  success: boolean;
  data?: T;
  message?: string;
  error?: string;
}

export const createSuccessResponse = <T>(data: T, message?: string): ApiResponse<T> => ({
  success: true,
  data,
  message
});

export const createErrorResponse = (error: string): ApiResponse => ({
  success: false,
  error
});

// controllers/baseController.ts
import { Request, Response, NextFunction } from 'express';
import { ApiResponse, createSuccessResponse, createErrorResponse } from '../utils/response';

export abstract class BaseController {
  protected sendSuccess<T>(res: Response, data: T, message?: string, statusCode = 200): void {
    res.status(statusCode).json(createSuccessResponse(data, message));
  }
  
  protected sendError(res: Response, error: string, statusCode = 500): void {
    res.status(statusCode).json(createErrorResponse(error));
  }
  
  protected async handleAsync(
    fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
  ) {
    return (req: Request, res: Response, next: NextFunction) => {
      Promise.resolve(fn(req, res, next)).catch(next);
    };
  }
}

8. 部署与运维

  • 8.1【推荐】使用 Docker 容器化。
dockerfile
# Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS runtime

WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .

# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs

EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]
yaml
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - PORT=3000
    depends_on:
      - redis
      - postgres
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    restart: unless-stopped

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres_data:
  • 8.2【推荐】配置健康检查。
typescript
// routes/health.ts
import { Router } from 'express';
import { createSuccessResponse } from '../utils/response';

const router = Router();

router.get('/health', async (req, res) => {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    version: process.env.npm_package_version
  };
  
  // 检查数据库连接
  try {
    await mongoose.connection.db.admin().ping();
    health.database = 'connected';
  } catch (error) {
    health.database = 'disconnected';
    health.status = 'unhealthy';
  }
  
  // 检查 Redis 连接
  try {
    await redis.ping();
    health.redis = 'connected';
  } catch (error) {
    health.redis = 'disconnected';
    health.status = 'unhealthy';
  }
  
  const statusCode = health.status === 'healthy' ? 200 : 503;
  res.status(statusCode).json(createSuccessResponse(health));
});

export default router;

9. 监控与日志

  • 9.1【推荐】集成 APM 监控。
typescript
// monitoring/apm.ts
import * as apm from 'elastic-apm-node';

if (process.env.NODE_ENV === 'production') {
  apm.start({
    serviceName: 'my-node-app',
    serverUrl: process.env.APM_SERVER_URL,
    secretToken: process.env.APM_SECRET_TOKEN,
    environment: process.env.NODE_ENV,
    logLevel: 'info',
    captureBody: 'all',
    transactionSampleRate: 0.1
  });
}

export { apm };
  • 9.2【推荐】结构化日志。
typescript
// utils/structuredLogger.ts
import winston from 'winston';
import { TransformableInfo } from 'logform';

const customFormat = winston.format.combine(
  winston.format.timestamp(),
  winston.format.errors({ stack: true }),
  winston.format.printf((info: TransformableInfo) => {
    return JSON.stringify({
      timestamp: info.timestamp,
      level: info.level,
      message: info.message,
      service: 'user-service',
      ...info.metadata
    });
  })
);

export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: customFormat,
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'logs/app.log' })
  ]
});

// 使用示例
logger.info('User logged in', {
  userId: user.id,
  ip: req.ip,
  userAgent: req.get('User-Agent')
});

logger.error('Database connection failed', {
  error: error.message,
  stack: error.stack,
  retryCount: 3
});

10. 代码质量工具

  • 10.1【推荐】配置 ESLint 和 Prettier。
json
// .eslintrc.json
{
  "extends": [
    "@typescript-eslint/recommended",
    "plugin:node/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "security"],
  "parserOptions": {
    "ecmaVersion": 2020,
    "sourceType": "module"
  },
  "rules": {
    "security/detect-object-injection": "warn",
    "security/detect-non-literal-regexp": "error",
    "security/detect-unsafe-regex": "error",
    "security/detect-buffer-noassert": "error",
    "security/detect-child-process": "warn",
    "security/detect-disable-mustache-escape": "error",
    "security/detect-eval-with-expression": "error",
    "security/detect-no-csrf-before-method-override": "error",
    "security/detect-non-literal-fs-filename": "warn",
    "security/detect-non-literal-require": "warn",
    "security/detect-possible-timing-attacks": "warn",
    "security/detect-pseudoRandomBytes": "error"
  }
}
json
// .prettierrc
{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "arrowParens": "avoid"
}

配套工具

参考资料