Inertia.js 静默破坏你的应用
Source: Dev.to
TL;DR
在一个生产环境的 Laravel 12 + React 19 + Inertia v2 应用中工作了数周后,我反复遇到一些诊断成本高的故障模式:
- 重叠访问取消
- 部署时陈旧块破坏
- 默认失败用户体验薄弱
- 框架特定的变通代码
Note: 这 不是 在声称 Inertia 在每个项目中都会失败。很多团队在 CRUD 密集的后台管理应用中成功使用 Inertia。
这里的论点是,在一个拥有活跃用户和频繁部署的真实生产环境中,Inertia 的路由抽象导致了反复出现的运维痛点和不易察觉的故障模式。
环境概览
| 组件 | 版本 |
|---|---|
| Laravel | 12 |
| React | 19 |
| Inertia.js | v2 |
| Bundler | Vite (code‑splitting) |
| Deploy | 某些环境下的就地替换式部署 |
Inertia 的核心卖点很强大:在不维护单独公共 API 层的情况下构建类似 SPA 的用户体验,以支持常规网页导航。
问题出现在工作流变得非平凡时:多步骤操作、部署频繁以及边缘情况的错误处理。
1️⃣ await 不 Serialize Inertia Router Visits
在我们的应用中,指派工作人员需要两个有序的操作:
const handleAssign = async () => {
// Step 1: Assign worker
await router.put(`/admin/tasks/${task.id}/assign`, {
assignee_id: Number(selectedUserId)
})
// Step 2: Update status
await router.put(`/admin/tasks/${task.id}/status`, {
status: 'In Progress'
})
setModalOpen(false)
}
使用基于 Promise 的客户端(fetch、axios)时,这种写法意味着严格的顺序执行。
在我们的案例中,观察到的结果是:
status已更新assignment未 更新- 第一个请求在 Network 面板中显示为 cancelled
- 默认情况下没有显式的应用层错误出现
为什么会出现这种情况
- Inertia 路由方法 并不是返回 Promise,代码的假设不成立。
- 因此
await并不能保证请求完成的顺序。 - 重叠的访问会 取消之前的访问(这是设计如此)。
社区讨论: 在多年请求后,Promise 支持被有意移除。
可行的模式
回调链
const handleAssign = () => {
router.put(`/admin/tasks/${task.id}/assign`, {
assignee_id: Number(selectedUserId)
}, {
onSuccess: () => {
router.put(`/admin/tasks/${task.id}/status`, {
status: 'In Progress'
}, {
onSuccess: () => setModalOpen(false)
})
}
})
}
手动将访问包装在 Promise 中
await new Promise((resolve, reject) => {
router.patch(route('profile.update'), data, {
onSuccess: resolve,
onError: reject,
})
})
这正是让人沮丧的地方:看起来像普通
async/awaitHTTP 的代码 并不是普通的async/awaitHTTP。
Source: …
2️⃣ 部署后出现的陈旧块(Stale Chunk)问题
任何使用代码拆分的 SPA 在部署后都可能出现陈旧块问题——这 并非 Inertia 独有。
Inertia 之所以影响更广,是因为导航依赖 服务器端组件解析 加上 客户端块导入。
代表性的块名称
assets/bookings-show-A3f8kQ2.js
assets/profile-Bp7mXn1.js
assets/schedule-Ck9pLw4.js
部署后会发生什么
- 服务器引用 最新的组件清单。
- 客户端标签页可能仍持有旧的运行时假设。
- 如果资源不再可用,所需的块导入会失败。
- 用户会感知到“死掉”的导航,直到强制重新加载。
关键细节
- 不可变制品 / 防倾斜平台 降低 影响。
- 原位替换部署 增加 风险窗口。
- 缓存和发布策略与框架选择同等重要。
参考资料: Inertia 资产版本控制 / 409,409 循环报告。
重要说明: 我并不是说每次部署都会在所有环境中导致每个标签页失效。我是在说明这在我们环境中是一个 重复出现的生产事故模式。
我们添加的防护措施
// 捕获导航异常并强制重新加载
router.on('exception', (event) => {
event.preventDefault()
window.location.href = event.detail.url || window.location.href
})
// 主动检查清单漂移
let manifest = null
fetch('/build/manifest.json')
.then(res => res.text())
.then(text => { manifest = text })
.catch(() => {})
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState !== 'visible' || !manifest) return
try {
const res = await fetch('/build/manifest.json', { cache: 'no-store' })
if (await res.text() !== manifest) window.location.reload()
} catch {}
})
这些缓解措施 有效,但它们是 框架特定的运营债务,你必须了解才能编写相应代码。
3️⃣ JavaScript 错误导致的静默导航失败
当目标页面组件出现 JavaScript 错误时,导航 会静默失败:
- 前一页仍然可见。
- 没有错误信息,没有控制台警告,也没有停止的加载指示器。
- 用户点击链接,等待,却没有任何反应。
服务器端错误
Inertia 的默认行为是将 整个错误响应渲染在模态覆盖层中:
- 开发环境: 完整的 Laravel 调试页面以模态形式显示在应用之上。
- 生产环境: 通用的 HTML 错误页面——仍然在模态框中,仍然是奇怪的用户体验。
解决方案: 覆盖异常处理器以返回 JSON,然后在客户端使用 toast 通知捕获它(需要更多的变通代码)。
4️⃣ 混合导航策略
- 后者表明开发者在某些流程中放弃了 Inertia 的路由器。
- 这种分裂可能是有道理的,但也意味着工程师必须学习 两种交互模式。
5️⃣ Prop 过度共享(安全)
这 不是 仅针对 Inertia 的安全问题——任何客户端交付的数据在客户端都是可见的。
然而,在 Inertia 中,序列化到 data-page 的 props 如果团队不小心,就很容易导致过度共享。
参考资料: 页面源代码中可见的 props,注销后缓存的敏感数据。
可辩护的声明
将每个 prop 视为公共输出;绝不在客户端负载中包含您不想公开的数据。
结束语
- 该营销语在早期可能有用:更少的网页导航部件。
- 在许多实际系统中,团队仍会因上述运营债务而添加显式的 API 层或回退方案。
如果您考虑在生产环境的 SPA 中使用 Inertia,请注意:
- 访问取消语义 – 不要依赖
await。 - 部署时块漂移 – 实施清单检查或使用不可变部署。
- 默认错误用户体验 – 计划覆盖它。
- 导航一致性 – 选择一种路由策略并坚持使用。
- 属性卫生 – 假设发送到客户端的所有内容都是公开的。
提前了解这些模式可以节省数周的调试时间,并帮助您判断 Inertia 的权衡是否符合项目的运营约束。
端点用于
- 第三方集成和 Webhook
- 移动客户端
- 后台工作流
- 专门的、强顺序交互
重要的准确性更正
- Inertia 支持文件上传和 FormData 模式。
- 我们的团队仍在某些上传路径中使用直接
fetch(),出于本地可靠性/可控性的考虑。 - 这是一种 项目层面的权衡,并不意味着 Inertia 不能上传文件。
反复出现的成本是 语义不匹配:
- 代码 看起来像普通的基于 Promise 的 HTTP 流程。
- 运行时行为 却遵循 router‑visit 的语义。
该问题在生产环境下显现,而不是在演示的顺畅路径中。
这种不匹配消耗了调试时间,并需要超出大多数开发者对 “简单 SPA 路由” 期望的防御性模式。
为什么显式 HTTP 更适合关键的有序操作
const handleAssign = async () => {
await fetch(`/api/tasks/${task.id}/assign`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ assignee_id: Number(selectedUserId) })
})
await fetch(`/api/tasks/${task.id}/status`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'In Progress' })
})
setModalOpen(false)
}
- 这 不是 为了减少代码行数。
- 而是为了 可预测的行为、符合标准工具的预期,以及在不同后端之间的可移植性。
对框架的挫败感
- 请求取消 bug → 消耗了一整天的调试时间。
- 部署问题 → 又花费了一个下午。
两者都可以解决,但需要 框架特定的防御性代码,本不该是必须的。
结论
可以防御的结论是 不是 “永远不要使用 Inertia”。
许多 Laravel 管理后台和内部工具都在使用它且没有问题。
关键点:
- 如果你的系统拥有 多步骤交互、活跃用户的部署 churn,以及 严格的运营可靠性 需求,请评估 显式 API + 标准 HTTP 客户端语义 是否能降低长期风险。
在我们的案例中,答案是 毫无歧义 的。
关于我
我在 CodeCrank 构建 MVP。
如果你正在为下一个项目评估技术栈,欢迎交流。