
前端开发
React项目最佳实践:提升代码质量与性能的实用指南
2024年5月28日
Node.js凭借其非阻塞I/O和事件驱动架构,已经成为构建高性能Web应用和APIs的流行选择。然而,Node.js的异步特性和单线程模型也带来了特有的挑战。本文将分享一系列Node.js开发中的最佳实践,帮助你构建可靠、高效且易于维护的应用。
将应用拆分为独立的模块,每个模块负责特定的功能:
project/
├── src/
│ ├── api/ # API路由定义
│ ├── controllers/ # 请求处理逻辑
│ ├── services/ # 业务逻辑
│ ├── models/ # 数据模型
│ ├── middleware/ # 中间件
│ ├── utils/ # 工具函数
│ └── config/ # 配置文件
├── tests/ # 测试文件
├── package.json
└── README.md
将配置项存储在环境变量中,而不是硬编码在代码中:
// config.js
require('dotenv').config(); // 使用dotenv加载.env文件中的环境变量
module.exports = {
port: process.env.PORT || 3000,
databaseUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV || 'development'
};
配合.env
文件使用(记得将它添加到.gitignore
中):
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your_secret_key
NODE_ENV=development
在Express.js应用中,使用中央错误处理中间件:
// errorHandler.js
module.exports = function errorHandler(err, req, res, next) {
// 记录错误
console.error(err.stack);
// 为不同类型的错误设置适当的状态码
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
status: 'error',
statusCode,
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
// app.js
const express = require('express');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// 其他中间件和路由...
// 错误处理中间件应该是最后一个中间件
app.use(errorHandler);
定义自定义错误类,以便更好地处理不同类型的错误:
// AppError.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true; // 标记为已知操作错误
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
// 使用示例
const AppError = require('./utils/AppError');
if (!user) {
throw new AppError('User not found', 404);
}
使用try-catch
和Promise错误处理来捕获异步错误:
// 使用async/await的错误处理
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return next(new AppError('User not found', 404));
}
res.status(200).json({ status: 'success', data: { user } });
} catch (err) {
next(err); // 传递错误到错误处理中间件
}
});
// 或者使用一个包装函数来避免重复的try-catch
const catchAsync = fn => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
app.get('/users/:id', catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new AppError('User not found', 404));
}
res.status(200).json({ status: 'success', data: { user } });
}));
避免在事件循环中使用CPU密集型操作:
// 不好的做法 - 阻塞事件循环
app.get('/compute', (req, res) => {
const result = computeIntensive(req.query.input);
res.json({ result });
});
// 好的做法 - 使用工作线程
const { Worker } = require('worker_threads');
app.get('/compute', (req, res) => {
const worker = new Worker('./workers/compute.js', {
workerData: { input: req.query.input }
});
worker.on('message', result => {
res.json({ result });
});
worker.on('error', err => {
res.status(500).json({ error: err.message });
});
});
使用缓存来避免重复计算或数据库查询:
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 300秒的标准TTL
async function getUserData(userId) {
// 尝试从缓存获取
const cacheKey = `user:${userId}`;
const cachedData = cache.get(cacheKey);
if (cachedData) {
return cachedData;
}
// 缓存未命中,从数据库获取
const userData = await db.Users.findById(userId);
// 存入缓存
cache.set(cacheKey, userData);
return userData;
}
使用压缩可以显著减少传输数据的大小:
const compression = require('compression');
app.use(compression());
使用流而不是一次性加载整个文件:
// 不好的做法 - 一次性读取整个文件
app.get('/download/:file', (req, res) => {
const file = fs.readFileSync(`./files/${req.params.file}`);
res.send(file);
});
// 好的做法 - 使用流
app.get('/download/:file', (req, res) => {
const fileStream = fs.createReadStream(`./files/${req.params.file}`);
fileStream.pipe(res);
});
在Express应用中使用各种安全中间件:
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const xss = require('xss-clean');
const hpp = require('hpp');
const cors = require('cors');
// 设置安全HTTP头
app.use(helmet());
// 限制来自同一IP的请求
const limiter = rateLimit({
max: 100, // 每小时最多100个请求
windowMs: 60 * 60 * 1000,
message: 'Too many requests from this IP, please try again in an hour!'
});
app.use('/api', limiter);
// 防止XSS攻击
app.use(xss());
// 防止HTTP参数污染
app.use(hpp());
// 启用CORS
app.use(cors());
使用加盐哈希而非明文存储密码:
const bcrypt = require('bcrypt');
async function registerUser(email, password) {
// 哈希密码
const salt = await bcrypt.genSalt(12);
const hashedPassword = await bcrypt.hash(password, salt);
// 存储用户信息和哈希密码
await db.Users.create({
email,
password: hashedPassword
});
}
async function loginUser(email, password) {
const user = await db.Users.findOne({ email });
if (!user) {
throw new AppError('Invalid email or password', 401);
}
// 比较提供的密码和存储的哈希密码
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new AppError('Invalid email or password', 401);
}
return user;
}
验证和净化所有用户输入:
const { body, validationResult } = require('express-validator');
app.post(
'/register',
[
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }).trim(),
body('name').not().isEmpty().trim().escape()
],
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 继续处理...
}
);
使用Jest、Mocha等测试框架编写测试:
// userService.test.js
const userService = require('../src/services/userService');
const db = require('../src/models');
const AppError = require('../src/utils/AppError');
jest.mock('../src/models');
describe('User Service', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('getUserById', () => {
it('should return user when user exists', async () => {
const mockUser = { id: 1, name: 'Test User' };
db.Users.findById.mockResolvedValue(mockUser);
const result = await userService.getUserById(1);
expect(result).toEqual(mockUser);
expect(db.Users.findById).toHaveBeenCalledWith(1);
});
it('should throw an error when user does not exist', async () => {
db.Users.findById.mockResolvedValue(null);
await expect(userService.getUserById(1)).rejects.toThrow(AppError);
expect(db.Users.findById).toHaveBeenCalledWith(1);
});
});
});
使用GitHub Actions或Jenkins配置CI/CD流程:
# .github/workflows/node.js.yml
name: Node.js CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
- name: Build
run: npm run build --if-present
使用Winston或Bunyan等日志库实现结构化日志:
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// 在请求中间件中使用
app.use((req, res, next) => {
logger.info({
message: 'Incoming request',
method: req.method,
url: req.url,
ip: req.ip
});
next();
});
// 在错误处理中使用
app.use((err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
method: req.method,
url: req.url,
ip: req.ip
});
res.status(err.statusCode || 500).json({
status: 'error',
message: err.message
});
});
集成APM工具如New Relic、Datadog或Elastic APM:
// New Relic示例
require('newrelic');
const express = require('express');
const app = express();
// 其他应用设置...
以上最佳实践将帮助你构建更加健壮、高效和安全的Node.js应用。请记住,没有一成不变的规则,始终根据你的具体项目需求和约束来应用这些实践。
随着你的Node.js开发经验增长,你会发现更多细微的优化点和特定于你应用领域的最佳实践。持续学习,不断实验,并与社区分享你的发现,将使你成为更好的Node.js开发者。