约 7 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证
三个函数各管一段,混用是绝大多数 URL 问题的根源。encodeURIComponent 编「一个值」,它会把 & = ? # / + 连同所有非 ASCII 字符一起编掉——放进查询串的参数值就用它。encodeURI 编「一整段已拼好的 URL」,它故意保留 & = ? / # 这些结构符号,所以拿它编值是错的(& 留着,参数照样被切开)。URLSearchParams 一步到位地做「键=值」的组装与编码,内部用的就是 component 级规则,最省心。路径段(path segment)属于 component 规则,锚点(# 后面的 fragment)根本不会发到服务器。
| 症状 | 真正的原因 | 修法 |
|---|---|---|
| 参数值里带 &,服务端多收到一个参数 | 未编码,& 被当成参数分隔符 | 用 URLSearchParams 或 encodeURIComponent 编值 |
| 值里的 = 号导致 key 被截断 | 手写 query 串,第一个 = 之后全归 value | 同样走参数化组装,别手拼 |
| 搜 "a b" 查不到结果 | 空格在表单编码里变 +,而服务端只解 %20 | 统一用 URLSearchParams,或解码后把 + 还原为空格 |
| 中文参数变乱码 | 按 GBK / Latin-1 编解码,或编了两层 | 全程 UTF-8,且只编一层 |
| 值出现 %2520 这种三段式 | 前端编了一次,库又编了一次 | 只在一层编,或在明确边界解码一次 |
| 带 # 的值让后面的内容全丢 | # 之后是 fragment,从不发给服务器 | 编码 # 为 %23 |
| 签名校验总是失败 | 两端编码规则或参数排序不同 | 约定「先排序、再编码、只编值」的统一规则 |
| 路径里的斜杠被当成目录分隔 | 路径段也按值编了 / | 按段编码,保留分隔斜杠 |
const q = "Tom & Jerry";
const note = "a=b";
const tag = "C# 教程";
// ?q=Tom & Jerry¬e=a=b&tag=C# 教程
fetch("/api/search?q=" + q + "¬e=" + note + "&tag=" + tag);
// 服务端收到:
// q=Tom (& 之后成了新的分隔符)
// Jerry、note=a、b、tag=C (= 和 # 各切一刀)
// 整条 URL 里的中文根本没编码const p = new URLSearchParams();
p.set("q", "Tom & Jerry");
p.set("note", "a=b");
p.set("tag", "C# 教程");
// ?q=Tom+%26+Jerry¬e=a%3Db&tag=C%23+%E6%95%99%E7%A8%8B
fetch("/api/search?" + p.toString());
// 空格编成 +(表单约定),& = # 编成 %26 %3D %23,中文按 UTF-8 逐字节编
// 读取:new URL(req.url, "http://x").searchParams.get("q")const v = "a&b=c 中文#d/e+f";
encodeURIComponent(v) // a%26b%3Dc%20%E4%B8%AD%E6%96%87%23d%2Fe%2Bf ← 值用它
encodeURI(v) // a&b=c%20%E4%B8%AD%E6%96%87#d/e+f ← & = # / 保留,别用来编值
new URLSearchParams({k: v}).toString()
// k=a%26b%3Dc+%E4%B8%AD%E6%96%87%23d%2Fe%2Bf ← 空格是 +空格有两种编码,取决于编码器声明的上下文。application/x-www-form-urlencoded(HTML 表单、URLSearchParams 默认)把空格写成 +;RFC 3986 的通用百分号编码(encodeURIComponent)把空格写成 %20。服务端解码时的行为决定了它能不能对上:多数语言把查询串里的 + 还原成空格,但只有一部分在路径段里也这么做。所以如果你的值可能出现在路径中,或者服务端解码器不认 +,就统一用 %20(把空格写进 URL 前先 encodeURIComponent 整段)。反过来,解析端必须显式把 + 还原成空格,否则 "a b" 会变成 "a+b"。
%2520 这种三段式的成因永远是「同一个值被编了两次」:前端为了安全先编一次,URL 库组装 query 时又编一次,或者客户端编完、网关又编一次。判断方法很简单——把错误值整体解码一次,如果得到的是合法的 percent-encoding(%20、%E4%B8%AD 这种形态),说明你看到的就是双层编码的中间态,而不是最终形态。正确做法是划清边界:编码只发生在「离开你的进程」的那一刻(前端或 CLI 出口处),中间层一律不解码;服务端在入口处解码一次,之后代码里拿到的永远是明文,后面再拼 SQL、再放 JSON 都不再需要任何编码。
把上面所有规则压成一条流水线:用户输入保持原文;写进 URL 时只编一次、用 component 规则、只针对值;服务端在入口解码一次到明文;之后所有校验、存储、拼接都在明文上进行;输出到另一个上下文(HTML、SQL、另一个 URL)时重新按那个上下文的规则处理一次。「解码一次、之后全程明文」这条最难坚持,也最容易出事——很多系统在中间层「顺手」再解码一次,就从这里开始出问题了。
两端必须都是 UTF-8,而且只编一层。客户端用 encodeURIComponent 或 URLSearchParams 把中文按 UTF-8 逐字节编成 %E4%B8%AD 这样的形式;服务端用 UTF-8 解一次。如果服务端是 GBK(常见于老旧 Java 项目或未设 charset 的响应),要么改服务端,要么在响应头显式声明 charset=utf-8。解码后出现「锟斤拷」这类文字就是编码选错了,可以用 乱码修复工具反查真实编码。
encodeURI 编一整段已拼好的 URL,会保留 & = ? / # 这些结构符号;encodeURIComponent 编「一个值」,把这些结构字符全部编掉。规则很简单:拼 query 的值用 encodeURIComponent,用 encodeURI 只会让 & 继续把参数切开。拿不准的时候用 URLSearchParams 一步组装,它内部就是 component 规则。
取决于方向。服务端把 + 收到当空格:客户端应把加号编成 %2B,或者整体改用 encodeURIComponent(它把空格编成 %20,不会产生 +)。前端解析查询串时把字面量 + 还原成空格:用 URLSearchParams 就自动处理,手写解析则要写 replace(/\+/g, " ")。
这是同一个值被编了两次:前端编一次,URL 库组装 query 时又编一次。判断方法是手动解码一次,如果结果仍是合法的 percent-encoding(%20、%E4%B8%AD),就说明你看到的是中间态。修法是划清边界——编码只发生在值离开进程的那一刻,中间层不解码,服务端入口解码一次之后全程保持明文。用 URL 解析工具把整条 URL 拆开,逐个参数看服务端实际收到什么。