# GET请求数据获取
30 秒速记
GET参数在URL查询串里:ctx.query拿对象,ctx.querystring拿原始字符串- 值全是字符串:
?page=2拿到的是'2',要自己转数字;没传就是undefined - 重复键变数组:
?tag=a&tag=b→{ tag: ['a', 'b'] };不支持嵌套,a[b]=1的键就是'a[b]' - 分页给默认值和上限,排序字段走白名单,别拼进
SQL token、手机号别放查询串,会进访问日志、浏览器历史和Referer
Koa 里拿 GET 参数很直接:ctx.query 是解析好的对象,ctx.querystring 是 ? 后面那串原始文本。 容易踩的是类型:查询串里的值全是字符串,?page=2&size=20 拿到的是 { page: '2', size: '20' },同一个键传两次会变成数组,没传就是 undefined。所以我会在接口入口统一做一层校验和转换,分页给默认值和上限,排序字段只认白名单。敏感信息别放查询串里,它会出现在访问日志、浏览器历史和 Referer 里。
理解「GET请求数据获取」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
列表接口里处理查询参数,一般就是下面这几步:转类型、给默认值、限上限、走白名单。
app.use(async ctx => {
// GET /list?page=2&size=500&sort=price&tag=a&tag=b
const page = Math.max(1, parseInt(ctx.query.page, 10) || 1)
const size = Math.min(100, parseInt(ctx.query.size, 10) || 20) // 上限 100
const sort = ['price', 'createdAt'].includes(ctx.query.sort) ? ctx.query.sort : 'createdAt'
const tags = [].concat(ctx.query.tag ?? []) // 单个值或数组都统一成数组
ctx.body = { page, size, sort, tags }
// => { page: 2, size: 100, sort: 'price', tags: ['a', 'b'] }
})
几个细节:size 不设上限,有人传 size=100000 就能把数据库拖慢;sort 直接拼进 ORDER BY 是典型的注入点,只能从白名单里取;tag 可能是字符串也可能是数组,用 [].concat() 统一一下,后面的代码就不用到处判断。字段一多,换成 zod 写一个 schema,转换和校验一次搞定。
面试官追问
追问 1GET /search 什么参数都没带,ctx.query 是什么?
空对象 {},不是 undefined,也不该报错。按业务给默认值,或者回 400 提示缺参数,别直接 500。
追问 2?page=2 和 ?page=2&page=3,ctx.query.page 分别是什么?
前者是字符串 '2',后者是数组 ['2', '3']。拿到值先判断类型,字段多了用 zod 这类 schema 库统一校验,别在每个接口里手写一堆 if。
追问 3前端传 ?filter[status]=done,后端拿到的是什么?
{ 'filter[status]': 'done' },键名就是整串。Koa 默认用 Node 的 querystring 解析,不支持嵌套;需要嵌套就用 koa-qs,或者让前端把复杂条件放到 POST 的 JSON 里。
追问 4ctx.query.hasOwnProperty('page') 为什么直接报错?
querystring.parse 返回的是没有原型的对象,上面根本没有 hasOwnProperty。改用 Object.hasOwn(ctx.query, 'page') 或 'page' in ctx.query。
追问 5用户的搜索词放在查询串里,要注意什么?
它会进 Nginx 访问日志、浏览器历史,还会通过 Referer 带给第三方。普通搜索词问题不大,token、手机号这类就别放;日志里要记也先脱敏。
# GET请求数据获取
# 使用方法
30 秒速记
- 两组四个入口:
ctx.query/ctx.querystring和ctx.request.query/ctx.request.querystring,前者代理后者,值一样 query→ 对象{ a: '1', b: '2' },值是字符串;querystring→'a=1&b=2',不带?- 带
?的是ctx.search;路径加查询串是ctx.url - 业务判断用对象,签名校验、原样透传用原始字符串
- 给
ctx.query赋值会同步改写querystring和url
Koa 拿 GET 参数就两种形态:query 是解析好的对象,querystring 是原始字符串,两者都能从 ctx 或 ctx.request 上取。 ctx.query 其实就是代理到 ctx.request.query,两种写法拿到的是同一个东西,团队统一一种就行,我习惯直接写 ctx.query。请求 /?a=1&b=2 时,ctx.query 是 { a: '1', b: '2' },注意值是字符串不是数字;ctx.querystring 是 'a=1&b=2',不带问号。业务逻辑用对象,要拿原始文本去算签名时才用 querystring。
在koa中,获取GET请求数据源头是koa中request对象中的query方法或querystring方法,query返回是格式化好的参数对象,querystring返回的是请求字符串,由于ctx对request的API有直接引用的方式,所以获取GET请求数据有两个途径。
- 1.是从上下文中直接获取
- 请求对象ctx.query,返回如 { a:1, b:2 }
- 请求字符串 ctx.querystring,返回如 a=1&b=2
- 2.是从上下文的request对象中获取
- 请求对象ctx.request.query,返回如 { a:1, b:2 }
- 请求字符串 ctx.request.querystring,返回如 a=1&b=2
面试官追问
追问 1ctx.querystring.a 为什么是 undefined?
querystring 是字符串 'a=1&b=2',字符串上没有 a 这个属性。要拿字段用 ctx.query.a。
追问 2ctx.query.a === 1 为什么是 false?
值是字符串 '1',严格相等比不过。写 Number(ctx.query.a) === 1,或者在入口统一转好类型。
追问 3网关要用原始参数串算签名,用 query 还是 querystring?
用 ctx.querystring。解析成对象再拼回去,参数顺序、+ 和 %20 的写法、空值都可能变,签名就对不上了。
追问 4ctx.query 能直接赋值改参数吗?
能,ctx.query = { a: '2' } 会反过来改写 ctx.querystring 和 ctx.url,后面的中间件看到的都是新值。一般只在做参数规范化时这么干,业务代码里别随手改。
追问 5反复读 ctx.query,会不会每次都重新解析?
不会。Koa 按 querystring 做了缓存,同一个请求里读多少次都只解析一次,ctx.query 和 ctx.request.query 拿到的也是同一份结果。
# 举个例子
30 秒速记
- 示例把
ctx.url、ctx.request.query、ctx.request.querystring、ctx.query、ctx.querystring一起返回,方便对照 - 访问
/page/user?a=1&b=2:url是'/page/user?a=1&b=2',query是{ a: '1', b: '2' },querystring是'a=1&b=2' ctx.xxx和ctx.request.xxx两组输出一模一样,前者就是代理- 跑起来顺手试边界:重复键、空值、
+、中文、不完整的%编码
这个例子的思路是把能拿到参数的几个入口一次性全返回,在浏览器里一眼对比它们的区别。 访问 /page/user?a=1&b=2,url 是带路径和查询串的完整地址,两个 query 都是 { a: '1', b: '2' },两个 querystring 都是 'a=1&b=2'。两组完全一致,说明 ctx.query 就是 ctx.request.query 的快捷方式。跑起来之后我建议多试几个边界输入,看看解析结果,比背文档记得牢。
