encodeURIComponent 和 encodeURI 区别,以及 URL 编码中文乱码的原因
URL 编码(百分号编码)看上去只是一个操作,实际有三种常见写法,混用之后总是掉进同几个坑:参数只收到一半、地址栏里冒出 %2520、解码时抛 URIError、中文变成乱码。每个现象背后基本只有一个原因。
两个函数到底各保留哪些字符
encodeURIComponent 只放过字母、数字和 - _ . ! ~ * ' ( ),其余全部转义。encodeURI 在此基础上,额外放过构成网址结构的那些符号:; , / ? : @ & = + $ #。
| 字符 | encodeURIComponent | encodeURI |
|---|---|---|
/ | %2F | 保留 |
? | %3F | 保留 |
# | %23 | 保留 |
& | %26 | 保留 |
= | %3D | 保留 |
+ | %2B | 保留 |
% | %25 | %25 |
| 空格 | %20 | %20 |
中 | %E4%B8%AD | %E4%B8%AD |
encodeURIComponent("a b&c=中"); // "a%20b%26c%3D%E4%B8%AD"
encodeURI("a b&c=中"); // "a%20b&c=%E4%B8%AD"
由此得到的使用规则很简单:encodeURIComponent 用来编码一小段要放进网址里的内容,比如某个查询参数的值,或者路径中的一段;encodeURI 用来处理已经拼好的完整网址,只想把空格和中文转义掉,同时保留 ?、&、= 这些分隔符继续起作用。
很多人搜”encodeURIComponent 和 encodeURI 区别”,答案落到一句话上:参数值永远用前者,整条链接才考虑后者,而且整条链接最好的做法其实是不要手工拼。
参数里带 & 或 = ,后面的内容丢了
症状:接口收到的参数被截断,或者多出一个谁也没传的参数。原因通常是值没有用 encodeURIComponent,而是用了 encodeURI,或者干脆没编码:
const q = "R&D = 50% off";
"/search?q=" + encodeURI(q); // /search?q=R&D%20=%2050%25%20off
"/search?q=" + encodeURIComponent(q); // /search?q=R%26D%20%3D%2050%25%20off
第一种写法会被服务器解析成 q=R,外加一个名字叫 D 的参数。encodeURI 没法区分这个 & 是数据还是分隔符,所以它选择原样保留。正确做法是对每个值单独编码,再用字面的 & 和 = 连起来;更省事的是交给标准 API:new URLSearchParams({ q }).toString() 会替你处理好。
# 同理。值里没转义的 # 会被当成片段(fragment)的开头,它后面的所有内容浏览器都不会发给服务器,于是你看到的现象是”参数后半截莫名其妙没了”。
空格是 %20 还是 +
在网址的任何位置,%20 都是对的。+ 只有在查询字符串和 application/x-www-form-urlencoded 请求体(也就是 HTML 表单提交的格式)里才表示空格;在路径里,+ 就是一个普通的加号。解码行为能看出区别:
decodeURIComponent("a+b"); // "a+b"
new URLSearchParams("a=1+2").get("a"); // "1 2"
由此产生两类常见问题。一是值里真有加号(电话号码 +86、c++、base64 字符串)却没被转义,接收方按表单规则把它读成了空格,所以它必须以 %2B 的形式传输。二是把表单风格的 + 用到路径里,结果路径里多出一个字面加号。
本站的 URL 编码工具里,“表单”方式会把 %20 换成 +,解码时也会把 + 还原成空格,所以只适合查询字符串和表单体。它只做这一件事:浏览器的 URLSearchParams 序列化时还会额外转义 ! ' ( ) ~,而这个方式不会。
Python 也是同样的划分:urllib.parse.quote("a b/c", safe="") 得到 a%20b%2Fc,quote_plus("a b/c") 得到 a+b%2Fc。注意 quote 默认保留 /,编码参数值时要写上 safe=""。
重复编码:出现 %2520
对已经编码过的文本再编码一次,每个 % 都会变成 %25:
encodeURIComponent("a b"); // "a%20b"
encodeURIComponent(encodeURIComponent("a b")); // "a%2520b"
识别特征是 %25 后面紧跟两位十六进制数。典型来源是两层都在编码:HTTP 客户端库已经会编码参数,业务代码又手工包了一层 encodeURIComponent;或者登录后的回跳地址 redirect=...,先作为参数编码一次,拼接回跳链接时又编码一次。
解码一次得到的是 a%20b,所以页面上显示的是一堆百分号而不是空格。治本的办法是找到多出来的那一层并去掉,而不是在末端多解码一次凑合。本站工具在”编码”方向遇到输入里已有 %XX 时会提示,解码结果里仍有 %XX 时也会提示,就是为了发现这种情况。
decodeURIComponent 报 URI malformed
decodeURIComponent 抛出 URIError: URI malformed 有两种完全不同的原因,报错信息本身分不出来:
%后面不是两位十六进制数字。 比如decodeURIComponent("100%")就会抛错。这是把根本没编码过的文本(折扣码、含百分号的描述)直接扔进了解码器。先把它转义成%25,或者别去解码它。- 转义序列是合法的十六进制,但不是合法的 UTF-8。
decodeURIComponent("%C4%E3%BA%C3")会抛错,因为这四个字节是”你好”的 GBK 编码。JavaScript 只按 UTF-8 解码,旧系统或别的字符集生成的链接,用它永远解不开。
URL 编码工具会指出出错的位置,并告诉你是哪一种。遇到第二种情况,可以打开”保留无效序列”,让它先解出能解的部分,其余原样保留,再去查来源用的是哪种字符集。编码也会失败:只剩半个 emoji 的孤立代理项(例如 "\uD800")会让 encodeURIComponent 抛错,因为它没法转成 UTF-8。
URL 编码中文乱码:字节与字符集
百分号编码作用在字节上,一个字符变成哪几个字节,取决于用什么字符集。UTF-8 下”你好”是六个字节,即 %E4%BD%A0%E5%A5%BD;一个 emoji 比如 😀 是四个字节,即 %F0%9F%98%80。同样两个字在 GBK 下只有四个字节,即 %C4%E3%BA%C3。两种结果都是合法的百分号编码,没有谁对谁错,但接收方必须用发送方的字符集去还原。
现代浏览器和 URL 标准都使用 UTF-8。如果服务端用某个老旧的默认字符集(比如 ISO-8859-1 或 GBK)解析 URL,凡是 ASCII 以外的字符都会变成乱码或问号。这种情况应当到服务端去改 URL 或请求的字符集配置,客户端这边改不动。反过来,如果你拿到的是别人用 GBK 生成的链接,用 UTF-8 去解就会报上一节的错误。
另外不要用 escape()。它已经废弃,也不是 UTF-8:escape("中") 返回 %u4E2D,这种写法没有任何标准的 URL 解析器认可。
地址栏里看到中文,并不代表发出去的就是中文。浏览器只是显示时把它解码了,实际请求里仍然是 %E4%BD%A0 这样的形式。复制链接粘贴到聊天软件里变成一长串百分号,是同一回事。
路径、片段和整条链接怎么处理
- 路径中的一段(例如文件名含空格或中文):用
encodeURIComponent单独编码这一段,再用/连接。直接对整个路径用它,会把分隔的斜杠也变成%2F。 - 整条已拼好的链接:
encodeURI只适合”链接基本没问题,只是混进了空格和中文”的情况。前提是其中不能有需要当作数据的&、=、#。 - 回跳地址、
url=这类嵌套链接:把内层整条链接当作一个值,用encodeURIComponent编码一次。外层用URLSearchParams或再手工拼,但不要在两层都编码。
用 URL 编码工具排查
先在上方选”编码”或”解码”,再选方式:组件用于单个参数值,完整网址用于保留 : / ? # & =,表单则把空格换成 +。状态栏会说明转换了多少个转义序列,“链接的组成部分”会把完整链接拆成协议、主机、路径和已解码的查询参数表。想确认一个含 & 的值是不是留在了同一个参数里,看这张表最快;它按表单规则解码每个值,所以输入里的 + 在表中会显示成空格。
转换在当前页面里用浏览器自带的函数完成,内容不会发送到任何地方,处理带 token 的链接时也可以放心粘贴。