第 24 章 · 最佳实践
第 24 章 · 最佳实践
24.1 项目结构
小型项目
my-app/
├── src/
│ ├── index.js # 入口文件
│ ├── app.js # Express 配置
│ ├── routes/
│ │ └── index.js
│ ├── middleware/
│ ├── models/
│ └── utils/
├── tests/
├── .env
├── .eslintrc.js
├── .gitignore
├── package.json
└── README.md
中大型项目(分层架构)
my-app/
├── src/
│ ├── config/ # 配置
│ │ ├── index.js # 统一配置入口
│ │ ├── database.js # 数据库配置
│ │ └── redis.js # Redis 配置
│ ├── api/ # API 层
│ │ ├── routes/ # 路由定义
│ │ │ ├── index.js
│ │ │ ├── users.js
│ │ │ └── posts.js
│ │ ├── controllers/ # 控制器(处理请求/响应)
│ │ │ ├── userController.js
│ │ │ └── postController.js
│ │ ├── middlewares/ # 中间件
│ │ │ ├── auth.js
│ │ │ ├── validator.js
│ │ │ └── errorHandler.js
│ │ └── validators/ # 请求验证
│ │ └── userValidator.js
│ ├── services/ # 业务逻辑层
│ │ ├── userService.js
│ │ └── postService.js
│ ├── repositories/ # 数据访问层
│ │ ├── userRepository.js
│ │ └── postRepository.js
│ ├── models/ # 数据模型
│ │ ├── User.js
│ │ └── Post.js
│ ├── utils/ # 工具函数
│ │ ├── errors.js
│ │ ├── logger.js
│ │ └── helpers.js
│ ├── jobs/ # 后台任务
│ └── events/ # 事件处理
├── tests/
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── scripts/ # 脚本(迁移、种子等)
├── docs/ # 文档
├── .env.example # 环境变量模板
├── .eslintrc.js
├── .prettierrc
├── .gitignore
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
├── jest.config.js
├── package.json
└── README.md
分层职责
| 层 | 职责 | 示例 |
|---|---|---|
| Route | URL 映射 | router.get('/users', ...) |
| Controller | 请求/响应处理 | 解析参数、调用 Service、格式化响应 |
| Service | 业务逻辑 | 验证规则、事务、编排多个 Repository |
| Repository | 数据访问 | 数据库 CRUD 操作 |
| Model | 数据结构定义 | Schema、Type、Interface |
// routes/users.js
router.get('/:id', userController.getById);
// controllers/userController.js
exports.getById = async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) throw new NotFoundError('用户');
res.json({ data: user });
};
// services/userService.js
exports.findById = async (id) => {
const user = await userRepo.findById(id);
return user;
};
// repositories/userRepository.js
exports.findById = async (id) => {
return prisma.user.findUnique({ where: { id: Number(id) } });
};
24.2 代码规范
ESLint + Prettier
npm install -D eslint @eslint/js prettier eslint-config-prettier
// eslint.config.js(Flat Config,ESLint 9+)
const js = require('@eslint/js');
const prettier = require('eslint-config-prettier');
module.exports = [
js.configs.recommended,
prettier,
{
languageOptions: {
ecmaVersion: 2024,
sourceType: 'commonjs',
globals: {
console: 'readonly',
process: 'readonly',
__dirname: 'readonly',
module: 'readonly',
require: 'readonly',
exports: 'readonly',
},
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': ['warn', { allow: ['warn', 'error'] }],
'eqeqeq': 'error',
'no-var': 'error',
'prefer-const': 'error',
'prefer-template': 'warn',
'no-throw-literal': 'error',
'no-return-await': 'error',
'require-await': 'error',
},
},
];
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"endOfLine": "lf"
}
Git Hooks
npm install -D husky lint-staged
npx husky init
// package.json
{
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
# .husky/pre-commit
npx lint-staged
命名规范
| 类型 | 规范 | 示例 |
|---|---|---|
| 变量/函数 | camelCase | userName, getUserById() |
| 常量 | UPPER_SNAKE | MAX_RETRY, API_BASE_URL |
| 类/类型 | PascalCase | UserService, HttpRequest |
| 文件名 | camelCase 或 kebab-case | userService.js, user-service.js |
| 测试文件 | 同名 + .test | userService.test.js |
| 环境变量 | UPPER_SNAKE | DATABASE_URL, NODE_ENV |
24.3 配置管理
// config/index.js
require('dotenv').config();
const config = {
env: process.env.NODE_ENV || 'development',
port: Number(process.env.PORT) || 3000,
database: {
host: process.env.DB_HOST || 'localhost',
port: Number(process.env.DB_PORT) || 5432,
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || '',
name: process.env.DB_NAME || 'mydb',
},
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES || '24h',
},
log: {
level: process.env.LOG_LEVEL || 'info',
},
};
// 启动时验证必需的配置
const required = ['JWT_SECRET', 'DATABASE_URL'];
for (const key of required) {
if (!process.env[key]) {
console.error(`缺少必需的环境变量: ${key}`);
process.exit(1);
}
}
module.exports = config;
# .env.example
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
REDIS_URL=redis://localhost:6379
JWT_SECRET=your-secret-here
LOG_LEVEL=info
24.4 依赖管理
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix",
"outdated": "npm outdated",
"update:patch": "npm update",
"update:minor": "npx npm-check-updates -u --target minor",
"dedupe": "npm dedupe"
}
}
| 工具 | 用途 |
|---|---|
npm audit | 检查安全漏洞 |
npm outdated | 检查过期依赖 |
npm-check-updates | 批量更新依赖 |
npm dedupe | 去重依赖树 |
depcheck | 检查未使用的依赖 |
24.5 日志与监控
// 结构化日志
logger.info({
event: 'request',
method: req.method,
path: req.url,
status: res.statusCode,
duration: Date.now() - start,
requestId: req.id,
userId: req.user?.id,
ip: req.ip,
userAgent: req.headers['user-agent'],
});
// 健康检查端点
app.get('/health', async (req, res) => {
const checks = {
uptime: process.uptime(),
status: 'ok',
timestamp: new Date().toISOString(),
checks: {
database: await checkDatabase(),
redis: await checkRedis(),
memory: process.memoryUsage(),
},
};
const isHealthy = Object.values(checks.checks).every(c => c.status === 'ok');
res.status(isHealthy ? 200 : 503).json(checks);
});
24.6 性能建议清单
| 建议 | 说明 |
|---|---|
| 使用流处理大文件 | 避免 readFile 一次性加载 |
| 数据库连接池 | 不要每次查询创建新连接 |
| 缓存热点数据 | Redis 缓存频繁查询的结果 |
使用 Promise.all | 并行执行无依赖的异步操作 |
| 压缩响应 | 使用 compression 中间件 |
| 分页查询 | 不要返回全量数据 |
| 索引优化 | 为查询字段添加数据库索引 |
| 避免 N+1 查询 | 使用批量查询或 JOIN |
| 使用 CDN | 静态资源使用 CDN 加速 |
| 限流 | 防止恶意请求或突发流量 |
24.7 常见反模式
// ❌ 反模式 1:回调地狱
fs.readFile('a', (err, a) => {
fs.readFile('b', (err, b) => {
fs.readFile('c', (err, c) => { });
});
});
// ✅ 使用 async/await
const [a, b, c] = await Promise.all([
fs.promises.readFile('a'),
fs.promises.readFile('b'),
fs.promises.readFile('c'),
]);
// ❌ 反模式 2:吞掉错误
try {
await doSomething();
} catch (err) { } // 空 catch!
// ✅ 至少记录日志
try {
await doSomething();
} catch (err) {
logger.error({ err }, '操作失败');
throw err; // 或者返回降级结果
}
// ❌ 反模式 3:回调和 Promise 混用
function bad(data, callback) {
return new Promise((resolve, reject) => {
try {
const result = process(data);
callback(null, result); // 回调
resolve(result); // 又 resolve
} catch (err) {
callback(err); // 回调
reject(err); // 又 reject
}
});
}
// ❌ 反模式 4:无限制的并发
for (const url of urls) {
await fetch(url); // 顺序执行,太慢
}
// ❌ 反模式 5:每次请求都创建实例
app.get('/api/data', async (req, res) => {
const client = new MongoClient(url); // 每次创建新连接!
await client.connect();
});
// ✅ 使用连接池/单例
const client = new MongoClient(url);
await client.connect();
注意事项
⚠️ 不要过度抽象:YAGNI(You Aren’t Gonna Need It),不要为了"以后可能需要"而增加复杂度。
⚠️ 12-Factor App:遵循 12 要素应用 原则构建应用。
⚠️ 保持依赖更新:定期运行
npm audit和npm outdated,及时修复漏洞。
⚠️ 文档即代码:README 要包含项目简介、启动方式、环境变量说明。
扩展阅读
上一章:第 23 章 · CI/CD 下一章:第 25 章 · 实战项目 — 全栈应用、爬虫、CLI 工具和微服务。