路由
VextJS 采用 约定式文件路由 + 三段式路由定义,将文件路径自动映射为 URL 前缀,在文件内部通过 defineRoutes() 声明具体路由。
本文从一个可运行路由开始,说明文件映射、请求校验和业务接入。完整字段与默认值见 路由定义 API,必须遵守的边界见 HTTP 与路由规范。
示例约定:route-demo.ts 是可直接加入现有 VextJS 项目的独立完整例;其余展示 app、req、res、handler 的代码是对应 factory 或 handler 内的说明片段。业务组合另列服务和认证前提。
先跑通一个路由
1. 创建路由文件
前置条件:已有按 快速开始 创建、安装依赖且能通过 npm run dev 启动的 VextJS 项目。新建下面的文件;它不依赖数据库、自定义 service 或认证中间件。
2. 验证路径和校验
在项目根目录运行 npm run dev,另开终端发送请求;端口替换为启动输出中的实际值。先在项目根目录保存两个请求文件,避免不同终端对 JSON 引号的处理差异:
route-valid.json:
route-invalid.json:
在同一目录执行下面的命令。Windows PowerShell 将命令名 curl 换为 curl.exe:
文件前缀是 /route-demo,所以文件内写 "/" 或 "/:id",不要再次添加 /route-demo。响应外层是否包装由应用配置决定,校验失败的具体消息可能随 validator 和语言配置变化。
3. 接入业务逻辑
最小路由验证成功后,再把业务操作交给 service,按需增加校验、中间件、认证和响应声明。
以下章节中的 app.get(...) 等局部片段均位于 defineRoutes((app) => { ... }) 内。handler、user、data 等示意变量和 app.services.* 由项目提供,不是框架自动生成的业务能力。
factory 必须同步,handler 可以异步。路由注册使用 factory 块体内的直接顶层语句,不放进循环、条件分支或异步回调。支持可静态解析的函数绑定与默认重导出,详细边界见 工厂规则。
基本概念
文件路由映射
src/routes/ 中符合加载规则的路由文件映射为 URL 前缀。下表是分别说明的布局选择,users.ts 与 users/index.ts 不能同时存在:
三段式定义
VextJS 路由使用 三段式 (path, options, handler) 或 两段式 (path, handler) 定义:
三段式中第二个参数 options 是一个声明式配置对象,常用字段如下;完整字段(含响应、缓存、上传等)见 RouteOptions:
路由文件写法
每个路由文件默认导出一个 defineRoutes() 结果。先用上面的独立示例确认路径和校验,再把业务操作移入 service。不要把数据库连接、认证实现或一整套 CRUD 同时塞入第一个路由。
两段式适合健康检查等简单接口;需要校验、中间件、访问保护或响应声明时使用三段式。一个 factory 可以声明多个方法,但每次注册都必须是它块体中的直接语句。文件加载规则见本文后面的“路由加载优先级”和“排除规则”,精确签名见 defineRoutes API。
需要创建、查询、修改和删除资源时,继续阅读 业务路由组合片段。该段明确列出 service 和认证前提,不将项目业务实现当作框架内置能力。
路由加载优先级
当存在可能冲突的路由时,router-loader 按以下规则处理:
- 静态路由优先于动态路由:
/users/list优先于/users/:id - 文件按字母序排序:确保加载顺序确定性
- 同时检查文件前缀和最终路由身份:静态索引会拒绝
routes/users.ts与routes/users/index.ts这类同前缀入口;运行时还检查规范化后的 HTTP 方法与完整路径重复,路径大小写及尾斜杠变体也参与检测。不要用“最终路径不同”绕过文件前缀限制。 - 同路径 HEAD 优先于 GET,具体路径优先于通配路径:不要依赖文件名顺序覆盖已有路由。
排除规则
路由源支持 .ts、.js、.mjs。.cjs 会使加载失败,不能当作受支持或静默排除的路由源。以下文件会被跳过:
- 测试文件:
*.test.ts、*.spec.ts - 类型声明文件:
*.d.ts - 以
_或.开头的文件或目录 node_modules目录- 包含
.__vext_compiled__的生成临时文件
这些文件会被跳过,不会作为启动错误处理。运行时路由加载、路由诊断和 manifest 生成共用同一套排除策略。
可以利用 _ 前缀创建路由共享的工具模块:
HTTP 方法
defineRoutes() 回调中的 app 对象支持以下 HTTP 方法:
动态路由参数
文件级动态参数
使用 [paramName] 作为文件名或目录名,自动转换为路由动态参数:
路由内动态参数
在文件内部的路由路径中也可以使用 :paramName 语法:
请求对象 (req)
handler 通过 req 读取 HTTP 输入。处理业务数据时优先使用已声明 Schema 的 req.valid():它包含校验和类型转换后的值;原始 req.params/query/body/headers/cookies 仍可读取。
只有声明的校验位置才会产生结果;未声明的位置返回 undefined。字段是否可选取决于 Schema,不能用 TypeScript 泛型代替运行时校验。上面的 id 示例将字符串转换为数字;分页等可选字段可以在 handler 中设置业务默认值:
方法、URL、原始输入、请求 ID、IP、协议、Cookie、Session 和应用实例等属性集中列在 请求公开成员;精确签名与类型推导见 req.valid()。Session 需要先启用,上传文件读取及普通字段限制见 上传指南。
长连接或流式响应需要释放定时器等资源时,使用 req.onClose()。它在正常响应完成或连接提前断开时调用,每个回调至多一次;结束后注册会立即执行。回调触发不代表客户端异常断连,正常完成也不会中止 req.signal。需要取消下游操作时另按 signal 的状态处理。
响应对象 (res)
普通 JSON 接口在 handler 内调用 res.json(data) 发送业务数据;创建资源时传入201,删除后无内容时使用204。仅 return data 不会自动发送响应:
上述各行是不同请求的响应选择,不要在同一请求中依次发送。默认 config.response.wrap: true 将 JSON 包装为 { code: 0, data, requestId };204不发送消息体。字段、默认值及关闭包装的行为见 JSON响应。
其他响应方式按任务选择,精确参数和示例由请求与响应API承载:
需要约束JSON输出字段时继续看下文“OpenAPI文档配置”中的顶层 responses;需要返回错误时使用 app.throw(),不要把错误响应当成成功数据传给 res.json()。
参数校验
VextJS 集成 schema-dsl,在路由 options.validate 中声明校验规则,框架自动执行校验并生成 OpenAPI 文档。
DSL 语法速查
本页入门示例使用 integer:1-! 和 string:1-50!:! 表示必填,范围约束限制值或长度;可选字段使用 ? 或不加必填标记。规则写在路由选项中,handler 读取转换后的结果。
字符串、数字、email、url、boolean、日期和枚举等语法集中见 DSL语法详解 与 路由校验速查。校验描述不了“邮箱未注册”“用户拥有资源”等业务条件,这些仍由服务层和权限检查处理。
校验位置
校验顺序为 param → query → header → cookie → body。路径 param 非法时立即返回 HTTP 400,其他位置失败时立即返回 HTTP 422。
校验错误响应
校验失败时框架自动返回结构化的错误信息:
路由级中间件
通过 options.middlewares 为路由指定中间件。以下是组合片段,先创建 中间件指南 中的 audit-log 和 response-label 文件,再加入配置白名单;handler 代表你的业务处理器:
自定义路由中间件按声明顺序执行,并位于路由自动校验之前;在 next() 前读取 req.valid() 不能假定已经取得校验结果。认证中间件建立 req.auth 后,路由 auth guard 才能进行保护检查。
这里的工厂参数来自 response-label 定义,路由 options 整体替换配置默认 options。内置限流通过全局 rateLimit.enabled 与路由 override.rateLimit 配置,window 单位为秒,详见 覆盖配置。
OpenAPI 文档配置
通过 options.docs 配置路由的 OpenAPI 文档信息:
顶层 responses 是编译 JSON 序列化、OpenAPI 与生成客户端类型共用的运行时
契约。描述和示例保留在 docs.responses;同一状态 selector 不要在其中重复
声明 schema。
隐藏路由
不希望出现在 OpenAPI 文档中的路由,设置 docs.hidden: true。这不会阻止HTTP访问,访问保护仍需认证和授权:
访问 app 对象
defineRoutes() 的回调参数 app 可用于访问服务、日志、错误处理和配置:
路由 handler 中可以通过两种方式访问 app:
- 闭包
app:defineRoutes((app) => ...)中的app参数 req.app:请求对象上的真实运行期app引用
factory 的 app 是以真实应用为能力来源的 Proxy facade。app.config、app.services 及扩展属性的读取会转发到真实应用;它们不是复制到 collector 的属性快照。req.app 指向真实应用。
需要使用 fetch.get()、fetch.create() 等挂载方法时,在 handler 中使用 req.app.fetch,具体边界见 HTTP 客户端。
如果把 const config = app.remoteConfig 放在请求处理之外,变量仍会保留当时读取的值;需要最新值时,在 handler 内读取 app.remoteConfig 或 req.app.remoteConfig。这属于 JavaScript 引用捕获,与选择哪种 app 入口无关。
factory 收集结束后,其 HTTP 注册入口关闭;在 handler 中继续调用 app.get() 等方法会失败。
错误处理
app.throw() — 抛出 HTTP 错误
在路由或服务中使用 app.throw() 抛出错误,框架会统一处理并返回结构化响应:
app.throw() 会终止当前请求处理流程(函数签名返回 never),无需在其后添加 return。
如果这里抛出的是未预期异常,也可以直接:
框架同样会捕获它,但这条路径表示“未知运行时错误”,最终会返回 500 Internal Server Error。开发环境下,当 response.hideInternalErrors = false 时,JSON 500 响应会附带 stack;若你的目标是主动返回一个明确的 4xx/5xx HTTP 结果,仍应优先使用 app.throw(...)。
业务路由组合片段
以下以文章创建为例,连接“HTTP输入 → 业务操作 → HTTP响应”。运行前须实现 post service,并提供、在配置白名单声明负责建立 req.auth.userId 的 auth 中间件。它是业务接线片段;不具备这些前提时,先使用上面的 route-demo.ts。
扩展为CRUD时,沿用同一职责划分:
文章status可以使用 draft|published|archived 枚举。校验只约束声明输入;auth.required 也不会自动检查文章所有权、状态或数据库唯一性。service与认证的实现分别见 服务层、安全指南,包括真实项目依赖的完整操作示例见 CRUD API。
下一步
- 了解 服务层 如何组织业务逻辑
- 学习 中间件 的洋葱模型
- 探索 参数校验 的高级用法
- 查看 OpenAPI 文档 自动生成
- 核对 HTTP 与路由规范 中的稳定 Rule ID