Sails 请求体解析完全指南:读懂 `req.body` 的底层机制与最佳实践
Sails 请求体解析完全指南读懂req.body的底层机制与最佳实践【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails导读req.body是 SailsRealtime MVC Framework for Node.js中用于承载 HTTP 请求体文本参数的属性。本文将围绕 req.body 官方参考文档 的核心定义结合本仓库 HTTP 钩子、路由层与 request 钩子的真实源码深入解析req.body的数据来源、解析流程、与req.allParams()/req.param()的关系、文件上传场景下的行为边界以及如何通过中间件配置扩展对 XML 等自定义格式的支持帮助你在控制器、策略policies和自定义响应中安全、准确地读取请求体数据。一、req.body是什么按照 docs/reference/req/req.body.md 的定义An object containing text parameters from the parsed request body, defaulting to{}.即req.body是一个包含解析后请求体中文本参数的对象在无请求体时默认值为{}空对象。这一默认空对象行为并非文档口号而是有测试用例背书的实现事实。在 test/unit/req.test.js 中可以看到it(.body, function () { req.body.should.be.an.Object; req.body.should.be.empty; });测试明确断言req.body永远是一个对象且在未携带请求体时为空对象。1.1 使用方式req.body;在 Sails 中req.body是内置的、可直接访问的属性无需引入任何模块。它继承自 Express 与 Node HTTP 服务器的请求对象约定因此在控制器、策略、自定义响应、中间件中都可以直接读取。关于req对象的整体定位可参见 docs/reference/req/req.mdSails 构建在 Express 之上遵循 Node HTTP 服务器约定因此 Express/Node 上所有req的方法与属性在 Sails 中同样可用而req.body正是其中之一。1.2 一个最小可运行示例假设在config/routes.js中注册如下路由对应仓库默认路由配置体系参见 docs/anatomy/config/routes.js.mdmodule.exports.routes { POST /api/echo: { action: echo }, };对应的控制器动作api/controllers/echo.js可以这样读取请求体module.exports { friendlyName: Echo request body, fn: async function (inputs, exits) { // 直接读取解析后的请求体 const body this.req.body; return exits.success({ received: body }); }, };当客户端以POST /api/echo提交application/json请求体{name: sails}时this.req.body即为{ name: sails }。二、默认支持哪些请求体格式文档明确指出By default, the request body can be URL-encoded or stringified as JSON.Sails 默认的请求体解析器是Skipper。从 lib/hooks/http/get-configured-http-middleware-fns.js 的源码可以看到默认解析器的装配逻辑// Configures the middleware function used for parsing the HTTP request body, if enabled. bodyParser: (function() { var opts {}; var fn; // 如果配置了自定义 bodyParser则使用它 if (sails.config.http.bodyParser) { fn sails.config.http.bodyParser; return fn(opts); } else if (sails.config.http.bodyParser false) { // 显式禁用 return undefined; } // 默认使用内置 bodyParserSkipper fn require(skipper); return fn(opts); })(),也就是说在未做任何配置时Sails 会引入skipper作为默认 body parserSkipper 同时负责把文件上传参数处理为req.file()流。Skipper 接受以下两种常见的文本请求体URL-encodedapplication/x-www-form-urlencoded即表单提交格式如namesailsversion1.0JSON 字符串化application/json如{name: sails}。两者经解析后都会以普通 JavaScript 对象的形式呈现于req.body中。补充虚拟请求WebSocket / socket.io的简易解析在虚拟请求场景下即非 HTTP 的 socket.io 请求Sails 在 lib/router/index.js 中内置了一个极简 body parser对于GET/HEAD/DELETE方法直接返回空对象对其他方法则将 body 缓冲后尝试JSON.parse再合并进req.body// Extremely simple body parser (req.body) function bodyParser (req, res, next) { // 设置 mock 的 req.file()说明 HTTP 文件上传只在 Skipper 下可用 req.file function fileUploadsNotAvailable(){ return res.status(500).send(Streaming file uploads via req.file() are only available over HTTP with Skipper.); }; var bodyBuffer; if (req.method GET || req.method HEAD || req.method DELETE){ req.body _.extend({}, req.body); return next(); } // ...读取流、JSON.parse、合并 req.on(end, function() { var parsedBody; try { parsedBody JSON.parse(bodyBuffer); } catch (unusedErr) {} req.body _.merge(req.body, parsedBody); next(); }); }这段源码也揭示了两个细节其一Sails 对虚拟请求同样保证req.body为对象_.extend({}, ...)其二req.file()仅在 HTTP Skipper 场景下可用虚拟请求中调用会收到明确的 500 错误提示。三、req.body与req.allParams()、req.param()的关系在实际开发中req.body常与 Sails 提供的两个参数访问工具配合使用理解三者的边界可以避免很多取值陷阱。3.1req.allParams()路径参数 查询串 请求体的合并视图Sails 在 lib/hooks/request/params.all.js 中实现了req.allParams()req.allParams function () { // 合并查询串与请求体 var allParams _.extend({}, req.query, req.body); // 混入路由参数路径参数 _.each(Object.keys(req.params), function(paramName) { if (allParams[paramName] || !_.isUndefined(req.params[paramName])) { allParams[paramName] !_.isUndefined(req.params[paramName]) ? req.params[paramName] : allParams[paramName]; } }); return allParams; };req.body只包含请求体中的参数req.allParams()则是req.queryreq.body 路径参数req.params三者的合并视图。从合并顺序看请求体与查询串中重名参数以合并结果为准路由路径参数优先级最高。该方法在每个路由匹配前都会被重新计算见 lib/hooks/request/index.js 中对router:route事件的监听以保证路径参数变化时取到的是当前路由上下文下的正确值。3.2req.param()按名取值的三级回退Sails 在 lib/hooks/request/param.js 中提供了req.param(name, defaultValue)外观方法查找顺序为路由路径参数req.params[name]请求体参数req.body[name]查询串参数req.query[name]不存在则返回传入的默认值。req.param function(param, defaultValue) { if (typeof req.params[param] ! undefined) { return req.params[param]; } if (req.body typeof req.body[param] ! undefined) { return req.body[param]; } return typeof req.query[param] ! undefined ? req.query[param] : defaultValue; };因此若你只关心请求体里的某个字段直接读req.body[fieldName]是最精确的方式若希望无论参数来自路径、查询串还是请求体都能取到则使用req.param()或req.allParams()更省心。3.3 一个特殊的边界req.param(length)由于数组对象自带length属性Sails 在 lib/hooks/http/initialize.js 中为req.param(length)加了专门的 shim当路由地址形如/foo/bar/:length/baz时返回字符串形式的路径参数否则依次检查req.body.length、req.query.length。如果你恰好要提交名为length的表单字段需要注意这一特殊处理。四、文件上传场景下的行为边界文档给出了两条非常重要的注意事项直接关系到含文件上传请求的取值正确性If a request contains one or more file uploads, only the text parameters sentbeforethe first file parameter will be available inreq.body.When using Skipper, the default body parser, this property will beundefinedfor GET requests.4.1 文本参数必须位于文件参数之前当请求中同时包含文件上传与文本字段时典型如 multipart/form-data 表单只有出现在第一个文件字段之前的文本参数才会进入req.body位于文件字段之后的文本字段将无法通过req.body读取。这一点对前端表单字段的排列顺序提出了明确要求。以 HTML 表单为例form action/api/upload methodpost enctypemultipart/form-data !-- 文件之前的文本字段能进 req.body -- input typetext namecaption valuehello sails / input typefile nameavatar / !-- 文件之后的文本字段读不到 -- input typetext nameextraNote valuewill be lost / button typesubmitUpload/button /form上面的caption可以在控制器中通过req.body.caption读取而extraNote不会出现在req.body中。因此在设计多部分表单时应把所有需要读取的文本字段放在文件字段之前这是使用 Skipper 时的硬性约定。4.2 GET 请求下req.body为undefined第二条注意事项针对默认解析器 Skipper对 GET 请求req.body是undefined而非{}。这与 lib/router/index.js 中虚拟请求解析器对GET/HEAD/DELETE的处理行为恰好形成对照——虚拟请求解析器会显式将req.body归一化为空对象if (req.method GET || req.method HEAD || req.method DELETE){ req.body _.extend({}, req.body); return next(); }而 HTTP 场景下由 Skipper 处理时GET 请求的req.body保持undefined。这意味着你在控制器中读取req.body前应做好防御性判断例如fn: async function (inputs, exits) { const body this.req.body || {}; // 兼容 GET 请求的 undefined const name this.req.body this.req.body.name; // 或逐字段判空 return exits.success({ name: name || anonymous }); },提示如果你希望代码在不同方法、不同传输层HTTP 与 socket.io下行为一致推荐统一使用req.allParams()或req.param()它们内部已对req.body做了存在性判断而不是直接裸读req.body。五、如何支持自定义格式如 XML文档指出默认的 URL-encoded 与 JSON 之外的其他格式支持可以通过中间件middleware配置实现。5.1 中间件装配模型Sails 的 HTTP 中间件通过sails.config.http.middleware配置。从 lib/hooks/http/index.js 的默认配置可以看到中间件执行顺序http: { middleware: { order: [ cookieParser, session, bodyParser, compress, poweredBy, router, www, favicon, ], // 内置 HTTP 中间件函数会在 Express app 实例创建后被注入 }, },bodyParser位于session之后、router之前即在路由分发前完成请求体解析。你可以替换默认的bodyParser或在其后追加自定义解析中间件来处理 XML 等格式。5.2 在config/http.js中自定义Sails 1.x 推荐使用sails.config.http.middleware.bodyParser进行配置旧的sails.config.http.bodyParser已被标记为弃用见 lib/hooks/http/index.js 中的弃用日志与迁移处理。示例// config/http.js module.exports.http { middleware: { // 保持默认执行顺序含 bodyParser order: [ cookieParser, session, bodyParser, compress, poweredBy, router, www, favicon, ], // 自定义请求体解析器在默认 JSON/表单解析之后追加 XML 支持 bodyParser: (function() { const skipper require(skipper); return function (req, res, next) { // 1. 先用 Skipper 做默认解析 const originalBody req.body; skipper()(req, res, function (err) { if (err) { return next(err); } // 2. 判断是否为 XML const contentType req.headers[content-type] || ; if (contentType.indexOf(application/xml) ! -1 || contentType.indexOf(text/xml) ! -1) { // 3. 收集原始文本并解析 XML此处示意实际需引入 xml2js 等库 let raw ; req.on(data, (chunk) { raw chunk; }); req.on(end, () { // 伪代码parseXml(raw, (err, result) { ... }) // req.body result; next(); }); } else { next(); } }); }; })(), }, };说明上述代码为示意骨架真实项目中请使用成熟的 XML 解析库如xml2js完成字符串到对象的转换并注意req是流对象读取data/end事件后要正确衔接next()。你也可以选择在bodyParser之外追加一个自定义中间件把解析结果合并进req.body。5.3 解析失败时的行为Sails 为 body parser 提供了onBodyParserError回调见 lib/hooks/http/get-configured-http-middleware-fns.js当请求体解析失败时会在生产环境NODE_ENVproduction返回 400 空响应非生产环境则返回包含错误详情的 400 响应并记录错误日志opts.onBodyParserError function (err, req, res, next) { var bodyParserFailureErrorMsg Unable to parse HTTP body- error occurred :: util.inspect((errerr.stack)?err.stack:err, false, null); sails.log.error(bodyParserFailureErrorMsg); if (IS_NODE_ENV_PRODUCTION) { return res.status(400).send(); } return res.status(400).send(bodyParserFailureErrorMsg); };这一行为说明非法的 JSON 请求体不会导致进程崩溃而是以 400 Bad Request 响应客户端。六、req.body在 Blueprint 与 ORM 层中的角色req.body不仅在控制器中直接可用还深度参与 Sails 的 Blueprint蓝图机制。从源码可以确认其实际调用关系在 lib/hooks/blueprints/actionUtil.js 中创建/更新数据的 Blueprint 动作以req.body为数据源核心var bodyData _.isArray(req.body) ? req.body : [req.allParams()];——如果请求体本身就是数组批量操作则直接使用否则回退到req.allParams()其中已包含req.body。在 lib/hooks/blueprints/parse-blueprint-options.js 中关联集合collection的筛选条件会从req.body[attrName]读取associatedIds也会优先取req.body数组见该文件 L410。这意味着当你通过 Blueprint 路由如POST /user、PUT /user/3提交 JSON 时req.body就是最终写入数据库的字段来源通过 Blueprint 的 populate 关联查询时请求体数组也可以直接作为关联 ID 列表。理解req.body的解析规则等于理解了 Blueprint 数据入口的行为。七、常见问题与避坑清单场景现象建议GET 请求读req.body得到undefinedHTTP Skipper用req.body || {}防御或改用req.allParams()multipart 表单中文件字段之后的文本字段丢失读不到把文本字段全部放在第一个文件字段之前请求体是 JSON 数组req.body是数组而非对象判断Array.isArray(req.body)Blueprint 批量操作依赖此特性非法 JSON 请求体收到 400 响应前端先校验序列化结果非生产环境可在响应中看到具体错误需要解析 XML 等格式默认解析器不支持在config/http.js的middleware中自定义bodyParser想同时取路径、查询串、请求体参数逐个读取繁琐使用req.allParams()或req.param(name, default)提交名为length的字段req.param(length)行为特殊直接读req.body.length更可控Sails 有专门 shim见 lib/hooks/http/initialize.js八、进一步阅读请求对象整体说明docs/reference/req/req.md请求体参考文档docs/reference/req/req.body.md中间件与默认中间件概念docs/concepts/Middleware/Middleware.mdHTTP 配置含 middleware 顺序docs/reference/sails.config/sails.config.http.md请求体解析实现lib/hooks/http/get-configured-http-middleware-fns.js、lib/router/index.jsreq.allParams()与req.param()的实现lib/hooks/request/params.all.js、lib/hooks/request/param.js相关单元测试test/unit/req.test.js【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考