URL 编码完全指南:为什么你的链接里全是百分号
什么是 URL 编码?
先说一个我踩过的坑。
几年前写一个文件下载接口,文件名是中文的——“2024年工作总结.pdf”。前端直接把文件名拼进 URL 里发请求,结果后端收到的是一串 %E5%B9... 开头的东西,文件名完全乱套。我当时第一反应是后端编码没设对,折腾了半小时才发现:浏览器地址栏里看着是中文,HTTP 请求发出去的时候早就被转成一堆百分号了。
URL 编码(也叫百分号编码)就是干这件事的——把 URL 里不能直接出现的字符,转成 % 加两位十六进制数的形式。
说白了,URL 的设计者当年做了一个非常”英语中心主义”的决定:URL 只能包含 ASCII 字符集里的一个小子集——大小写字母、数字、以及 -_.~ 等少数几个符号。其他所有字符(空格、中文、特殊符号…)想进 URL,必须经过编码。
这个限制到今天还在影响着每一个 Web 开发者。
工作原理
URL 编码的规则本身并不复杂,但它的边界条件是真的多。
基本规则
一个字符如果不属于 “URL 安全字符”,就把它换成 %XX——其中 XX 是该字符 UTF-8 字节序列中每个字节的十六进制表示。
举个例子:空格的 ASCII 码是 32(十六进制 0x20),编码后就是 %20。你在浏览器地址栏里看到 q=hello%20world,实际上空格被换成了 %20。有些老系统也把空格编成 +,那是 application/x-www-form-urlencoded 规范的遗产,不是 URL 标准本身。
再看中文。“你好” 的 UTF-8 编码是 E4 BD A0 E5 A5 BD(6 个字节),所以编码后变成:
%E4%BD%A0%E5%A5%BD
一个汉字三个字节,一个字节一个百分号组合——URL 编码后一个中文字符要占 9 个字符的位置。
哪些字符需要编码?
这得看 RFC 3986 的定义。URL 字符分成两类:
保留字符(有特殊含义,不用在它本来的语义上就得编码):
: / ? # [ ] @ ! $ & ' ( ) * + , ; =
比如 ? 在 URL 里标记查询参数的起点,如果你的参数值本身包含一个 ?,那就得把它编成 %3F,否则解析器会认为后面又开始了新的查询串。
非保留字符(永远不用编码):
A-Z a-z 0-9 - _ . ~
实用判断法:如果你不确定某个字符要不要编码,就去看
encodeURIComponent会不会动它——如果动了,就说明它不是安全字符。这个函数比任何文档都好使。
核心特性
| 特性 | 说明 |
|---|---|
| 编码格式 | %XX,XX 为 UTF-8 字节的十六进制大写值 |
| 适用场景 | URL 的路径段、查询参数、片段标识符 |
| 可逆性 | 完全可逆,前提是知道原始字节序列就是 UTF-8 |
| 膨胀率 | 仅 ASCII 安全字符则不变;全中文极值约 300%(1 字符变 9 字符) |
| 大小写 | 十六进制部分理论上大小写都可以,但推荐大写,RFC 3986 用的就是大写 |
说实话,膨胀率这件事在 GET 请求里特别容易出问题。我之前做一个搜索功能,用户在输入框里打了 50 个中文字,前端用 GET 方式发给后端,URL 长度直接过了 600 个字符——有些浏览器和服务器对 URL 长度有 2048 字符的上限,稍微复杂一点的查询就撑爆了。后来果断换成了 POST,参数放 Body 里,彻底避开 URL 长度限制。
实际应用场景
1. 查询参数传参
这是最最常见的场景。用户在搜索框输入 “iPhone 16 Pro Max”,前端发出去的请求大概是:
https://example.com/search?q=iPhone%2016%20Pro%20Max
空格变成了 %20。如果你用 + 号来传空格(q=iPhone+16+Pro+Max),接收方也要按 application/x-www-form-urlencoded 的规则来解码,两边不一致就会出现空格变成加号显示的 bug——这个坑我帮同事排查过两次。
2. RESTful API 路径中的中文资源
把资源 ID 或名称放在 URL 路径里(而不是查询参数里)的时候,编码同样生效:
GET /api/files/%E5%B7%A5%E4%BD%9C%E6%80%BB%E7%BB%93.pdf
不过说回正题,路径里的编码跟查询参数里的编码规则不太一样——路径里 + 不算空格,* 不编码。所以别一把梭用同一个编码逻辑对付整个 URL,要分部位处理。
3. encodeURI vs encodeURIComponent:一个被误解多年的 API
JavaScript 提供了两个编码函数,很多人搞不清什么时候用哪个:
encodeURI('https://a.com/文件?name=值&x=1')
// → "https://a.com/%E6%96%87%E4%BB%B6?name=%E5%80%BC&x=1"
// 保留 : / ? & = 不编码,适合编码整个 URL
encodeURIComponent('name=值&x=1')
// → "name%3D%E5%80%BC%26x%3D1"
// 连 = 和 & 都编码了,适合编码单个参数值
翻译成人话:encodeURI 假设你传给它的是完整的 URL,会保留下划线、协议分隔符等;encodeURIComponent 假设你传给它的是一个参数值,会把所有特殊字符全干掉,保证拼进 URL 后不会产生语义歧义。
我第一次用的时候正好把这两个用反了——对参数值用了 encodeURI,结果参数值里的 & 没被编码,硬生生把一个参数切成了两个,后端解析出来的数据直接少了一半字段。
| 对比维度 | encodeURI | encodeURIComponent |
|---|---|---|
| 编码范围 | 保留字符不编码(:/?#[]@!$&'()*+,;=) | 除了 A-Z a-z 0-9 - _ . ! ~ * ' ( ) 全编 |
| 用途 | 编码完整 URL | 编码 URL 片段(参数名/值) |
& 处理 | 不编码 | 编码为 %26 |
= 处理 | 不编码 | 编码为 %3D |
4. 后端自动解码
大部分 Web 框架会自动对 URL 解码。你用 Express、Spring、Django 拿到的 req.query.q 已经是解码后的 “iPhone 16 Pro Max” 了。但如果你手写了 URL 解析逻辑,或者在做反向代理时手动拼接转发 URL,就得小心双重编码问题——数据传进来编码一次,你又 encodeURIComponent 了一次,接收方解码一次后发现还是百分号,拿到的是乱码。
常见误区
误区一:URL 编码就是 HTML 实体编码
这两种编码长得有点像但完全不一样。URL 编码是 %XX,HTML 实体是 &#xXXXX; 或 &。前者是给 HTTP 传输用的,后者是给 HTML 页面渲染用的。之前在做一个邮件模板的项目里,同事把 HTML 实体写进了 <a href="..."> 的链接里,结果链接点击跳转后参数全错了——邮件客户端的渲染引擎处理链接时只认 URL 编码,不认识 HTML 实体。
误区二:把整个 URL 用 encodeURIComponent 编码
这样做的话,:// 会变成 %3A%2F%2F,URL 的正结构直接被毁了,浏览器根本不认识。正确的做法是分部位处理:协议和域名不动,路径分段编码,查询参数逐个 encodeURIComponent。
误区三:认为 encodeURI 已经够用,不需要 encodeURIComponent
说回正题。很多初级教程只教 encodeURI,甚至让新人误以为这是”URL 编码的正确姿势”。但实际上你在拼接查询参数的时候,必须用 encodeURIComponent——encodeURI 留下的那些保留字符正好就是查询参数的语法符号,不留神就会造成参数注入或者参数解析错误。
URL 编码 vs 其他编码方式
URL 编码经常被拿来和 Base64、HTML 实体编码做对比,但它们服务于完全不同的场景:
| URL 编码 | Base64 | HTML 实体编码 | |
|---|---|---|---|
| 格式 | %XX | A-Za-z0-9+/= | &xxx; 或 &#NNN; |
| 膨胀率 | 视内容而定 | ~33% | 视内容而定 |
| 场景 | URL 传输 | 二进制 → 文本 | HTML 渲染 |
| 可读性 | 差(百分号占视觉空间) | 较差 | 极差(尖括号加符号名) |
| 标准 | RFC 3986 | RFC 4648 | HTML5 规范 |
老实说,如果你想把一段二进制数据塞进 URL 参数里(比如 JWT 的某些用法),URL 编码不是最佳选择——Base64URL(把 +/ 换成 -_,去掉 =)才是标准做法。URL 编码的设计初衷是转义文本字符,不是打包二进制。
常见问题
Q: 浏览器地址栏里看到的中文 URL 是错觉吗?
是。现代浏览器地址栏会自动把 %E4%BD%A0%E5%A5%BD 显示成 “你好”,但复制粘贴到别的地方就变回百分号了。这是浏览器的显示优化,不是 URL 本身没被编码。你用 curl -v 抓包看原始请求,百分号全在。
Q: + 号到底是空格还是加号?
看上下文。在 application/x-www-form-urlencoded 的查询串里,+ 表示空格(历史原因,源自早期表单提交格式)。但在 URL 路径段里,+ 就是 + 本身。这就是为什么建议查询参数里空格用 %20 而不是 +——%20 在任何位置都明确表示空格,不会产生歧义。
Q: encodeURIComponent 编码后的字符串要解码用什么?
decodeURIComponent,一一对应。同理 encodeURI 的解码是 decodeURI。别混着用——decodeURI 去解 encodeURIComponent 的产物,里面的 %26 不会被解码回 &,因为它认为 & 不该出现在那个位置。
Q: 非 UTF-8 编码的网站怎么处理 URL 编码?
这是历史债务了。URL 编码标准(RFC 3986)规定的是字节的十六进制,至于这些字节按什么字符集解释,标准没说死。理论上是 UTF-8,但有些老的中文站点用的是 GBK/GB2312。如果一个 GBK 站点把”你好”编成 %C4%E3%BA%C3,你用 UTF-8 去解码得到的就是乱码。现实世界里几乎不用纠结——现在新系统默认全是 UTF-8,碰到老系统你自然会知道的。
Q: URL 编码能防止 XSS 吗?
不能,或者说只能算一层很薄的防护。如果你把用户输入用 URL 编码后放进 href 属性里,javascript: 协议头被编码成了 %6A%61%76%61%73%63%72%69%70%74%3A,浏览器解码后照样执行。防 XSS 要靠 CSP、HTML 实体编码、输入校验这些组合拳,不能指望 URL 编码。