URLSearchParams 编码边界:空格、加号与重复参数
搜索条件包含空格或加号时,手工拼接问号与连接符很容易埋雷。本文只处理查询参数的构造、解析与回读,不讨论路径编码。依据二〇二六年九月核验的 WHATWG URL 标准,示例在 Node.js v24.19.0 执行;浏览器实现也应遵循相同规则,但这里没有宣称做过浏览器兼容性测试。
AI生成概念配图:原始字符经过查询参数编码,再被解析回原值的流程示意
先分清传进去的是值还是查询串
把原始值交给 set、append 或键值对构造器,序列化交给 URLSearchParams。它采用表单查询编码:空格输出为加号,原本的加号则输出为百分号编码。这个区别不是显示格式的小问题。若直接把含加号的文本拼进查询串,解析器会把其中的加号当作空格,接收端便拿不到原值。
const p = new URLSearchParams();
p.set('q', 'C++ x y');
p.set('token', 'a+b/=');
console.log(p.toString());
console.log(new URLSearchParams(p).get('token'));
console.log(new URLSearchParams('q=C++').get('q'));
console.log(new URLSearchParams({q: encodeURIComponent('C++')}).toString());前两行输出依次为 q=C%2B%2B+x+y&token=a%2Bb%2F%3D 和 a+b/=,说明原始加号、斜杠及等号能够回读。第三行是字母 C 后跟两个空格。第四行得到 q=C%252B%252B:先调用 encodeURIComponent,又让参数对象编码一次,百分号就变成了 %25。这正是常见的重复编码。
判断该不该编码,应看数据所处阶段,不要看字符串里是否恰好出现百分号。来自表单控件的内容通常是原始值;完整地址先交给 new URL,再读取 searchParams;已序列化的查询部分才交给字符串构造器。不要先对整段查询串解码再切分,否则值中原本编码的连接符可能被误当分隔符。
重复键必须先确定业务含义
const tags = new URLSearchParams([['tag', 'js'], ['tag', 'web']]);
console.log(JSON.stringify(tags.getAll('tag')));
tags.set('tag', 'url');
console.log(tags.toString());输出依次为 ["js","web"] 和 tag=url。append 保留多值,get 只取第一个,getAll 才返回所有匹配项;set 会替换首项并移除同名余项。接口若允许多个标签,就用重复键及显式的数组读取规则。把数组塞进普通对象不会自动建立这份多值协议,后端也未必采用相同的重复键策略。
在项目里可以给参数建一张小清单:标签允许重复,分页只能出现一次,空搜索词是否保留。若后端把重复分页参数取最后一个,而前端校验只读第一个,两边就会理解成不同请求。编码工具负责语法,不负责替接口选定这些业务规则。
空值与缺失也要分开:空查询值读出空字符串,不存在的键读出 null。业务若要求某个字段必须填写,检查 has 只能证明键存在,还要判断取出的值。建议保留输入原文作为测试样本,在发送端和接收端分别断言,而不是只比较浏览器地址栏里看起来相近的字符串。
值没有变,地址文本也可能变化
const u = new URL('https://example.com/?q=x%20y~');
console.log(u.search);
u.searchParams.sort();
console.log(u.search);这里先输出 ?q=x%20y~,随后输出 ?q=x+y%7E。即使只有一个键、排序不改变顺序,参数对象的更新也会触发重序列化。调用方若比较地址原文、生成缓存键或校验外部签名,就不能把“解析值相同”直接视为“字节完全相同”。需要保留原文时,另建参数副本分析,不要修改原地址对象。
建立可检查的边界清单
测试至少覆盖中文、空格、加号、百分号、空值和重复键;对自己生成的参数做序列化后回读断言,同时单独测试业务拒绝条件。不要把编码当作加密,也不要因此把口令放入查询参数。将查询值用于页面、数据库或跳转目的地时,还要执行对应场景的校验。这样能把编码问题和业务问题分开定位,而不是靠不断补百分号碰运气。


