长话短说:建议不要自己实现,用库它不香吗?

出于好奇看了某播客官网的“发评论”前端实现,发现其实现拼接URL参数字符串的逻辑有点草率。

反例1

简单搜索下又发现不少存在谬误的简中技术文章,恰巧这也是我面试时经常问的题目,所以决定来说明下这个事情。

反例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 更香,BlobFormData 也香)。如果不考虑兼容低版本Node或浏览器的话*,也可以直接使用标准API URLSearchParams

再多一嘴,日期时间处理也都可以用库 dayjs,i18n 啥的人家也都给你实现了,不要犯傻自己实现还搞出一堆问题。其它的各种转义也是一样的道理,碰到太多自己造跛脚轮子搞出的线上问题了。