找回密码
 会员注册
查看: 16|回复: 0

关于编码的那些事-URL编码_UTF_8

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-9-30 16:40:22 | 显示全部楼层 |阅读模式
点击上方关注 · 文末有彩蛋背景Web 项目中经常会遇到处理 URL 中 Query 的情况,来看下下面问题你有疑惑吗?项目中发现会用到 qs、query-string、URLSearchParams、甚至 querystring 几种不同的库,其到底差异在哪里,我该用哪个?在 query 中 key=a&key=b 这种情况 key 取值是什么?和 key[]=a&key[]=b 有区别嘛?在 query 中会有结构如 %HH 的数据,为什么是这样形式的?我们为什么要使用 encodeURIComponent 进行编码?和过时的 escape 又有何区别?Content-type 中 x-www-form-urlencoded 的取值,是怎么一回事?于是梳理一下关于 URL Query 的相关知识点,用来去伪解惑。URL QueryString首先介绍下 Query String 的基本概念,这是一切问题的开始。下面是 wiki[1] 的描述:A query string is a part of a uniform resource locator[2] (URL) that assigns values to specified parameters.通常的理解就是 URL 中问号()后面的部分,其设计最初是用做 HTML form 表单提交时的传参。基本结构下面我们看下 query 的基本结构 field1=value1&field2=value2&field3=value3...包含了如下标准:Query String 由一组键值对(field-value)组成;每组键值对的 field 和 value 用=分割;每组数据用&分割;补充个冷知识:除了使用&分割每对数据外,W3C 曾在 1999 年建议所有 Web 服务器同时支持分号;分割符:We recommend that HTTP server implementors, and in particular, CGI implementors support the use of ";" in place of "&" to save authors the trouble of escaping "&" characters in this manner.但在 2014 年以来,就只建议使用 & 作为分隔符了。也就目前我们用到的方式。允许多个value被关联到同一个field上,但 field 如何取值,其实并无明确的处理标准。例如:field=a&field=b时,field 的值应该是 a、b、['a', 'b']、'a, b' 并无任何权威解释。关于处理标准这点实在令人出乎意料。通常这类情况会按照数组的方式处理,即 field 值为 ['a', 'b'],但这仅是不同的框架的决定了如何实现而已。关于这个问题可以前往 stackoverflow[3] 上查看。数据编码前面定义好了整体结构,接下来我们看下数据是如何在 query 中传输的。由于某些字符集(如中文)和在 URL 中有特殊含义的字符(如 空格、%、&、=、、# 等)无法直接在 Query String 中使用,因此使用了一种叫做「百分号编码[4] Percent-encoding[5]」的方式先将这类特殊字符进行编码后,再进行传输。其基本结构就是 % + 2 个 16 进制数字(一个 Byte 的内容),范围 %00 - %FF。具体规则如下:对保留字符进行编码,具体对应如下:!#{{#bodyData}}'()*+,/:;=@[]%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D其对应的就是这些字符的 ASCII 编码的 16 进制格式;如下非保留字符不进行编码,包含:[A-Z]、[a-z]、[0-9]、- 、_、.、~;%百分号编码为%25;空格编码为+或%20;其余字符数据使用某种编码方式转换为字节流,再用百分号编码%HH方式表示。这里需要注意的是,由于早期规范中未明确应使用何种编码,所以会导致如果不明确说明使用何种编码,数据的解析会有歧义。因此在 2005 年发布的 RFC 3986 [6]建议是先转成 UTF-8 编码,再对每个字节进行%HH的编码。注意,如果使用 from 表单 action 方式时,具体编码会根据 meta 头的 charset 的选择。当然上述只是标准,实践中 JavaScript 内置了使用 UTF-8 编码的 encodeURI/encodeURIComponent 函数,大大简化的编码过程。关于指定编码,这里有个有趣的事情:在使用百度时,你会发现 URL 中有个 ie 参数,其实含义就是 Input Encoding(对,不是 IE 浏览器),目的就是指定关键词 wd 的编码格式。曾默认是 GB2312(因为当时很多网站还使用 GB2312 编码),当然现在已经默认成 UTF-8 。(不过百度结果里依然有不少文章还在说 ie 的默认值是 GB2312 )可以用 https://www.baidu.com/swd=%E4%B8%AD&ie=gb2312 和 https://www.baidu.com/swd=%E4%B8%AD 来感受下他们的差异吧~编码实践这一节我们挑重点地对比下各类 Query String 的函数库,了解老虎老鼠的差异,避免开发时傻傻分不清楚。以下仅对常用 API 的部分用法做演示,更多用法可自行查找。瑞士军刀 qsgithub[7]A querystring parsing and stringifying library with some added security.官方介绍很简单:一个增加了安全性的 Query String 解析和序列化的函数库。.parse(string, [options])对于简单 query,可以进行常规的转换,同时会对 field 和 value 进行 decode 解码。qs.parse('a=c&b%201=d%26e');//{a:'c','b1':'d&e'}注意 qs 不会忽略头部的 ,需要自行去掉,否则会当做 field 的一部分,例如:qs.parse('a=b')会解析为 { 'a': 'b' }。支持 query 中的嵌套对象。qs.parse('foo[bar]=baz');//{foo:{bar:'baz'}}但默认子元素最多嵌套 5 层,需要通过 parse(string, [options]) 的 opinion.depth 来修改。//defalutqs.parse('a[b][c][d][e][f][g][h][i]=j');//{a:{b:{c:{d:{e:{f:{'[g][h][i]':'j'}}}}}}}//setdepthqs.parse('a[b][c][d][e][f][g][h][i]=j',{depth:1});//{a:{b:{'[c][d][e][f][g][h][i]':'j'}}}支持自定义除&以外的分隔符。vardelimited=qs.parse('a=b;c=d',{delimiter:';'});//{a:'b',c:'d'}这点符合 W3C 对;支持的建议,但大部分情况应该不会用到。支持各种 array 的解析,虽然官方文档写了 [] 作为数组标识,但实际上不使用[]依然可以解析。varwithArray=qs.parse('a[]=b&a[]=c');//{a:['b','c']}varwithArray=qs.parse('a=b&a=c');//{a:['b','c']}同时也支持为数组指定索引顺序。varwithIndexes=qs.parse('a[1]=c&a[0]=b');//{a:['b','c']};并行支持 allowSparse 获取抽稀形式的数组。varsparseArray=qs.parse('a[1]=2&a[3]=5',{allowSparse:true});//{a:[,'2',,'5']};但默认指定的 index 最大值为 20,如果超过最大值,则按照 object 形式解析。使用 arrayLimit控制最大值。varwithMaxIndex=qs.parse('a[100]=b');//{a:{'100':'b'}}varwithArrayLimit=qs.parse('a[1]=b',{arrayLimit:0});//{a:{'1':'b'}}.stringify(object, [options])这里主要介绍下 array 类型的编码。qs 默认会对 field 和 value 都进行编码,同时会使用[]作为数据的标识(且默认对[]进行百分号编码),需指定 encodeValuesOnly: true才仅对 value 编码。//defalutqs.stringify({key:['a','b']});//key%5B0%5D=a&key%5B1%5D=b//qs.stringify({key:['a','b']},{encodeValuesOnly:true});//key[0]=a&key[1]=b去掉[]标识,可使用 { indices: false }。qs.stringify({key:['a','b']},{indices:false});//key=a&key=b支持配置 charset默认使用 UTF-8,内置了 ISO-8859-1 模式,也可以支持 encoder 扩展。而接下来的库仅支持 UTF-8 的编码方式。简洁专注 query-stringgithub[8]Parse and stringify URL query strings[1]For browser usage, this package targets the latest version of Chrome, Firefox, and Safari.官方名字看起来,依旧是处理 Query String 的。另外,官方还送上友(wei)情(xian)提示,各位同学不要看走眼。Not npm install querystring !!!!!.parse(string, [options])基本的解析和 qs 一样,会对 field 和 value 进行 decode。不过,头部的和#的部分将被忽略,因此可以直接将 location.search 和 location.hash 传入。queryString.parse('a=c&b%201=d%26e');//{a:'c','b1':'d&e'}不支持嵌套,官方建议可以使用 JSON 序列化的方式传值。This module intentionally doesn't support nesting as it's not spec'd and varies between implementations, which causes a lot of edge cases[10].You're much better off just converting the object to a JSON string:query 中 array 的解析,默认不支持[]形式,需要指定 { arrayFormat: 'bracket' }开启。queryString.parse('key=a&key=b');//{key:['a','b']};queryString.parse('key[]=a&key[]=b');//{'key[]':['a','b']};queryString.parse('key[]=a&key[]=b',{arrayFormat:'bracket'});//{key:['a','b']};当然 query-string 也支持索引的方式标记的数组,{arrayFormat: 'index'}。queryString.parse('foo[0]=1&foo[1]=2&foo[3]=3',{arrayFormat:'index'});{foo:['1','2','3']}.stringify(object, [options])依然重点介绍 array 类型的编码,默认不使用[]标识。queryString.stringify({key:['a','b']});//key=a&key=b需要[]的话,使用 {arrayFormat: 'bracket'}开启,默认[]也不会被 encode。queryString.stringify({key:['a','b']},{arrayFormat:'bracket'});//key[]=a&key[]=b这点和 qs 是相反的,需要特别注意!历史产物 querystringNodeJS 中解析 query 的模块。NodeJS 14.x[11] 中明确标记为 Legacy,官方推荐 URLSearchaParms 代替。The querystring API is considered Legacy. New code should use the URLSearchParams[12] API instead.但在 15.x 以及以后的版本又改为 Stable,但指出这是非标准 API。querystring is more performant than[13] but is not a standardized API. Use when performance is not critical or when compatibility with browser code is desirable.功能类似 query-string,不支持嵌套对象的解析,这里不再赘述。血统纯正 URL / URLSearchParamsURL 和 URLSearchParams 是 URL API 规范[14] 中的两个标准的接口。其提供了访问、操作 URL 的 API。其中,URL 定义了像域名、主机和 IP 地址等概念,URLSearchParams 定义了一些常用的方法来处理 Query String。我们重点介绍下后者。URLSearchParams两种方式创建 URLSearchParams 对象,URLSearchParams构造函数会忽略 search 中的。//1.通过URLconsturl=newURL('https://abc.com/path/v1key=a&key=b%26c');constsearch1=url.searchParams;//2.直接构造constsearch2=newURLSearchParams(location.search);.get(name)该方法获取的值会被自动 decode,如果 name 不存在返回 null,如果 value 不存在返回空字符串。constsearch=newURLSearchParams('key=b%26c&key2');search.get('key');//b&csearch.get('key2');//''search.get('key3');//null.getAll(name)需要特别注意,如果有多个相同的 name,get() 只能获取第一个值。获取全部需要使用 getAll(),该函数返回数组(即便只有一个 value)。constsearch=newURLSearchParams('key=a&key=b');search.get('key');//asearch.getAll('key');//['a','b'].set(name, string) / .append(name, string)向 URLSearchParams 中添加数据,set() 会覆盖原有值。如果需要添加重复的 name,需要使用 append()。set() 和 append() 仅支持 string 类型的 value。同时 field 和 value 都会被 encode,无需额外处理。constsearch=newURLSearchParams();search.append('key','a');search.append('key','b');search.toString();//key=a&key=b.keys()返回一个 IterableIterator迭代器,可以使用for...of遍历。需要注意,重复的 key 会出现多次constsearch=newURLSearchParams('key=a&key=b');for(constkeyofsearch.keys()){console.log(key);}//key//key.toString()获取的 Query String,会被自动 encode 处理。空格转成+。对于重复 field,使用了 field=v1&field=v2 的方式。constsearch=newURLSearchParams();search.set('key','&=')search.set('key2','ab');search.toString();//key=%3F%26%3D&key2=a+b兼容关于兼容,目前浏览器占比基本上没有问题。实际开发中遇到 iOS10 以下不兼容的情况,使用 polyfill 即可。总结对比从上面的总结来看,我们发现 qs 和 query-string / URLSearchParams 最大的差异在于对于多层嵌套对象(Nested object)的支持与否。qs被设计用于解析x-www-form-urlencoded数据,拥有强大的序列化能力,可以处理复杂的类 JSON数据。query-string 和 URLSearchParams 则使用简单的序列化算法,适合常规的 Web 端数据传输,处理平面数据结构。对于平面数据,以上效果是一样的。而当使用复杂的 JSON 数据结构时,我们通常会使用JSON.stringify() 方法先将数据进行序列化(也称字符串化),将复杂数据转换成基本的字符串数据后,再进行传输。另外如果有特殊编码需求,除qs外都仅支持 UTF-8 的编码。因此通常情况下:在 Web 项目中解析 GET 形式的 query,使用 URLSearchParams 就足够了(可代替query-string);而在 NodeJS 项目中,除了解析 GET query 外,还要解析 POST body 中的数据,因此使用 qs 可以获得更好的兼容性。同时不少框架也依然使用了 querystring 这个原生 API。expressjs 的 body-parser 中,用户可以自行选择使用 qs 还是 querystring;koajs的koa-body和bodyparser所依赖的 co-body,都选择了qs。当然了解了他们差异后,选择哪种方式就要根据你的实际情况而定了。延伸话题整理资料过程中,引申出更多有趣的问题,也稍作整理。空格编码问题还记得前面提到的编码规则里, 空格的编码可以是 + 或者%20,这里描述的就很模糊。函数对比我们先来看下上面不同 API 是如何处理的?对+和%20的识别都没问题(毕竟兼容还是能做到的),但是转换空格URLSearchParams就有不同的逻辑了。至于为什么会有两种编码结果?这里要特别说明的是URLSearchParams采用了application/x-www-form-urlencoded编码模式,而这个编码采用了一个非常早期(RFC 1738)的通用百分号编码方法——就是将 空格转换为+。至于为什么会采用这种方式,我猜想是因为要考虑到历史兼容问题——生成的 URL 需要被那些旧的仅支持+的程序识别。当然+已经不推荐了,在 RFC 3986[15] 中已推荐使用%20。特别说明这里特别说明下 decodeURIComponent,是无法解析+为 空格的,因此实际业务中,如果无法保证传入空格的编码方式,还是使用URLSearchParams或者query-string来解析数据吧。或者做一个简单的兼容处理:functiondecodeQueryParam(p){returndecodeURIComponent(p.replace(/+/g,""));}decodeQueryParam("search+query%20%28correct%29");//'searchquery(correct)'扩展参考URLSearchParams中 +的问题,具体细节可参考 whatwg 的描述:As a URLSearchParams object uses the application/x-www-form-urlencoded format underneath there are some difference with how it encodes certain code points compared to a URL object (including href and search ). This can be especially surprising when using searchParams to operate on a URL[16]’s query[17].URLSearchParams objects will percent-encode anything in the application/x-www-form-urlencoded percent-encode set, and will encode U+0020 SPACE as U+002B (+).以及 whatwg 中关于 application/x-www-form-urlencoded 的描述:Control names and values are escaped. Space characters are replaced by '+', and then reserved characters are escaped as described in [RFC1738][18], section 2.2: Non-alphanumeric characters are replaced by %HH, a percent sign and two hexadecimal digits representing the ASCII code of the character. Line breaks are represented as "CR LF" pairs (i.e., %0D%0A).Content-type 中的 x-www-form-urlencoded当我们在HTTP中使用 MIME 类型为x-www-form-urlencoded格式提交数据时,所使用的就是前文所介绍的编码方式。只是如果发送的是 GET 请求,数据会拼接在 Query 中;而发送 POST 请求则会将数据放置在消息体(body)中,通过Header中的Content-Type 来指定 MIME 类型。当然并不是所有的数据都适合使用 x-www-form-urlencoded,通常有二进制数据时,urlencoded使用百分号%HH和UTF-8的编码方式,会大大增加了数据的长度。为了节省传输数据的空间,会选择form-data代替。原生 from 表单的编码除了上面提到的各类函数外,原生 html 的 form 表单在提交数据时,本身也是可以进行编码的。当点击提交时, 表单内的 input 数据会进行百分号编码。但要注意的是编码的格式是按照 meta 中设定的 charset 进行的。例如当输入「中」时,UTF-8 是 name=%E4%B8%AD,GBK 则是 name=%D6%D0。空格 则是+。encodeURI(Component) 和 escape前文提到过encodeURI(encodeURIComponent)使用 UTF-8 编码,而 escape 是一个已经被废弃的非标准方式,其采用了 UTF-16 编码,同时在码点小于 255 的使用 %uXX 表示,码点大于 255 的使用 %uXXXX 的方式。同时要注意当decodeURI(decodeURIComponent)解析非法的 %HH 格式数据时(如不合规范的 UTF-8 数据、被截断的%HH 字符等),会包抛出URIError异常。try{consta=decodeURIComponent("%E0%A4%A");}catch(e){console.error(e);}//URIError:malformedURIsequence因此如果无法保证数据的可用性,记得总是要 try...catch 一下比较保险。或者更推荐使用类似safe-decode-uri-component[19]的三方库,来避免这类麻烦。至于 UTF-8 的合法格式是什么样的,这就要涉及更多的编码知识了。 互 动 抽 奖 码上掘金正在举办第 2 期月赛邀你共玩「互动抽奖」活动点击左下方“阅读原文”解锁抽奖玩法参考资料[1]https://en.wikipedia.org/wiki/Query_string[2]https://en.wikipedia.org/wiki/Uniform_resource_locator[3]https://bytedance.feishu.cn/docx/D6LTd2zgHo2S5NxaFE6cnh9knHf#Oo6UdgOqSog2G2xsp3VcSPdIn6d[4]https://zh.wikipedia.org/wiki/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81[5]https://en.wikipedia.org/wiki/Percent-encoding[6]https://datatracker.ietf.org/doc/html/rfc3986[7]https://github.com/ljharb/qs[8]https://www.npmjs.com/package/query-string[9]https://www.baeldung.com/postman-form-data-raw-x-www-form-urlencoded[10]https://github.com/visionmedia/node-querystring/issues[11]https://nodejs.org/docs/latest-v14.x/api/querystring.html[12]https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[13]https://nodejs.org/dist/latest-v19.x/docs/api/url.html#class-urlsearchparams[14]https://url.spec.whatwg.org/#api[15]https://datatracker.ietf.org/doc/html/rfc3986#section-2.1[16]https://url.spec.whatwg.org/#concept-url[17]https://url.spec.whatwg.org/#concept-url-query[18]https://www.w3.org/TR/html4/references.html#ref-RFC1738[19]https://github.com/jridgewell/safe-decode-uri-component[20]https://stackoverflow.com/questions/29175465/body-parser-extended-option-qs-vs-querystring/29177740#29177740[21]https://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2[22]https://stackoverflow.com/questions/1746507/authoritative-position-of-duplicate-http-get-query-keys[23]https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1[24]https://url.spec.whatwg.org/#interface-urlsearchparams[25]https://datatracker.ietf.org/doc/html/rfc1738[26]https://www.w3.org/International/O-URL-code.html
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-14 20:44 , Processed in 0.618158 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表