一、查询字符串在HTTP请求中的位置
URL里问号?之后的部分就是查询字符串。它由一组键值对组成,键值之间用=连接,多个键值对之间用&分隔。看一个具体地址:
http://localhost:8080/users?id=1&name=John
id=1和name=John是两个查询参数,&是它们的分隔符。这部分数据通常跟随GET请求发送,用于筛选、分页、排序这类不需要请求体的轻量信息传递。
Node.js提供了两个工具来处理它:内置的querystring模块和标准的URLSearchParams。前者是Node.js早期的实现,后者来自Web标准,现在被Node.js全局支持。官方文档已经把querystring标记为遗留(Legacy)API,新项目推荐使用URLSearchParams。但遗留代码库中querystring仍然大量存在,理解它的行为对维护和迁移都有必要。
二、querystring.parse:把字符串拆成对象
parse()把查询字符串转换成JavaScript对象,键值对一一对应。
const querystring = require('node:querystring');
const query = 'category=technology&author=John';
const result = querystring.parse(query);
console.log(result);
输出:
[Object: null prototype] { category: 'technology', author: 'John' }
输出中[Object:nullprototype]这个标记值得留意。parse()返回的对象没有原型链,也就是说它不继承Object.prototype上的方法,hasOwnProperty、toString这些都不能直接调用。这个设计是为了避免原型污染——如果查询字符串里出现__proto__这样的键,普通对象可能会被污染,而无原型对象天然免疫。
如果要判断某个键是否存在,不能写result.hasOwnProperty('category'),会报错。正确写法是:
if (Object.prototype.hasOwnProperty.call(result, 'category')) {
console.log('存在');
}
或者用Object.hasOwn(result,'category'),Node.js16.9之后可用。
parse()的完整签名是querystring.parse(str[,sep[,eq[,options]]])。sep自定义键值对分隔符,默认&;eq自定义键值分隔符,默认=;options中可以设置maxKeys限制解析的键数量,默认1000。超过上限的键会被丢弃,这在处理不可信输入时能起到限制作用。
当同一个键出现多次时,parse()会把值聚合成数组:
const result = querystring.parse('tag=js&tag=node&tag=stream');
console.log(result.tag); // [ 'js', 'node', 'stream' ]
这个行为和URLSearchParams不同,后者保留原始顺序,需要用getAll()获取全部值。
三、querystring.stringify:对象转回字符串
stringify()是parse()的逆操作,把对象序列化成查询字符串。
const querystring = require('node:querystring');
const obj = {
category: 'technology',
author: 'John'
};
const query = querystring.stringify(obj);
console.log(query);
输出:
category=technology&author=John
它能处理字符串、数字、布尔值,以及由这些类型组成的数组。数组会被展开成重复的键:
const query = querystring.stringify({ tag: ['js', 'node'] });
console.log(query); // tag=js&tag=node
值里的特殊字符会被自动编码。空格变成%20,中文会被转成UTF-8的百分号编码。这一点很重要——直接用字符串拼接构造URL时,如果值里含有&或=,会把参数结构破坏掉,而stringify()会替你处理这些。
stringify()签名中的sep和eq参数可以自定义分隔符。比如某些系统用;分隔参数,可以传';'。这类需求不常见,但对接第三方接口时偶尔会遇到。
需要提醒的是,stringify()只序列化对象自身的可枚举属性,原型链上的属性会被忽略。如果传入的对象是类实例,方法不会出现在结果里,这是符合预期的行为。
四、escape与unescape:手动编码解码
escape()把字符串转成URL编码形式,unescape()是它的逆操作。
const querystring = require('node:querystring');
console.log(querystring.escape('Node.js Tutorial'));
console.log(querystring.unescape('Node.js%20Tutorial'));
输出:
Node.js%20Tutorial
Node.js Tutorial
这两个方法在parse和stringify内部已经被调用,日常很少需要单独使用。它们真正的用武之地是在做部分编码或调试时——比如只想编码值而不编码键,或者查看某个编码字符串的原文。
需要注意的是querystring.escape()和全局的encodeURIComponent()行为并不一致。前者对某些字符的处理有细微差别,跨系统传递数据时不要混用,一端用escape编码、另一端用decodeURIComponent解码,可能在某些字符上产生偏差。保持编解码方法成对使用是基本要求。
经验提示:
escape和unescape这两个名字容易引起误解,它们和JavaScript早期废弃的全局escape()函数不是一回事。看遗留代码时别把两者混为一谈。
五、querystring与URLSearchParams的取舍
两者都能解析和构造查询字符串,但设计定位不同。
| 对比项 | URLSearchParams | querystring(遗留) |
|---|---|---|
| 状态 | 现在标准API | Node.js遗留API |
| 引入方式 | 全局可用,无需require | 需要引入node:querystring |
| 返回类型 | URLSearchParams对象 | 普通对象(无原型) |
| 重复键 | 保留顺序,用getAll取全部 | 聚合成数组 |
| 缺失值 | 返回空字符串 | 返回空字符串 |
| 浏览器支持 | 现在浏览器支持 | 仅Node.js |
| 主要操作 | get、set、append、delete | parse、stringify |
URLSearchParams的优势在于它是Web标准,同一套代码在浏览器和Node.js都能跑。它的API更接近操作,增删改查都支持,而querystring只提供整体转换。
const params = new URLSearchParams('category=technology&author=John');
params.append('tag', 'node');
params.set('author', 'Jane');
console.log(params.toString());
输出:
category=technology&author=Jane&tag=node
这种逐项修改的能力在构造请求时很实用。用querystring做同样的事,得先parse成对象、改对象、再stringify,多了一步转换。
个人建议:新项目直接用
URLSearchParams。老项目维护时,如果只是读一下参数,querystring.parse也能用,不必为了统一而大改。但如果涉及重复键处理、动态增删参数,迁移到URLSearchParams会让逻辑清晰不少。迁移时注意返回值类型的变化——原来result.tag拿数组,现在要改成params.getAll('tag')。
六、查询字符串的典型应用场景
筛选和搜索是最常见的用途。客户端指定要获取的数据子集:
/products?category=electronics&brand=apple
服务端解析后按条件查询数据库。
分页通过查询参数控制起始位置和数量:
/posts?page=3&limit=10
服务端根据page和limit计算offset,从数据集中取出对应片段。
排序让客户端动态指定排序字段和方向:
/users?sort=createdAt&order=desc
这里有个安全细节:sort和order的值不能直接拼进SQL,必须做白名单校验,只允许预定义的字段名和方向。否则会引入SQL注入风险,这是查询参数处理里容易被忽略的一环。
埋点和分析参数在营销链接中大量出现:
/products?utm_source=google&utm_campaign=sale
这类参数服务端通常只做记录,不参与业务逻辑。
服务端处理这些参数时,类型转换和默认值是必须考虑的问题。查询字符串里的值都是字符串,page=3拿到的是'3'而不是数字3,需要显式转换。缺失的参数要给出默认值,否则parseInt(undefined)会得到NaN,进而引发下游错误。
const page = parseInt(params.get('page'), 10) || 1;
const limit = Math.min(parseInt(params.get('limit'), 10) || 20, 100);
这段代码做了三件事:转数字、给默认值、限制上限。上限尤其重要,不加限制的话客户端传limit=999999就能一次拉走整张表。
七、本节课程知识要点
-
查询字符串是URL中
?之后的部分,由键值对经&连接组成。 -
querystring是遗留API,新项目推荐用URLSearchParams。 -
parse()返回无原型对象,避免原型污染,判断键存在需用Object.hasOwn。 -
重复键在
parse()中聚合为数组,在URLSearchParams中用getAll()获取。 -
stringify()自动处理特殊字符编码,比手动拼接字符串可靠。 -
escape()和unescape()内部已被调用,单独使用时注意与全局编码函数保持一致。 -
参数值都是字符串,需要显式转数字并设置默认值与上限。
-
参与SQL或文件路径的排序、筛选参数必须做白名单校验。
-
maxKeys可限制解析数量,处理不可信输入时有用。
八、练习与思考
-
写一个函数,接收查询字符串,返回
{page,limit,sort},其中缺失值给默认值,limit上限为100。 -
用
URLSearchParams和querystring分别实现同一段筛选逻辑,对比两种写法的差异。 -
构造一个含重复
tag参数的查询字符串,分别用两种方式读取全部值,说明返回结构的不同。 -
思考:为什么
parse()返回无原型对象?如果返回普通对象,__proto__键会带来什么后果。 -
设计一个安全的排序参数处理函数,只接受预定义的字段名,其余输入抛出错误或回退到默认值。
九、常见问题排查
解析结果里键名或值出现乱码,检查字符串是否经过了正确的URL解码,不要对已解码的字符串重复解码。result.hasOwnProperty报错,说明拿到的parse()结果是无原型对象,改用Object.hasOwn。数组值被当成字符串处理,确认parse()对重复键返回的是数组,读取时判断类型。构造的URL参数丢失,检查值里是否含有未编码的&或=,改用stringify()生成。分页参数传了非数字,parseInt返回NaN,处理时加上默认值兜底。
查询字符串是Web开发中传递参数的基础形式,理解它的解析规则和编码行为,能避免大量参数丢失、乱码、注入类的隐蔽问题。