# 聊聊 Remix 的错误边界不只是捕获错误那么简单在构建现代 Web 应用时错误处理往往被放在次要位置直到线上出了问题才匆忙补救。Remix 框架对错误边界的设计却让人感受到一种不同的思考方式——它不仅仅是技术实现更是一种用户体验的哲学。错误边界在 Remix 中到底是什么如果你熟悉 React 的错误边界概念可能会觉得 Remix 的版本应该大同小异。但实际上Remix 将这个概念扩展到了整个数据流层面。它不仅仅是组件层级的错误捕获而是贯穿了路由、加载器、动作和渲染的全链路错误处理机制。想象一下传统 Web 开发中的错误页面——通常是服务器返回一个 500 状态码然后显示一个冷冰冰的错误提示。Remix 的错误边界更像是给应用穿上了一件分层的防护服每一层都有自己的保护机制外层失效时内层还能继续工作。它能解决哪些实际问题最直接的作用当然是防止整个应用因为局部错误而崩溃。但 Remix 的错误边界做得更细致一些。比如某个页面的数据加载失败了这个页面会显示错误状态但应用的导航栏、侧边栏这些共享组件依然可以正常交互。用户不会被困在一个完全空白的页面里。另一个容易被忽视的作用是错误恢复。在某些框架中一旦组件抛出错误可能需要刷新整个页面才能恢复正常。Remix 的错误边界允许在错误解决后用户可以通过简单的交互比如点击重试按钮重新尝试失败的操作而不丢失当前的应用状态。还有错误上报的便利性。由于错误被边界明确地捕获和处理开发者可以更精确地知道错误发生在哪个路由、哪个数据加载环节而不是在全局错误监控中看到一堆模糊的调用栈信息。具体怎么用起来在 Remix 中定义错误边界出奇地简单。每个路由文件都可以导出一个ErrorBoundary组件当该路由或其子组件抛出错误时这个组件就会被渲染。// app/routes/products/$id.jsx import { useRouteError } from remix-run/react; export function ErrorBoundary() { const error useRouteError(); return ( div classNameerror-container h1产品信息加载失败/h1 p{error.message}/p a href/products返回产品列表/a /div ); }这里有个细节值得注意ErrorBoundary组件本身应该尽可能简单可靠避免在这个组件里再做复杂的数据获取或状态管理否则可能陷入无限错误的循环。对于数据加载错误Remix 提供了更精细的控制。在加载器函数中可以抛出带有特定状态码的响应错误边界会接收到这个响应对象从而可以显示不同的错误界面。export async function loader({ params }) { const product await getProduct(params.id); if (!product) { throw new Response(产品不存在, { status: 404, statusText: Not Found }); } return json(product); }一些实践中的经验在项目中实际使用一段时间后会发现一些值得注意的地方。比如错误边界的层级关系很重要。Remix 会沿着路由树向上寻找最近的错误边界这意味着你可以在父路由设置一个兜底的错误处理在子路由设置更具体的错误处理。错误信息的展示需要平衡。给用户的信息要友好但给开发者的信息要详细。一种常见的做法是在开发环境下显示完整的错误堆栈在生产环境下显示简化后的用户友好提示。错误边界不应该只处理意外错误还应该处理预期的“错误”状态。比如 API 返回的业务逻辑错误、表单验证失败等这些都可以通过错误边界来统一处理保持用户体验的一致性。还有一个容易被忽略的点错误边界的样式应该与应用的整体设计语言保持一致。一个风格突兀的错误页面会让用户感觉更加不安。和其他方案的对比与传统的 React 错误边界相比Remix 的版本更加“框架化”。React 的错误边界是纯客户端的概念而 Remix 将其扩展到了服务端渲染和数据加载的全过程。这意味着在服务端渲染时发生的错误也能被优雅地处理不会导致整个页面渲染失败。和 Next.js 的错误处理相比Remix 的做法更加显式和结构化。Next.js 也有类似的概念但 Remix 通过强制要求每个路由都可以定义错误边界使得错误处理成为开发时必须考虑的一部分而不是可选的附加功能。与那些在全局统一处理错误的方案相比Remix 的层级式错误边界提供了更细粒度的控制。不同的路由可以有不同的错误处理逻辑不同重要性的错误可以有不同的处理方式。这种设计反映了现实世界的复杂性——不是所有错误都需要相同的处理方式。## 关于 pnpm monorepo 的一些实践与思考最近几年前端工程化的演进速度确实很快。从早期的 npm 和 Yarn到后来 workspace 概念的普及再到如今 pnpm 带来的新思路工具链的每一次变化背后都对应着实际开发中遇到的痛点。今天想聊聊 pnpm 对 monorepo 的支持这不仅仅是多了一个选择更是在依赖管理和项目组织方式上的一次实质性改进。它到底是什么简单来说pnpm 是一个包管理工具而它对 monorepo 的支持主要体现在其内置的 workspace 功能上。但这样说可能过于抽象了。可以把它想象成一个更智能的“项目文件夹管理器”——它允许你在一个大的代码仓库里管理多个相互关联的包或应用同时用一种非常节省空间的方式处理它们的依赖关系。传统的 monorepo 方案比如用 lerna 配合 npm 或 Yarn虽然也能管理多个包但依赖安装往往是各自为政的。每个子包都有自己的 node_modules即使它们依赖了同一个库的同一个版本也会在磁盘上重复存储很多份。pnpm 的做法不同它采用了“内容寻址存储”和“硬链接”的方式。所有依赖都会被统一存储在一个全局的仓库里项目中的 node_modules 里放的实际上是指向这个全局仓库的链接。这就好比家里有很多本书传统做法是每个房间都放一套相同的百科全书而 pnpm 的做法是在书房放一套实体书每个房间只放一张写着“百科全书在书房第几个书架”的卡片。它能解决什么问题pnpm monorepo 主要解决的是两个层面的问题磁盘空间和依赖一致性。在大型 monorepo 中几十甚至上百个子包很常见。如果每个包都完整安装自己的依赖node_modules 的总大小会非常惊人可能轻松达到几十GB。这不仅占用硬盘空间还会影响安装速度、备份效率甚至 IDE 索引的性能。pnpm 通过共享存储通常能将总空间占用减少一半以上。曾经有个项目从 Yarn workspace 迁移到 pnpm 后整个仓库的 node_modules 从 40GB 降到了不到 15GB效果非常直观。另一个更隐蔽但更重要的问题是“幽灵依赖”和依赖版本不一致。在传统的扁平化 node_modules 结构下一个包可能会意外地访问到不属于其直接依赖的模块因为那个模块被提升到了顶层。这就像在图书馆里你本来只能借自己登记的书但因为书架管理混乱你顺手把别人订的书也拿走了短期内可能没问题但一旦图书馆重新整理你的程序就“借不到”那本书了。pnpm 的 node_modules 结构是严格的、非扁平的每个包只能看到自己明确声明的依赖彻底杜绝了幽灵依赖。同时由于所有依赖都从全局存储链接而来可以严格保证同一个版本库在 monorepo 的任何地方都是同一份物理文件避免了因拷贝产生的细微差异。具体怎么用起来使用 pnpm monorepo 并不复杂。首先需要在项目根目录下创建一个pnpm-workspace.yaml文件这个文件定义了工作空间的根目录。通常里面就一行内容比如packages: - packages/*意思是所有在 packages 文件夹下的子目录都会被识别为工作空间内的一个包。每个子包都有自己的package.json就像普通的 npm 包一样。在根目录下运行pnpm installpnpm 会识别所有子包的依赖统一解析然后从全局存储创建链接到各个子包的 node_modules 里。如果要在子包之间添加本地依赖不需要发布到 npm直接用pnpm add your-scope/package-a --filter package-b这样的命令或者手动在 package-b 的 package.json 里写上your-scope/package-a: workspace:*即可。这个workspace:*协议是 pnpm workspace 的一个关键它告诉 pnpm 这个依赖指向的是工作空间内的另一个本地包而不是外部的 npm 包。运行命令时可以用--filter参数来指定在哪个或哪些子包上执行。例如pnpm run build --filter your-scope/ui-components就只构建 UI 组件库。这对于大型 monorepo 的日常操作非常方便。一些值得注意的实践细节在实际项目中有一些细节处理好了会让体验更顺畅。目录结构的设计很关键除了常见的packages/放所有包很多人也会区分apps/存放可独立运行的应用和libs/存放共享库这取决于项目的复杂度和团队习惯。在根目录的package.json里定义一些全局脚本是个好习惯比如统一的构建、测试、代码检查命令方便一键操作。依赖管理上建议尽量将公共依赖提升到根目录的package.json中声明。这不仅能进一步减少重复还能统一版本。可以使用pnpm的-w标志在根目录安装依赖比如pnpm add -wD typescript eslint。对于子包独有的依赖再在各自的 package.json 里安装。版本发布和变更管理是 monorepo 的一个挑战。pnpm 本身不提供发布工具但可以很好地与changesets或lerna这类工具配合。changesets的理念现在更受青睐它通过开发者手动声明变更CHANGELOG然后集中计算版本号和发布流程更清晰可控。在 CI/CD 流程中可以结合pnpm -r递归执行命令和--filter来优化构建流水线只构建和测试那些代码发生变更的子包及其依赖能节省大量计算资源。和同类方案的对比最后不可避免地要和其他工具做个比较。最常被拿来对比的是 Yarn workspace 和基于 lerna 的方案。Yarn workspace 出现得更早生态集成不错很多工具都优先支持它。它的使用也很简单。但它的依赖管理还是基于 Yarn 传统的扁平化 node_modules前面提到的磁盘空间和幽灵依赖问题依然存在。在安装速度和严格性上pnpm 更有优势。Lerna 本身不是一个包管理器它更像一个 monorepo 的“工作流管理器”负责版本发布、包链接等高层操作底层依赖安装通常交给 npm 或 Yarn。所以“lerna vs pnpm”这个对比不太对等。事实上现在很多人用lerna配合pnpm作为后端用 lerna 的发布流程用 pnpm 的安装和链接能力两者可以互补。npm 从 v7 开始也内置了 workspace 功能但整体上它的安装速度和磁盘效率依然不如 pnpm社区工具链的适配也还在逐步完善中。选择哪个往往取决于团队现有的技术栈和具体的痛点。如果项目庞大对磁盘空间和安装速度敏感或者深受幽灵依赖困扰那么 pnpm 的收益会非常明显。如果项目规模不大或者已经深度绑定了 Yarn 的生态那么迁移的必要性可能就没那么强。工具终究是为人和项目服务的解决真问题比追逐新技术更重要。pnpm monorepo 提供了一种在依赖管理上更严谨、更高效的思路这或许是它最核心的价值所在。最后想说的是错误边界的设计其实反映了框架对“健壮性”的理解。有些框架追求的是“尽量不崩溃”而 Remix 似乎更倾向于“即使部分崩溃也要保持最大程度的可用性”。这种设计哲学上的差异在实际项目中会产生深远的影响。好的错误处理就像建筑中的抗震设计——平时看不见关键时刻却能决定整个系统的命运。Remix 在这方面的思考值得每个前端开发者仔细品味。