碎碎念:JS如何拼接URL参数字符串?
长话短说:建议不要自己实现,用库它不香吗?
出于好奇看了某播客官网的“发评论”前端实现,发现其实现拼接URL参数字符串的逻辑有点草率。
简单搜索下又发现不少存在谬误的简中技术文章,恰巧这也是我面试时经常问的题目,所以决定来说明下这个事情。
反例2
拼接逻辑其实很简单,就是把如 {"a": "b", "c": "d"}
这样的数据变成形如 a=b&c=d
的字符串,唯一需要注意的就是转义(废话,任何一种序列化都需要转义),即把数据中会跟 &
、=
等会产生混淆的字符以另一种方式表示。这种形式的字符串出现在 URL 参数字符串,或者在 x-www-form-urlencoded
编码*的 ajax 请求体中。
理论上,HTTP协议传输的只是一坨文本*,至于 URL 里的参数长成什么样是无所谓的,只要后端的实现逻辑能够解析出参数就行了(下例)。
能够识别 自定义参数拼接逻辑 的后端实现(以 NodeJS 为例,不完备)
HTTP请求:
GET /status@name(ryan)other(&=) HTTP/1.1
Host: localhost:8080
后端服务:
require('http').createServer(function (request, response) {
// request.url 是 `/status@name(ryan)other(&=)` ,自定义格式不能直接用标准实现了
// let {searchParams} = new URL(request.url, `http://${request.headers.host}`);
let {searchParams} = parseCustomURL(request.url);
console.log(searchParams);
}).listen(8080);
function parseCustomURL(url) {
let i = url.indexOf('@');
let search = i >= 0 ? url.substring(i) : '';
let searchParams = {};
if (search) {
search = search.substring(1);
let matched;
let pattern = /\)?(?<key>\w+)\((?<value>[^)]*)\)/g;
while (matched = pattern.exec(search)) {
let {key, value} = matched.groups;
searchParams[key] = value;
}
}
return {search, searchParams};
}
但实践上,我们用的 Nginx 服务器或者 koa-router/body 等中间件,大家都遵循同一个规范才能协作。所以从标准实现的角度看,上述两个例子都没有正确实现转义:
- 前者只考虑到了要对
&
进行转义,但 错误地实现 了。因为JS的String.prototype.replace()
函数的第一个参数为字符串时只会替换一次,当出现多个&
时就不符合预期了。 - 后者则完全没有考虑转义,假如传入的数据是
{"a":"&="}
,得到字符串将是&a=&=
。解析后得到的数据是{"a":"","":""}
,明显是不对了。
而JS内置的 encodeURIComponent()
函数就可以完美解决url转义的问题*,根本不需要自己去替换字符串啥的,只要别忘记调用就行了。
我的不完备实现
export function stringify(obj) {
return Object.entries(obj).map(items => items.map(encodeURIComponent).join('=')).join('&');
}
export function parse(str) {
return str.split('&').reduce(function (ret, pair) {
let [key, val] = pair.split('=').map(decodeURIComponent);
ret[key] = val;
return ret;
}, {});
}
上面是我的一个不完备实现,参数是 Map
类型、value为 null|undefined
等情况都没有处理。所以我的建议就是这种标准的逻辑,在生产环境能用库的就用,不要自己实现。能力一般水平有限,又不写单元测试(或测试覆盖不全),不出问题很难啊。直接用 query-string 不香吗,对于复杂的拼接/解析场景(如:值是数组类型)也都有很好的支持(这里没有建议复杂数据类型用urlencoded方式序列化的意思,直接发 JSON 更香,Blob 用 FormData 也香)。如果不考虑兼容低版本Node或浏览器的话*,也可以直接使用标准API URLSearchParams
。
再多一嘴,日期时间处理也都可以用库 dayjs,i18n 啥的人家也都给你实现了,不要犯傻自己实现还搞出一堆问题。其它的各种转义也是一样的道理,碰到太多自己造跛脚轮子搞出的线上问题了。