热重载
VextJS 内置热重载机制,通过 vext dev 命令启动开发模式。框架监听文件变更,按职责执行后端 soft reload、前端重建或应用 worker 冷重启。
先用下方示例验证保存后响应变化,再了解重载范围、状态保留和失败恢复。热重载用于开发反馈;生产进程更新见 Cluster。
快速开始
1. 准备应用与路由
使用已有 TypeScript API 应用,或按 CLI 创建项目并安装依赖。在项目根目录新增:
文件名自动提供 /reload-demo 前缀;定义中的 "/" 不要重复写前缀。pid 仅用于此开发示例观察进程变化。
2. 启动并取得基线
在另一个终端请求:
应得到 HTTP 200,JSON 的 data.message 为 "v1"。记下 data.pid。已有 "dev": "vext dev" script 时,也可执行 npm run dev -- --port 3000 --verbose-lifecycle。
3. 修改已有文件
把上面路由中的 "v1" 改为 "v2" 并保存。等待终端出现成功重载结果,再重复请求。应仍为 HTTP 200、data.message 变为 "v2";正常 soft reload 下 PID 保持不变。
详细日志会显示变更文件及阶段耗时,例如:
数值只是示意。日志 T1:code 表示本批修改已有后端文件;编辑器若以删除再新建方式保存,也可能被识别为结构变更。
4. 验证新增与冷重启
- 复制此示例为
src/routes/reload-extra.ts:新增后请求/reload-extra应成功,正常日志为T2:structural;删除后该路径应回到 404。 - 在
src/config/default.ts的现有配置对象中添加或修改logger: { level: "debug" }并保存:等待重新 ready,再请求/reload-demo,业务结果仍为 v2,但 PID 应改变。保留其他配置项。 - 如未看到预期响应,先检查终端是否报错或正在恢复,不要只凭“检测到文件变化”判断完成。
验证结束后用 Ctrl+C 停止开发服务。键盘 h、r 等交互需要前台终端支持;后台管道中不应依赖这些按键。
日志与前端更新
默认模式简洁输出地址、启动耗时与重载结果;--verbose-lifecycle 或 --startup-profile 才显示详细启动 Banner,前者还会输出变更列表和重载阶段耗时。
启用前端后,页面、组件和静态资源有独立的 client rebuild 路径,开发输出默认为 .vext/client/。React Fast Refresh、CSS 更新和后端重载后的浏览器刷新分别受前端开发配置控制,见 Fast Refresh 与渲染刷新。后端请求验证成功不等于浏览器状态一定保留。
三层重载策略
先按文件职责分类为 cold、soft、client 或 ignore;soft 内部再按修改类型选择 T1/T2。服务或 Model 文件同样可能走 T1,新增路由同样可能走 T2。
Tier 1 — 修改已有后端文件
适用于 soft 类文件的 modify 事件,例如 routes、services、middlewares、models 和 locales 中支持的源码/资源。
最终替换的是请求入口,不是只更换某一个路由对象。原有 server socket 继续监听,新请求读取新 handler;已经进入旧 handler 的请求仍执行旧闭包,但它访问的 Service 等共享状态可能已经变化,不能承诺所有进行中请求完全不受影响。
耗时取决于依赖范围和重新装配工作。编译少量文件也可能引发广泛依赖失效;达到级联阈值时会升级为冷重启。
Tier 2 — 新增、删除或全量重载
soft 文件出现 add / delete,或按 h 请求全量源码重载时,先重新扫描编译入口并重建,再执行与 T1 相同的运行态更新流程。
文件重命名通常表现为删除和新增。编辑器保存方式和同批其他文件变化也会影响最终事件类别。
Tier 3 — 冷重启
配置、插件、preload、定时任务目录(默认 src/jobs,可通过 jobs.dir 修改)、根目录 package.json/lockfile/tsconfig.json 和 .env* 等变化需要重新初始化 worker。软重载若沿已加载的依赖图失效到定时任务模块,也会升级为冷重启;旧任务先停止触发并按关闭预算等待,然后新进程注册未来触发点。开发父进程等待旧 worker 退出,再等待新 worker ready,期间服务可能不可用,请求也可能中断。
修改中间件定义通常走 soft;修改配置中的中间件装配列表则因配置变化走 cold。依赖文件变化触发重启不会自动执行 npm install,应先安装实际需要的依赖。
Service、Model 与共享状态
受影响 Service 会重新实例化,未失效实例通常保留。框架会尝试调用旧实例的可选 dispose();dispose 失败记录警告,业务负责资源清理。进程内计数、定时器、长期缓存等不应假定能跨重载保存。
按需从 app.services 读取当前实例;其他对象在构造时缓存的旧服务引用不会因属性替换自动更新。源码 import 依赖与运行时动态持有的对象引用也不能视为同一张依赖图。
启用数据库和 Model 加载后,受影响 Model 会尝试替换注册定义;这不等于执行数据库迁移或改写已有记录。下面仅演示定义文件,完整数据库前置配置与验证见数据库:
Service/Model 局部恢复旧引用或定义不能撤销已发生的外部副作用。运行态更新失败时,整体流程会冷重启恢复一致状态。
重载策略决策表
以下以默认布局为例,实际还受已解析目录、变更事件与失败恢复影响:
源码目录外显式配置的语言包/Model 读取目录由布局单独登记,其变更走 cold;不要把它们概括为只能监听 src。
与 vext build 的关系
vext dev 将后端源码编译到 .vext/dev/,再由 worker 加载这些开发产物,不需要预先执行 vext build。
以下是具有对应 scripts 的 TypeScript 项目流程;纯 JavaScript 和自定义输出边界见构建:
CLI 选项
--host :: 会监听 IPv6 all interfaces;ready 日志会输出 http://[::1]:PORT 与 bracketed IPv6 Network URL。具体 IPv6 host 也会按 http://[IPv6]:PORT 展示。
默认 vext dev 只打印监听地址与总启动耗时;--startup-profile-json <path> 只写 JSON,不会自动打印 summary/details。需要在终端看到每个阶段耗时时,显式使用 --startup-profile。
文件监听规则
监听范围
默认覆盖 src 中支持的源码/资源、public 静态资源、项目 preload,以及根目录指定的配置和依赖文件。启用前端后,还覆盖配置解析得到的页面、组件和 publicDir 等角色目录,包括 src 之外的项目内目录。
原生 fs.watch 与 polling 均使用快照发现增删;静态资源不局限于 JavaScript/CSS,例如 robots.txt 和 PDF 也可触发 client rebuild。语言包和 Model 自定义读取目录由后端布局单独登记。
忽略规则
一般生成区和依赖区(node_modules/、dist/、build/、.vext/、.git/)、src/types/generated/、框架临时模块及未识别的文件类型不会进入正常重载流程。根目录 .env* 有明确 cold 规则,不能笼统说所有点开头文件均忽略;前端静态目录也会覆盖普通文本资源。
监听分类与编译排除是两层规则。 当前普通 src/**/*.test.ts、src/**/*.d.ts 等可能被监听器分类为 soft,但编译器排除这些输入;修改它们可能得到跳过/编译诊断,不能承诺触发有效的业务替换。测试宜放在项目 test/ 或 tests/,类型是否正确仍由 TypeScript 检查。
coldPatterns / ignorePatterns 是内部分类器扩展点,当前不能当作公开 config.dev 配置项使用。
防抖处理
默认 debounce: 0,不额外等待防抖窗口;操作仍需经过扫描、preflight 和串行队列。如需在快速连续保存时合并多次变更为一次重载,可通过 --debounce <ms> 开启防抖窗口。
例如:同时修改了 routes/users.ts(soft)和 config/default.ts(Tier 3),框架会执行一次 Tier 3 冷重启(包含所有变更)。
重载协调与失败恢复
前端监视目标来自本次已解析配置,支持自定义 frontend.root、页面/组件目录和 publicDir,包括 src/ 之外的项目内目录。原生监视和 polling 都覆盖静态资源的任意文件扩展名(如 robots.txt、PDF),以及后端 .mts/.cts 文件的增删事件。配置重启后会更新监视目标;前后端文件在同一批变化时分别触发对应构建流程。开发父进程只接收目录信息,不为监视再次执行配置 provider。
首次检查和启动前会建立监听基线,启动期间保存的变更会继续处理。文件保存、手动重启和故障恢复按顺序执行;连续保存按路径合并并保留最终的增删状态。读取目录失败时保留上次完整快照并诊断重试,原生监听降级为 polling 时保留积压变更。
重载完成以实际 worker 处理结果为准。编译失败时保留上次合法后端产物;运行态已修改后失败或无法确认 worker 结果时,需要冷重启恢复。终端 h 触发后端源码全量重载,r 触发冷重启,? 显示帮助;冷重启会等待旧 worker 退出和新 worker 完成初始化。退出后不再启动排队任务。
多个独立项目可以同时运行;同一真实项目根的 dev、build 和写入型 typegen 共享写入所有权,竞争命令会报告冲突。typegen --check 保持只读。每个服务仍需使用各自可用的端口;不要把一个服务的目录配置成另一个服务的输出目录。
vext dev 在启动和处理重载/重启前执行 dev preflight:
- 自动运行基础
typegen,同步.vext/types/*.generated.d.ts;TypeScript 项目还同步src/types/generated/index.d.ts - TypeScript 诊断默认异步运行,不等待它完成就继续启动或重载
- 如果基础 typegen 发现 blocking issue,则跳过本轮 reload / restart;如需让 TypeScript 语义诊断也阻塞,可使用
--strict-preflight
路由重载使用编译器的真实项目根、源码目录和输出目录;自定义更深的输出目录不会改变源码映射或清单位置。新处理器构建和缓存清理成功后才提交 .vext/manifest/routes.json,提交冲突会阻止替换并进入冷重启恢复。首次启动时,该清单先作为前端构建输入生成,因此判断服务就绪应使用启动完成回执,判断重载生效应使用实际重载结果。
soft reload 在编译与缓存失效前失败时,保留旧 handler 继续服务;缓存失效后,语言包、中间件、Service、Model 或路由装配失败,可能已经改变共享运行态,此时请求冷重启。恢复流程先停止可能混合运行态的 worker;若严格 preflight 阻止启动,服务会保持停止,修复后保存才重新启动。
前端构建失败会保留上一代有效产物;前后端混合变更只完成一部分时,后续恢复也可能升级为完整启动。终端发送成功不代表 worker 已完成;应等结果并检查实际请求。
TypeScript 支持
开发后端由 esbuild 转译,TypeScript 类型诊断使用项目本地编译器的 tsc --noEmit。默认先刷新基础 typegen,类型诊断异步运行;这意味着服务 ready 时,类型检查可能仍未结束或已经报告错误。
- 基础 typegen 有阻断项:本轮启动/重载停止。
- 普通类型错误:默认输出诊断,但不阻塞 ready 或 reload。
--strict-preflight:等待类型诊断,通过后才进入启动/重载。- 缺少 tsconfig 的项目会跳过该 TypeScript 检查;项目有配置时应安装可解析的本地 TypeScript。
也可设置 VEXT_DEV_STRICT_PREFLIGHT=1,具体 shell 写法见CLI。类型检查不替代测试、Lint 和生产构建验证;有 typecheck script 时,在提交或 CI 中显式运行 npm run typecheck。
esbuild 使用受支持的 tsconfig 编译选项,不能把它等同于实现所有 tsc 发射选项。开发/生产 Source Map 形态也不同,见构建的 Source Map 说明。
常见问题
修改后没有触发重载?
- 检查实际监听角色与项目根 — 后端 src、已配置的前端/资源目录和指定根文件各有规则,任意根外文件不会自动监听
- 区分监听与编译排除 — generated、测试和声明文件的边界见上文;Docker/NFS 可尝试
--poll - 检查终端输出 — 是否有错误信息(如语法错误导致编译失败)
热重载后行为不符合预期?
- 尝试手动重启 — 按
Ctrl+C停止后重新运行vext dev - 检查是否完成替换或进入恢复 — 不要手动删除运行中的
.vext/或修改已登记产物;需要干净进程时用r或重新启动命令 - 检查对象引用与副作用 — 受影响服务按模块失效范围更新,动态缓存的旧引用和外部副作用不自动回滚;必要时冷重启
冷重启太慢?
- 先定位启动阶段 — 用
--startup-profile查明耗时。将工作移到onReady()不会自动消除 ready 前的等待;必需依赖仍应在接收请求前完成 - 减少非必要初始化 — 在开发配置中关闭可选功能;插件是否支持禁用取决于自己的实现
- 使用
local.ts简化配置 — 本地开发时关闭不需要的功能(如限流、访问日志等)
端口占用怎么办?
冷重启时如果端口仍被占用,VextJS 不再承诺“内部自动重试直到恢复”。当前行为是按 --port-conflict / VEXT_PORT_CONFLICT 执行显式策略:
error(默认):直接失败prompt:交互式询问retry / kill / next / abortkill:尝试终止占用进程next:自动切换到下一个可用端口
如果接受改用其他端口,可显式选择:
如果需要手动排查占用进程:
与 Cluster 模式的关系
vext dev 使用一个开发父进程管理一个应用 worker,不启动生产 Cluster 的多个请求 worker。不能把“单个应用 worker”理解为系统中只有一个 Node 进程。
生产环境如需多进程,使用 vext start 配合 Cluster 配置:
Windows 上 vext reload 不支持该信号操作;发送成功也不等于滚动替换已完成。详见 CLI reload。
最佳实践
1. 充分利用 Tier 1/2
将大部分应用逻辑放在路由和服务层,可以让 Vext 更常使用定向重载路径。配置和插件变化较少时,再使用更稳妥的冷重启路径。
2. 配合 IDE 实时类型检查
虽然 vext dev 会输出 TypeScript 语义诊断,但 IDE 的实时类型检查仍然是最快的反馈来源。推荐同时开启 IDE 提示和 npm run typecheck;需要阻塞式 preflight 时再开启 strict 模式。
3. 开发环境简化配置
通过 development.ts 关闭生产环境才需要的功能,加速冷重启:
4. 使用 _ 前缀共享代码
路由和服务目录中以 _ 开头的文件不会被自动加载为路由/服务。修改这些工具文件时:
- 修改已有源码通常进入 T1;新增/删除通常进入 T2。
- 使用它的模块可沿反向依赖图进入失效集合;是否重建某个 Service 取决于该集合,不能仅凭工具文件位于 routes 或 services 判断。
_前缀是加载器命名约定,不表示编译器或监听器忽略该模块。
下一步
- 了解 CLI 命令 的完整用法
- 学习 Cluster 多进程 的生产环境部署
- 查看 配置 的环境覆盖机制
- 探索 测试 确保热重载后的代码正确性