强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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

分层职责

职责示例
RouteURL 映射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

命名规范

类型规范示例
变量/函数camelCaseuserName, getUserById()
常量UPPER_SNAKEMAX_RETRY, API_BASE_URL
类/类型PascalCaseUserService, HttpRequest
文件名camelCase 或 kebab-caseuserService.js, user-service.js
测试文件同名 + .testuserService.test.js
环境变量UPPER_SNAKEDATABASE_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 auditnpm outdated,及时修复漏洞。

⚠️ 文档即代码:README 要包含项目简介、启动方式、环境变量说明。

扩展阅读


上一章第 23 章 · CI/CD 下一章第 25 章 · 实战项目 — 全栈应用、爬虫、CLI 工具和微服务。