ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenCode框架核心模块深度解析:从应用启动到异常处理的全链路实践

OpenCode框架核心模块深度解析:从应用启动到异常处理的全链路实践 1. 项目缘起为什么我们需要梳理OpenCode的核心模块最近在整理一个基于OpenCode框架开发的项目文档过程中我意识到虽然每天都在用它写代码、调接口但真要让我把它的核心模块脉络清晰地画出来还真得花点功夫。这就像你天天开车却不一定清楚发动机、变速箱、底盘的具体协作关系。对于团队新人来说理解一个框架的“骨架”更是头等大事直接决定了后续的开发效率和问题排查能力。所以我决定花点时间把OpenCode的核心模块彻底梳理一遍这既是一次自我复盘也希望能给正在学习或评估这个框架的朋友们一份清晰的“地图”。OpenCode是一个面向现代Web应用开发的开源框架以其清晰的架构和强大的插件化能力著称。但它的文档往往侧重于具体API的使用对于模块间的职责划分和协作逻辑则需要开发者自己在实践中摸索。这次梳理我将抛开官方文档的目录结构从一个一线开发者的视角拆解那些真正构成OpenCode心脏和骨架的模块并分享在实际项目中与它们“打交道”的心得与避坑指南。2. 基石与蓝图应用初始化与配置管理模块任何框架的启动都始于一个明确的入口和一套可管理的配置。在OpenCode中这部分职责主要由应用实例Application和配置管理Configuration两大模块承担。理解它们是理解整个框架运行逻辑的第一步。2.1 Application框架的单一入口与生命周期管家Application类是OpenCode应用的起点和总控中心。它遵循单一实例原则在整个应用生命周期中你通常只与这一个Application实例交互。它的核心职责远不止“启动应用”那么简单。2.1.1 核心职责与启动流程拆解首先Application负责解析启动参数。无论是通过命令行传入的环境变量、配置文件路径还是默认的约定都会在这里被统一处理。一个常见的启动代码片段如下// 通常在你的应用入口文件 (如 app.js 或 main.js) 中 const { Application } require(opencode); const path require(path); async function bootstrap() { // 1. 实例化Application可以传入配置对象或配置文件路径 const app new Application({ baseDir: path.join(__dirname, ..), // 指定项目根目录 env: process.env.NODE_ENV || development, // 设置运行环境 }); // 2. 加载配置。这里会合并默认配置、环境配置、应用配置。 await app.loadConfig(); // 3. 加载并初始化所有在配置中定义的服务、控制器、中间件等。 await app.load(); // 4. 启动应用监听端口对外提供服务。 await app.start(); // 5. 注册优雅关闭钩子处理进程退出信号。 app.on(stop, async () { await app.close(); process.exit(0); }); } bootstrap().catch(err { console.error(Application startup failed:, err); process.exit(1); });这个过程看似简单但内部包含了复杂的模块加载顺序。app.load()方法是关键它会按照预设的优先级例如插件 - 配置 - 服务 - 控制器 - 中间件依次初始化各个模块。这里有一个非常重要的经验如果你自定义的模块依赖于另一个模块比如你的服务需要用到数据库连接你必须确保依赖模块的加载顺序在前。OpenCode通常通过beforeStart或afterStart这样的生命周期钩子来管理但理解这个顺序能帮你避免“服务未定义”的运行时错误。2.1.2 生命周期钩子在关键时刻注入你的逻辑Application提供了丰富的生命周期事件这是框架扩展性的体现。除了上面代码中的stop更常用的是ready和beforeStart。beforeStart: 在所有模块加载完成之后应用启动如监听端口之前触发。这是进行最后检查、建立数据库连接池、预热缓存等操作的黄金时间点。ready: 应用完全启动并准备好接收请求时触发。适合在这里注册一些需要依赖已启动服务的后台任务。app.on(beforeStart, async () { // 确保数据库连接池已建立 await app.database.authenticate(); console.log(Database connection pool is ready.); }); app.on(ready, () { // 启动一个定时任务例如每5分钟同步一次数据 setInterval(syncExternalData, 5 * 60 * 1000); });注意生命周期钩子中的异步操作必须妥善处理错误。如果beforeStart钩子中的操作失败框架应该阻止应用启动否则会带着隐患运行。在实际编码中务必对这里的异步调用进行try...catch并根据业务决定是抛出错误终止启动还是记录日志降级处理。2.2 Configuration灵活且强大的配置驱动引擎OpenCode推崇“约定优于配置”但优秀的配置系统是约定得以实现的基础。其配置管理模块支持多环境、多数据源合并并实现了配置的动态更新。2.2.1 配置的加载与合并策略配置的加载源按优先级从低到高通常是框架默认配置 应用默认配置(config.default.js) 环境配置(config.{env}.js) 本地覆盖配置(config.local.js) 运行时传入的配置对象。这种分层策略保证了在不同环境开发、测试、生产下能灵活切换配置。一个典型的配置目录结构如下project-root/ ├── config/ │ ├── config.default.js // 所有环境的默认配置 │ ├── config.prod.js // 生产环境覆盖配置 │ ├── config.unittest.js // 单元测试环境配置 │ └── plugin.js // 插件配置 └── package.json在config.default.js中你可能会这样定义数据库配置// config/config.default.js module.exports { database: { client: mysql2, connection: { host: 127.0.0.1, port: 3306, user: root, password: , database: myapp_dev }, pool: { min: 0, max: 5 } } };然后在config.prod.js中覆盖生产环境的连接信息// config/config.prod.js module.exports { database: { connection: { host: process.env.DB_HOST || prod-db-host, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: myapp_prod }, pool: { min: 2, max: 20 } // 生产环境连接池更大 } };2.2.2 配置的动态获取与监听在代码中你可以通过app.config对象获取任何配置。OpenCode的配置对象是惰性加载且缓存的访问效率很高。const dbConfig app.config.database; const serverPort app.config.server?.port || 7001;更强大的是部分配置支持热更新。例如你更改了某个业务开关的配置希望在不重启应用的情况下生效。这需要配置源本身支持如来自配置中心并且在定义配置时声明为可监听。框架内部会使用类似Object.defineProperty或Proxy的机制来实现。这里有个坑不是所有配置都适合热更新。像数据库连接字符串、服务器端口这种在应用启动时就被其他模块消费并建立长连接的配置动态更新很可能导致状态不一致或连接泄漏。通常只有业务规则、功能开关等无状态配置适合热更新。3. 请求的旅程路由、控制器与中间件模块这是与业务开发最直接相关的部分也是HTTP请求进入应用后经历的核心处理链路。OpenCode在这部分的设计清晰地区分了路由寻址、业务逻辑和横切关注点。3.1 RouterURL到处理函数的智能映射器路由模块的职责是将HTTP请求的MethodGET、POST等和Path/api/users映射到具体的控制器Controller和方法Action上。OpenCode的路由器支持多种声明式写法。3.1.1 路由定义的两种主流风格第一种是装饰器Decorator风格在TypeScript或ES Next项目中非常流行代码意图清晰。// app/controller/user.controller.ts import { Controller, Get, Post, Body, Query } from opencode; Controller(/api/users) // 定义路由前缀 export class UserController { Get(/) // GET /api/users async index(Query() query: { page: number, size: number }) { // 查询用户列表 return await this.userService.list(query); } Post(/) // POST /api/users async create(Body() createUserDto: CreateUserDto) { // 创建新用户 return await this.userService.create(createUserDto); } Get(/:id) // GET /api/users/123 async show(Param(id) id: string) { // 获取单个用户详情 return await this.userService.findById(id); } }第二种是配置文件风格在一个集中的路由文件如app/router.js中定义所有规则。这种方式将所有路由规则收口在一处便于管理和查看全局API结构尤其在大型项目中。// app/router.js module.exports app { const { router, controller } app; router.get(/api/users, controller.user.index); router.post(/api/users, controller.user.create); router.get(/api/users/:id, controller.user.show); // 嵌套路由 router.resources(posts, /api/posts, controller.post); // 自动生成CRUD路由 };3.1.2 路由参数解析与校验的实践路由参数/users/:id中的id、查询字符串?page1和请求体Body的解析是路由层的重要工作。OpenCode通常与参数校验库如class-validator、joi深度集成。我强烈建议将参数校验放在路由/控制器入口处遵循“Fail Fast”原则。使用装饰器进行校验是最优雅的方式import { IsString, IsInt, Min, Max } from class-validator; class QueryUserDto { IsInt() Min(1) page: number; IsInt() Min(1) Max(100) size: number; } Controller(/api/users) export class UserController { Get(/) async index(Query() query: QueryUserDto) { // 框架会自动校验 // query参数在此处已经是校验通过且类型转换后的结果 const { page, size } query; // ...业务逻辑 } }如果校验失败框架会自动抛出400状态码的异常并附上详细的错误信息无需在业务代码中手动判断。避坑提示确保你的校验规则如IsInt()能正确处理字符串形式的数字如?page“1”。有些校验库默认是严格类型检查需要配合Transform装饰器先进行类型转换。3.2 Controller业务逻辑的协调者与HTTP适配器控制器Controller是MVC模式中的“C”它不应包含复杂的业务逻辑而应作为HTTP世界与业务领域之间的适配器。其核心职责是接收并校验输入从请求中提取参数、查询字符串、请求体、头部信息。调用服务将处理委托给一个或多个服务Service层方法。组装响应将服务层返回的结果封装成合适的HTTP响应状态码、数据格式、头部。一个健康的控制器方法应该非常“薄”。如果发现控制器方法超过了50行里面充满了if-else和计算逻辑那就要考虑是否应该将这部分逻辑下沉到服务层或领域模型中。// 反面教材臃肿的控制器 Post(/orders) async createOrder(Body() body) { // 参数校验手动冗长 if (!body.userId || !body.productId) { throw new Error(Missing required fields); } // 业务逻辑应放在Service中 const user await this.userRepo.find(body.userId); if (!user) { throw new Error(User not found); } const product await this.productRepo.find(body.productId); if (!product || product.stock body.quantity) { throw new Error(Product out of stock); } // 计算、状态变更等应放在Service或Domain中 const totalPrice product.price * body.quantity; const order { ...body, totalPrice, status: created }; await this.orderRepo.save(order); // 响应 return { success: true, orderId: order.id }; } // 正面教材清晰的控制器 Post(/orders) async createOrder(Body() createOrderDto: CreateOrderDto) { // 装饰器自动校验 // 一行代码调用服务层职责清晰 const order await this.orderService.createOrder(createOrderDto); // 统一响应格式可以结合拦截器Interceptor做得更好 return { code: 200, data: order, message: Order created successfully }; }3.3 Middleware处理横切关注点的利器中间件Middleware是洋葱模型的核心用于处理那些跨越多个路由的公共逻辑例如身份认证、请求日志、响应时间计算、全局错误处理等。3.3.1 中间件的注册与执行顺序在OpenCode中中间件可以在全局、单个路由或路由组级别注册。执行顺序至关重要它决定了你的认证、日志、限流等逻辑的生效时机。// config/config.default.js module.exports { middleware: [errorHandler, auth, logger], // 全局中间件执行顺序 }; // app/middleware/auth.js module.exports (options, app) { return async function authMiddleware(ctx, next) { // 1. 前置处理例如检查请求头中的Token const token ctx.headers[authorization]; if (!token) { ctx.throw(401, Unauthorized); } const user await verifyToken(token); // 验证Token ctx.state.user user; // 将用户信息挂载到ctx.state上 // 2. 执行后续中间件和路由处理器 await next(); // 3. 后置处理通常用于清理或记录 // 注意这里无法修改已经发送的响应体但可以设置响应头或记录日志 app.logger.info(User ${user.id} accessed ${ctx.path}); }; };3.3.2 编写高质量中间件的经验保持无状态和幂等性中间件不应依赖外部可变状态相同的输入应产生相同的副作用。这有利于测试和复用。善用ctx.state这是框架提供的用于在中间件和下游控制器/服务之间传递数据的命名空间。避免直接往ctx对象上随意添加属性以免造成污染和冲突。异常处理中间件中发生的错误应该被抛出由全局错误处理中间件如errorHandler统一捕获和格式化。不要在中间件内部吞掉错误并返回一个不规范的响应。性能考量中间件在每次请求都会执行避免在其中进行沉重的同步操作或阻塞I/O。对于耗时的操作如复杂的权限计算考虑使用缓存或异步处理。注意一个常见的错误是在中间件的后置处理阶段await next()之后尝试修改响应体ctx.body。此时响应可能已经发送给客户端修改是无效的。后置处理通常只适合记录日志、设置缓存头如Cache-Control等操作。4. 数据的桥梁服务、模型与数据库集成模块业务逻辑的核心在服务层而数据持久化的核心在模型层。OpenCode通过服务Service和模型Model模块以及集成的ORM如Sequelize、TypeORM清晰地分离了业务规则与数据访问细节。4.1 Service领域逻辑的安身之所服务层是放置复杂业务逻辑、协调多个模型实体操作、以及封装外部服务调用的最佳位置。它应该是无状态的并且可以被控制器和其他的服务调用。4.1.1 服务的组织与依赖注入在OpenCode中服务通常存放在app/service目录下框架的依赖注入DI容器会自动管理它们的生命周期和依赖关系。// app/service/user.service.ts import { Provide, Inject } from opencode; import { UserModel } from ../model/user.model; import { MailService } from ./mail.service; Provide() // 声明此类由容器管理 export class UserService { Inject() // 注入UserModel实例 userModel: UserModel; Inject() mailService: MailService; async createUser(createUserDto: CreateUserDto) { // 1. 业务规则校验例如用户名是否已存在 const existingUser await this.userModel.findOne({ where: { username: createUserDto.username } }); if (existingUser) { throw new Error(Username already exists); } // 2. 创建用户实体这里可以加入密码加密等逻辑 const hashedPassword this.hashPassword(createUserDto.password); const user await this.userModel.create({ ...createUserDto, password: hashedPassword, }); // 3. 触发副作用例如发送欢迎邮件 await this.mailService.sendWelcomeEmail(user.email); // 4. 返回结果 return user; } private hashPassword(password: string): string { // 密码哈希逻辑 return crypto.createHash(sha256).update(password app.config.salt).digest(hex); } }依赖注入的优势在于解耦。UserService不需要知道UserModel或MailService是如何被创建的它只需要声明依赖框架会在运行时提供正确的实例。这使得单元测试变得非常容易你可以轻松地用Mock对象替换真实的依赖。4.1.2 事务管理确保数据一致性涉及多个数据库写操作的服务方法必须考虑事务。OpenCode集成的ORM通常提供了事务支持。async function placeOrder(orderData) { // 不使用事务危险 await this.productModel.decrement(stock, { where: { id: orderData.productId } }); await this.orderModel.create(orderData); // 如果这里失败库存已经减少了 } async function placeOrderWithTransaction(orderData) { // 使用事务安全 const transaction await this.ctx.model.transaction(); // 从上下文获取事务 try { const product await this.productModel.findByPk(orderData.productId, { transaction, lock: transaction.LOCK.UPDATE }); if (product.stock orderData.quantity) { throw new Error(Insufficient stock); } product.stock - orderData.quantity; await product.save({ transaction }); const order await this.orderModel.create(orderData, { transaction }); await transaction.commit(); // 提交事务 return order; } catch (error) { await transaction.rollback(); // 回滚事务 throw error; // 重新抛出错误 } }重要提示事务的边界要合理。不要在整个服务方法外层包裹一个大事务这会降低并发性能并增加死锁风险。事务应只包含必须原子执行的数据库操作序列。同时注意在事务内查询时使用{ lock: ... }进行行锁或表锁以防止更新丢失。4.2 Model与ORM数据访问的抽象层模型Model是数据表的抽象定义了数据结构、关系和操作。OpenCode通常不自己实现ORM而是集成成熟的第三方库如Sequelize对多种SQL数据库或Mongoose对MongoDB。4.2.1 模型定义与关系映射以Sequelize为例模型定义不仅包括字段还包括与其他模型的关系一对一、一对多、多对多。// app/model/user.model.js module.exports app { const { STRING, INTEGER, DATE } app.Sequelize; const User app.model.define(user, { id: { type: INTEGER, primaryKey: true, autoIncrement: true }, username: { type: STRING(30), unique: true, allowNull: false }, email: { type: STRING(50), unique: true }, password: { type: STRING(100), allowNull: false }, createdAt: DATE, updatedAt: DATE, }, { // 模型选项如指定表名 tableName: users, }); // 定义关联 User.associate function() { // 一个用户拥有多篇文章 app.model.User.hasMany(app.model.Post, { foreignKey: authorId, as: posts }); // 一个用户属于多个角色多对多 app.model.User.belongsToMany(app.model.Role, { through: app.model.UserRole, // 通过联结表 foreignKey: userId, as: roles, }); }; return User; };定义关联后你就可以在查询时非常方便地进行预加载Eager Loading避免N1查询问题。// 查找用户及其所有文章 const userWithPosts await app.model.User.findByPk(userId, { include: [{ model: app.model.Post, as: posts }] }); // 查找用户及其角色 const userWithRoles await app.model.User.findByPk(userId, { include: [{ model: app.model.Role, as: roles }] });4.2.2 查询构建与性能优化ORM提供了强大的查询构建器但不当使用会导致性能问题。避免使用SELECT *始终明确指定需要的字段。Model.findAll({ attributes: [id, username] })善用预加载如上例所示使用include一次性加载关联数据。使用分页对于列表接口务必使用limit和offset或基于游标的分页。警惕循环中的查询绝对不要在循环内部执行数据库查询。应该先收集所有ID然后通过一次IN查询获取所有数据再在内存中进行关联。理解ORM生成的SQL在开发阶段开启ORM的日志功能查看实际执行的SQL语句检查是否有不必要的联表、全表扫描或错误索引。5. 扩展与集成插件、定时任务与自定义生命周期一个框架的活力在于其扩展能力。OpenCode通过插件机制、定时任务和自定义启动逻辑允许开发者无缝集成第三方能力或构建平台化功能。5.1 Plugin功能模块化的终极形态插件Plugin是一个独立的、可复用的功能单元它可以包含配置、中间件、服务、控制器等任何应用组件。使用插件可以保持核心应用简洁并方便地开启或关闭功能。5.1.1 插件的结构与启用一个典型的插件目录结构如下your-plugin/ ├── package.json ├── config/ │ └── config.default.js ├── app/ │ ├── middleware/ │ ├── service/ │ └── controller/ ├── app.js (可选插件的入口文件用于执行自定义初始化) └── README.md在应用中使用插件非常简单只需在配置文件中声明即可// config/plugin.js module.exports { // 启用一个内置或第三方插件 sequelize: { enable: true, package: opencode-sequelize, // 指定插件包名 }, redis: { enable: true, package: opencode-redis, }, // 启用一个本地开发的插件 myPlugin: { enable: true, path: path.join(__dirname, ../plugins/my-plugin), // 指定插件路径 } };插件被启用后它提供的中间件、服务等就可以像应用本身内置的一样被使用。开发插件时需要注意命名空间隔离避免与服务名、配置键名发生冲突。一个好的实践是使用插件名作为前缀例如redis插件提供的服务可以命名为redis.client。5.2 Schedule后台任务的优雅管理很多应用需要执行定时任务如数据清理、报表生成、消息推送等。OpenCode的定时任务模块通常通过插件如opencode-schedule实现提供了集中式的、基于Cron表达式的任务管理能力。5.2.1 定义与配置定时任务任务通常定义在app/schedule目录下每个文件导出一个任务类。// app/schedule/cleanup_log.js const { Subscription } require(opencode-schedule); module.exports class CleanupLog extends Subscription { // 通过cron属性指定执行周期 static get schedule() { return { interval: 1d, // 每天执行一次也支持cron表达式如 0 0 3 * * *每天凌晨3点 type: worker, // 指定在哪个进程中执行。all在所有worker进程worker在随机一个worker进程 }; } // 任务实际执行的逻辑 async subscribe() { const { ctx, app } this; const cutoff new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30天前 const result await app.model.Log.destroy({ where: { createdAt: { [app.Sequelize.Op.lt]: cutoff, }, }, }); app.logger.info([CleanupLog] Deleted ${result} old log records.); } };5.2.2 定时任务的最佳实践与陷阱幂等性定时任务必须设计成幂等的即多次执行与单次执行的效果相同。因为网络抖动、进程重启都可能导致任务被重复执行。执行类型选择type: worker任务在单个worker进程执行。适用于非全局性、可并行执行的任务。要确保逻辑支持多实例同时运行或通过分布式锁控制。type: all任务在所有worker进程都会执行。慎用除非你明确需要在每个进程都执行例如每个进程都需要刷新自己的本地缓存。大多数情况下这会导致任务被重复执行N次N为worker数。长任务处理如果一个任务执行时间可能很长需要考虑将其拆分为更小的批次或者使用消息队列异步处理避免阻塞其他定时任务和占用过多资源。错误处理在subscribe方法内部做好try-catch并记录详细的错误日志。未捕获的错误可能导致整个任务进程中断。5.3 自定义启动逻辑在框架生命周期中嵌入你的代码除了插件和定时任务有时你只需要在应用启动时执行一些简单的初始化代码比如连接一个外部API、预加载一些数据到内存。这可以通过在app.js或agent.js中编写代码来实现。app.js在应用Worker进程启动时执行。agent.js在Agent进程一个长期运行的辅助进程用于处理后台任务或跨Worker通信启动时执行。// app.js module.exports app { // 应用启动完成后执行 app.beforeStart(async () { // 例如预加载城市数据到内存缓存 const cities await app.model.City.findAll({ attributes: [id, name] }); app.cache app.cache || {}; app.cache.cities cities.reduce((map, city) { map[city.id] city.name; return map; }, {}); app.logger.info(Preloaded ${cities.length} cities into cache.); }); // 也可以直接监听框架事件 app.on(server, server { // HTTP/HTTPS服务器创建完成 console.log(Server is listening on, server.address()); }); };这种模式非常适合进行一些轻量级的、与应用核心业务紧密相关的初始化工作。切记这里的代码会在每次Worker进程启动时运行如果操作非常耗时会拖慢应用启动速度。对于重型初始化考虑使用定时任务在后台异步执行或者使用Agent进程。6. 保障与洞察日志、监控与异常处理模块线上应用的稳定运行离不开可观测性。OpenCode提供了日志、监控和异常处理机制帮助开发者洞察应用内部状态快速定位问题。6.1 Logger应用行为的忠实记录者一个设计良好的日志系统是线上排查问题的生命线。OpenCode的日志器通常支持多级别DEBUG, INFO, WARN, ERROR、多输出目的地控制台、文件、日志服务和上下文关联。6.1.1 分级日志与上下文// 在控制器、服务或中间件中记录日志 ctx.logger.debug(Detailed debug info: %j, someObject); // 开发环境使用 ctx.logger.info(User %s logged in from %s, userId, ip); // 记录常规信息 ctx.logger.warn(API %s is deprecated, please use %s, oldPath, newPath); // 警告 ctx.logger.error(new Error(Database connection failed)); // 记录错误会自动包含堆栈 // 在非请求上下文如定时任务、自定义脚本中使用app.logger app.logger.info(Scheduled task started.);关键技巧为每条日志添加上下文。在Web请求中框架通常会通过中间件为每个请求生成一个唯一的requestId并自动附加到该请求生命周期中的所有日志里。这样在查看日志文件时你可以轻松地过滤出同一个请求的所有相关日志完整还原请求的处理链路。确保你的日志聚合系统如ELK、Sentry支持按requestId进行检索。6.1.2 日志配置与切割在生产环境中日志必须被妥善管理避免单个文件过大。// config/config.prod.js module.exports { logger: { dir: /path/to/your/logs, // 日志目录 level: WARN, // 生产环境只记录WARN及以上级别 consoleLevel: ERROR, // 控制台只输出ERROR appLogName: myapp-app.log, coreLogName: myapp-core.log, agentLogName: myapp-agent.log, errorLogName: myapp-error.log, // 按文件大小切割 formatter: meta ${meta.date} ${meta.level} ${meta.pid} ${meta.message}, // 使用logrotator插件进行日志切割 // 通常配置在plugin.js中 }, };6.2 异常处理从崩溃到优雅降级未处理的异常是应用崩溃的元凶。OpenCode通过统一的异常处理中间件将异常转化为结构化的HTTP错误响应。6.2.1 定义业务异常首先定义你自己的业务异常类继承自框架的基础异常类。// app/exceptions/business.error.js const { HttpException } require(opencode); class BusinessException extends HttpException { constructor(code, message) { super(200); // HTTP状态码设为200实际错误码用业务code表示 this.code code; // 业务错误码如 10001 this.message message; this.isBusinessException true; } } class UserNotFoundException extends BusinessException { constructor() { super(10001, 用户不存在); } } class InsufficientBalanceException extends BusinessException { constructor() { super(10002, 账户余额不足); } } module.exports { BusinessException, UserNotFoundException, InsufficientBalanceException, };6.2.2 全局异常捕获与响应格式化然后编写一个全局错误处理中间件捕获所有未被处理的异常并格式化为统一的响应。// app/middleware/error_handler.js module.exports () { return async function errorHandler(ctx, next) { try { await next(); } catch (err) { // 记录错误日志 ctx.logger.error(err); // 设置默认的HTTP状态码和响应体 ctx.status err.status || 500; let response { code: ctx.status, message: err.message || Internal Server Error, // 非生产环境返回堆栈信息方便调试 stack: app.config.env prod ? undefined : err.stack, }; // 处理自定义的业务异常 if (err.isBusinessException) { ctx.status 200; // 业务异常HTTP状态码仍为200 response { code: err.code, message: err.message, data: null, }; } // 处理参数校验错误例如class-validator抛出的错误 if (err.status 422 err.errors) { ctx.status 200; response { code: 422, message: 参数校验失败, errors: err.errors, // 包含详细的字段错误信息 }; } // 发送响应 ctx.body response; // 注意这里不能再throw err否则错误会继续向上抛 } }; };在业务代码中你就可以直接抛出定义好的异常而无需关心如何向客户端返回错误。async function getUser(id) { const user await this.userModel.findByPk(id); if (!user) { throw new UserNotFoundException(); // 直接抛出错误处理中间件会接管 } return user; }这种模式使得业务逻辑非常干净错误处理逻辑集中且一致极大地提高了代码的可维护性。
返回列表