尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Sails 错误处理架构全解:深入 errors/ 目录的致命错误与告警机制

Sails 错误处理架构全解:深入 errors/ 目录的致命错误与告警机制 Sails 错误处理架构全解深入 errors/ 目录的致命错误与告警机制【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails导读本文以 SailsRealtime MVC Framework for Node.js源码仓库中的 errors/ 目录为切入点系统剖析 Sails 框架在应用启动lift/load阶段如何集中管理与输出致命错误Fatal Errors与警告Warnings。你将了解到 Sails 内部如何通过fatal.js与warn.js两个模块统一组织错误信息、何时调用process.exit()终止进程、如何识别userError与核心钩子错误并决定是否打印堆栈以及这些错误处理器在isLocalSailsValid、policies钩子、responses钩子中的真实调用链。读完本文你将具备阅读 Sails 源码错误路径、定位启动失败根因、理解框架设计取舍的能力。一、errors/ 目录的定位与整体架构Sails 的 errors/ 目录是一个集中式错误出口所有需要在进程层面提示用户、甚至直接终止进程的错误输出都被收敛到这里。目录入口文件 errors/index.js 只有寥寥几行// Merge together error sub-modules module.exports { fatal: require(./fatal), warn: require(./warn) };它把两个子模块合并成一个命名空间对外暴露两个能力fatalerrors/fatal.js致命错误输出错误信息后通常直接终止进程process.exit(1)warnerrors/warn.js警告仅打印告警日志不中断流程。从源码结构看这两个模块都遵循同一套初始化模式模块被require时通过require(../lib/app/configuration/rc)()读取运行配置再交给captains-log构建 logger 实例fatal.js第 411 行、warn.js第 412 行。也就是说错误输出的格式、级别、着色等行为都统一由 Sails 的日志配置sails.config.log控制。注意errors/目录本身是一个过渡性设计。目录内的 README 明确记录了作者的 TODO 规划最终希望把这些错误信息内联inline到各自的使用现场例如lib/app/load.js、lib/app/private/loadHooks.js中然后删除整个errors/目录。因此你在较新版本的 Sails 中可能看到错误处理逻辑被分散到了调用点附近但当前仓库中errors/仍是最完整的集中式错误出口。二、致命错误fatal.js何时终止进程fatal.js 中每一个导出函数都代表一类会让 Sails 无法继续运行的场景。它们大多在打印错误后调用_terminateProcess(1)退出进程。逐一拆解如下2.1 启动与加载期错误函数触发场景行为failedToLoadSails(err)Sails 应用加载/启动失败区分userError与内部错误打印错误后给出排查 Tips 并退出noPackageJSON()当前目录没有package.json提示这真的是一个 Sails 应用吗并退出notSailsApp()package.json未将 Sails 列为依赖提示目录不是 Sails 应用并退出badLocalDependency(path, version)本地安装的 Sails 的package.json损坏或缺失提示重装命令并退出malformedHook()钩子定义格式错误提示钩子应是一个接收sails参数的函数并退出hooksTookTooLong()钩子初始化超时提示检查自定义钩子initialize()是否调用了回调并退出其中failedToLoadSails是最具代表性的分级打印逻辑fatal.js第 2052 行failedToLoadSails: function(err) { log.error(); // 用户可修复的错误userError只打印错误消息不打印堆栈 if (err.name err.name userError) { log.error(err.message); } else { // 已识别的加载期错误码只打印消息 switch (err.code) { case include-all:COULD_NOT_REQUIRE: case E_COULD_NOT_LOAD_ADAPTER: case E_ADAPTER_NOT_INSTALLED: case E_BIND_ERR: log.error(err.message); break; default: log.error(err); // 其余情况打印完整错误对象含堆栈 } } console.error(); log.error(Could not load Sails app.); log.error(Tips:); log.error( • First, take a look at the error message above.); log.error( • Make sure you\ve installed dependencies with npm install.); log.error( • Check that this app was built for a compatible version of Sails.); log.error( • Have a question or need help? (http://sailsjs.com/support)); _terminateProcess(1); },这段代码体现了一个重要的设计原则区分用户错误与框架内部 Bug。如果是用户可修复的错误err.name userError或已被 Sails 识别的加载期错误码如适配器加载失败E_COULD_NOT_LOAD_ADAPTER、端口绑定失败E_BIND_ERR只打印精简的错误消息避免把指向 Sails 内核的完整堆栈甩给新手只有无法识别的错误才打印完整错误对象方便开发者向框架反馈 Bug。2.2 用户模块错误函数触发场景行为invalidCustomResponse(responseIdentity)自定义 response 的名称与 Connect/Express/Sails 保留字冲突提示移除该文件并退出__UnknownPolicy__(policy, source, pathToPolicies)config.policies或路由中引用了不存在的策略提示策略应在的路径并退出__InvalidConnection__(connection, sourceModelId)模型中的 connection 缺少adapter键提示 connection 必须含adapter并退出__UnknownConnection__(connectionId, sourceModelId)模型引用了未定义的 connection提示应在sails.config.connections中定义并退出__ModelIsMissingConnection__(sourceModelId)模型未配置 connection提示检查config/models.js中的默认 connection 并退出__UnknownAdapter__(adapterId, sourceModelId)模型使用了未安装的适配器提示安装sails-adapter或检查自定义适配器并退出__InvalidAdapter__(moduleName, err)require适配器模块时抛错提示该模块不是合法的 Sails/Waterline 适配器并退出注意这些函数名的__双下划线前缀——从源码结构看这是 Sails 内部用于准私有/内部错误的命名约定与公开 API 的错误命名区分开。其中__UnknownAdapter__展示了 Sails 对用户一键修复的引导能力fatal.js第 140152 行__UnknownAdapter__: function(adapterId, sourceModelId) { log.error(Trying to use unknown adapter, adapterId , in model sourceModelId .); log.error(Are you sure that adapter is installed in this Sails app?); log.error(If you wrote a custom adapter with identity adapterId , it should be in this app\s adapters directory.); var probableAdapterModuleName adapterId.toLowerCase(); if (!probableAdapterModuleName.match(/^(sails-|waterline-)/)) { probableAdapterModuleName sails- probableAdapterModuleName; } log.error(Otherwise, if you\re trying to use an adapter named adapterId , please run npm install probableAdapterModuleName --save); return _terminateProcess(1); },它会把模型里写的adapterId自动补全为sails-adapterId形式的 npm 包名若尚未带sails-或waterline-前缀并给出可直接执行的npm install ... --save命令——这是 Sails 提升开发者体验的典型手法。2.3 进程终止机制_terminateProcess所有致命错误最终都汇入_terminateProcessfatal.js第 181207 行function _terminateProcess(code) { // 测试环境下不退出而是抛错方便测试断言捕获 if (process.env.NODE_ENV test) { throw new Error({ type: terminate, code: code, }); } return process.exit(code); }两个关键点测试模式豁免当NODE_ENV test时致命错误不会真的process.exit(1)而是抛出{ type: terminate, code: code }异常让测试框架能捕获并断言。这正是 Sails 在跑自身测试套件时不会中途退出进程的原因。源码注释的演进痕迹代码注释fatal.js第 165206 行记录了该机制的演进史——早期版本曾将附加选项塞进抛出的错误对象Sails v12016 年 12 月的提交将其移除因为反正也没用。这提示阅读者opts参数已被弃用。三、警告warn.js不中断流程的提醒与致命错误不同warn.js 只负责提醒输出后流程继续。它包含三组警告函数触发场景行为incompatibleLocalSails(requiredVersion, localVersion)本地安装的 Sails 版本不满足package.json的版本要求警告并建议npm install sailsrequiredVersionnoPackageJSON()当前目录无package.jsonverbose 级别提示可能不是 Sails 应用notSailsApp()package.json未列出 Sails 依赖verbose 级别提示可能不是 Sails 应用badLocalDependency(pathToLocalSails, requiredVersion)本地 Sails 的package.json损坏/缺失建议rm -rf path npm install sailsversion其中incompatibleLocalSails是最常被触发的场景——当你在一个依赖sails^1.0.0的项目里意外用全局或错误版本的本地 Sails 执行sails lift时会看到类似下面的提示warn.js第 2031 行incompatibleLocalSails: function(requiredVersion, localVersion) { log.warn(Trying to lift app using a local copy of sails); log.warn((located in nodepath.resolve(process.cwd(), node_modules/sails) )); log.warn(But the package.json in the current directory indicates a dependency); log.warn(on Sails requiredVersion , and the locally installed Sails is localVersion !); log.warn(If you run into compatibility issues, try installing requiredVersion locally:); log.warn( $ npm install sails requiredVersion); log.blank(); },源码注释还将noPackageJSON、notSailsApp标注为仅 verbose 级别输出的警告warn.js第 35 行说明它们的触发并不致命框架会继续尝试用其他方式判断只有日志级别足够细时才会打扰用户。四、真实调用链errors/ 在框架中的使用现场光看模块定义还不够接下来沿源码把errors/的调用点逐一走通印证它在框架中的真实作用。4.1isLocalSailsValid启动前的体检lib/app/private/isLocalSailsValid.js 是errors/的第一大消费方通过var Err require(../../../errors)引入整个命名空间。它在应用启动前对本地 Sails 安装做一次完整性校验流程如下package.json缺失→Err.warn.noPackageJSON()第 30 行package.json解析失败或未列出sails依赖→Err.warn.notSailsApp()第 37、46 行本地 Sails 的package.json损坏→Err.warn.badLocalDependency(sailsPath, appDependencies.sails)第 63 行版本不匹配→ 用semver.satisfies比对后触发Err.warn.incompatibleLocalSails(requiredSailsVersion, sailsPackageJSON.version)第 105 行。这里有一个值得注意的细节同一类问题如没有 package.json在warn.js中是警告、在fatal.js中是致命错误两者并存。从代码结构看这属于历史过渡状态——校验逻辑对不同的调用路径同步校验 vs 启动加载给出了不同的严重级别具体触发哪条路径取决于调用方的上下文。4.2 policies 钩子未知策略即致命lib/hooks/policies/index.js 通过require(../../../errors)引入错误模块在route:typeUnknown事件处理器中校验路由上显式声明的policy第 306313 行var policyId event.target.policy.toLowerCase(); // Policy doesnt exist if (!sails.hooks.policies.middleware[policyId] ) { var routeAddrToDisplay event.verb ? event.verb event.path : event.path; Err.fatal.__UnknownPolicy__ (policyId, routeAddrToDisplay, sails.config.paths.policies); // ^^That begins terminating the process. return; }也就是说在config/routes.js中给路由挂了policy: xxx但api/policies/xxx.js不存在时Sails 会在启动阶段直接终止进程__UnknownPolicy__内部调用_terminateProcess(1)而不是把错误留到请求到达时。这样设计的好处是尽早暴露配置错误避免线上运行时才发现策略缺失。4.3 responses 钩子保留字冲突即致命lib/hooks/responses/index.js 引入的是单模块require(../../../errors/fatal)在加载自定义 response 时会把用户自定义的 response 名与 Connect/Express/Sails 的保留字比对第 8494 行var reservedResKeys [ view, status, set, get, cookie, clearCookie, redirect, location, charset, send, json, jsonp, type, format, attachment, sendfile, download, links, locals, render ]; _.each(responseDefs, function (responseDef, customResponseKey) { if ( _.contains(reservedResKeys, customResponseKey) ) { Err.invalidCustomResponse(customResponseKey); } });例如你在api/responses/下新建了一个名为redirect.js的文件试图自定义res.redirect()——这会被判定为非法invalidCustomResponse致命错误因为res.redirect等 22 个方法名在 Connect/Express/Sails 语义中具有特殊含义覆盖它们会破坏框架的核心行为。随后该钩子用_.defaults把内置的ok、negotiate、notFound、serverError、forbidden、badRequest六个默认 response 与用户自定义项合并第 97104 行。4.4 触发时机总结调用点触发错误阶段lib/app/private/isLocalSailsValid.jswarn.*系列启动前校验lib/hooks/policies/index.jsfatal.__UnknownPolicy__启动期路由绑定lib/hooks/responses/index.jsfatal.invalidCustomResponse启动期 response 加载三处调用点全部位于应用启动/加载阶段这与errors/目录的定位完全一致Sails 把启动期就能发现的问题集中在此处做统一、友好的错误输出而运行期请求处理阶段的错误则交由 lib/hooks/responses/defaults/ 下的默认响应serverError、notFound、badRequest等处理。五、设计思想与演进方向通读errors/及其调用点可以提炼出 Sails 错误处理的几个核心设计思想把错误出口集中化所有进程级错误提示集中在errors/命名空间避免每个模块各自拼日志、各自调process.exit保证错误文案风格统一。区分错误类型与用户画像userError/ 已识别错误码只打印消息未知错误打印完整堆栈——面向新手友好同时不牺牲面向框架开发者的可调试性。尽早失败Fail Fast策略缺失、response 保留字冲突这类配置错误在启动期就终止进程而不是留到运行时炸掉请求。测试友好_terminateProcess在NODE_ENV test时改抛异常让 Sails 自身的测试套件见 test/ 目录如 test/unit/ 与 test/integration/可以在不退出进程的前提下断言致命错误路径。版本兼容性提醒而非阻断本地 Sails 版本不匹配时只给警告warn.incompatibleLocalSails因为 Sails 历史上大量项目用git://、latest、beta、edge等非常规版本声明isLocalSailsValid.js第 7096 行专门处理了这些特例强行阻断会误伤。关于演进方向errors/README.md 的 TODO 写得很直白将错误消息内联inline进使用现场替代字符串文件sails-stringfile在 v1 之后才值得引入尽量不调用process.exit()而是统一调用sails.lower()优雅关闭——不过由于已有进程信号处理器某些场景下直接退出也无可厚非关键是错误处理取决于上下文所以需要内联把这些错误内联到各自的使用点后整个errors/目录即可删除。这解释了为什么当前仓库中fatal.js里仍残留// FUTURE: inline this error注释如malformedHook、hooksTookTooLong也提醒读者错误处理逻辑散落到调用点是 Sails 后续版本的设计方向理解这一点有助于你阅读不同版本的 Sails 源码。六、小结errors/目录虽小却是理解 Sails 启动期错误处理哲学的钥匙命名空间errors/index.js 统一导出fatal与warn两组能力致命错误errors/fatal.js 覆盖加载失败、非法用户模块、未知策略/连接/适配器 15 类场景统一经_terminateProcess退出测试环境抛异常警告errors/warn.js 负责版本不匹配、目录非 Sails 应用等提醒不中断流程真实调用启动前校验isLocalSailsValid、策略绑定policies钩子、自定义 response 加载responses钩子三处调用点共同构成了 Sails 的启动期错误防线。当你下次遇到 Sails 应用sails lift启动失败时先对照errors/中列出的错误文案定位所属类别再回到对应调用点排查配置会比盲目搜堆栈高效得多。若你正在为 Sails 贡献代码errors/README.md中内联错误、删除本目录的 TODO 也是一个明确的方向性指引。【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表