Node.js 编码规约
前言
Node.js 规约主要包含编码风格、安全规约、最佳实践等几个部分,目的是给业务同学提供研发过程中的实质性规范和指导。其中编码风格 follow eslint-config-egg。
版本兼容性
本规范适用于 Node.js >= 14.0.0 版本,推荐使用 LTS 版本。
核心原则
- 安全第一:确保应用安全,避免常见安全漏洞
- 性能优先:关注应用性能,避免阻塞操作
- 可维护性:编写清晰、可读、可维护的代码
- 错误处理:完善的错误处理和日志记录
- 测试覆盖:保证代码质量和稳定性
1 编码风格
- 1.1【推荐】使用 Node.js 内置的全局变量。eslint: node/prefer-global
// 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');- 1.2【推荐】使用模块内支持的
promisesAPI。eslint: node/prefer-promises
Node.js 从 v11.14.0 开始支持 require('dns').promises 和 require('fs').promises API。
// 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 包、本地文件或其他,几类文件代码块之间各空一行,每类文件代码块中的引用顺序按照字典排序,如有解构引用情况,字典序以解构的第一个为准,解构内部按照字典排序。
// 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
// 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。
// 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+语法特性,保持代码现代化。
// 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【推荐】使用模板字符串而不是字符串拼接。
// 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【推荐】使用解构赋值简化代码。
// 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【推荐】使用对象简写。
// 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。
// 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【推荐】对接口入参严格校验。
使用 jsonschema 或 joi 校验入参,减少意外输入造成的程序报错或崩溃,同时也能减少脏数据形成。
// 使用 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 注入攻击。
// 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【强制】安全处理文件上传。
限制文件类型、大小,扫描恶意内容,使用安全的文件存储位置。
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 和安全头。
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。
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方便链式调用。
class Jedi {
jump() {
this.jumping = true;
return this;
}
setHeight(height) {
this.height = height;
return this;
}
}
const luke = new Jedi();
luke.jump()
.setHeight(20);- 3.8【推荐】使用 阿里云 Node.js 性能平台 作为应用的性能监控工具。
阿里云 Node.js 性能平台提供 Node.js 应用性能监控、管理及报警,性能快照远程截取与调优, 安全与依赖更新提示,异常日志与慢 HTTP 日志等功能,能有效帮助开发者监控和排查 Node.js 应用性能问题。
- 3.9【推荐】实现统一的错误处理机制。
// 错误处理中间件
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【推荐】使用环境变量管理配置。
// 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【推荐】实现优雅关闭。
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【推荐】使用日志管理工具。
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【推荐】使用缓存减少数据库查询。
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【推荐】使用连接池。
// 数据库连接池
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【推荐】使用流处理大文件。
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【推荐】编写单元测试。
// 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【推荐】编写集成测试。
// 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。
// 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 编译选项。
// 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【推荐】模块化和可复用性。
// 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
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"]# 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【推荐】配置健康检查。
// 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 监控。
// 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【推荐】结构化日志。
// 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。
// .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"
}
}// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid"
}配套工具
- eslint-config-ali:本规约配套的 ESLint 规则包,可使用
eslint-config-ali/node或eslint-config-ali/typescript/node引入本文介绍的规则 - husky:Git hooks 管理工具
- lint-staged:暂存文件检查工具
- commitizen:规范化 commit message
- standard-version:自动化版本管理和 CHANGELOG 生成
