「中」在网址里是 %E4%B8%AD,「文」是 %E6%96%87,两个字一共 6 个字节、18 个字符。
同一个空格,encodeURIComponent 给的是 %20,表单编码给的是加号。
我把 22 组输入丢进一个网页逐个拆到字节,页面上 27 项断言全绿,耗时 0.50 毫秒。

在线案例与源码:atomgit.com/deli007/dem… 成品只有一个 index.html,双击就能打开。下面每个数字都是我在页面里读回来、又用一份 Python 脚本复算过的。
一、为什么值得搞清楚:百分号后面不是加密,是另一种读法
%E4%B8%AD 不是密文,它就是三个字节:E4、B8、AD。
浏览器拿 UTF-8 去读这三个字节,读出「中」。
网址规范只允许 ASCII 字符,中文、日文、emoji 都得先变成字节,再写成 %XX。
所以网址里的「乱码」多半不是文件坏了,是同一串字节被另一种编码读了。
这个规则平时感知不强,一到拼接参数、生成分享链接就会咬人。
我这次想弄清楚三件事:哪些字符会被编码、三种 API 差在哪、空格为什么有两种写法。
二、准备环境:进入码道 Web,把「不许写死结果」写进需求
浏览器打开码道 Web 版:devcloud.cn-north-4.huaweicloud.com/chat?source… ,登录后在对话框里写需求,就能拿到一个能跑的单文件应用,不用装软件。
码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。
这轮需求里最值钱的是三句话。
第一句是把 22 组用例的输入直接写进需求,别让模型自己回忆。
第二句是结果必须现场算:编码走 encodeURIComponent,字节走 TextEncoder。
第三句是失败路径要显示浏览器抛出的原始错误,并保留上一次有效结果。
第三句看着啰嗦,但它决定这个页面能不能当尺子用。

三、一屏看清:五个面板加一枚自检徽章
页面从上往下是五块。
第一块是编码与解码,输入即时联动,两个方向都能看。
第二块是字节拆解,按序号列出十六进制、十进制和 %XX 三列。
第三块是三种 API 对照,把 encodeURI、encodeURIComponent、表单编码并排摆出来。
第四块是 22 组用例实测表,每行给原始输入、编码结果、字节数和说明。
第五块是内置断言,页面加载时自己跑一次。
右上角的徽章就是自检结果,27 项全过是绿色的 27/27。
四、实测:22 组输入逐个拆开
这张表的结果不是抄来的,是页面每次加载时用 encodeURIComponent 现场算的。
| 序号 | 原始输入 | encodeURIComponent 结果 | UTF-8 字节 | 说明 |
|---|---|---|---|---|
| 1 | 中文 | %E4%B8%AD%E6%96%87 | 6 | 中文 |
| 2 | a中b | a%E4%B8%ADb | 5 | 中英混排 |
| 3 | a b | a%20b | 3 | 空格 |
| 4 | a+b | a%2Bb | 3 | 加号 |
| 5 | a/b | a%2Fb | 3 | 斜杠 |
| 6 | ? | %3F | 1 | 问号 |
| 7 | # | %23 | 1 | 井号 |
| 8 | = | %3D | 1 | 等号 |
| 9 | & | %26 | 1 | 和号 |
| 10 | % | %25 | 1 | 百分号 |
| 11 | ~ | ~ | 1 | 波浪线 |
| 12 | - | - | 1 | 连字符 |
| 13 | _ | _ | 1 | 下划线 |
| 14 | . | . | 1 | 点号 |
| 15 | ! | ! | 1 | 感叹号 |
| 16 | * | * | 1 | 星号 |
| 17 | ' | ' | 1 | 单引号 |
| 18 | ( | ( | 1 | 左括号 |
| 19 | emoji | %F0%9F%98%80 | 4 | emoji |
| 20 | あ | %E3%81%82 | 3 | 日文平假名 |
| 21 | 中文 测试 | %E4%B8%AD%E6%96%87%20%E6%B5%8B%E8%AF%95 | 13 | 中文加空格 |
| 22 | %25 | %2525 | 3 | 双重编码 |

第 11 到 18 行的结果和输入长得一模一样,这不是没算,是这些字符本来就不用编码。
第 22 行最能说明问题:输入本身已经是 %25,再编码一次变成 %2525。
一个参数被编码两次,服务端解一次就只能拿到 %25,拿不到真正的 %。
五、三种 API 差在哪:同一个斜杠,两种结果
encodeURI 把整个网址当成一个整体,所以 ; , / ? : @ & = + 和 # 都不动它。
encodeURIComponent 假设你在拼参数的值,这些字符全部要转义。
表单编码另起一套:空格写成加号,其余和 encodeURIComponent 一样。
| 输入 | encodeURI | encodeURIComponent | 表单编码 |
|---|---|---|---|
| a/b?c=d&e=f | a/b?c=d&e=f | a%2Fb%3Fc%3Dd%26e%3Df | a%2Fb%3Fc%3Dd%26e%3Df |
| 中 文 | %E4%B8%AD%20%E6%96%87 | %E4%B8%AD%20%E6%96%87 | %E4%B8%AD+%E6%96%87 |
| 100% | 100%25 | 100%25 | 100%25 |
| 两个 emoji | %F0%9F%98%80%F0%9F%98%80 | %F0%9F%98%80%F0%9F%98%80 | %F0%9F%98%80%F0%9F%98%80 |
前两行的差别就是全部:路径里的斜杠可以留,参数值里的斜杠必须转。
有个反直觉的点:表单编码把空格写成加号,但网址路径里的加号就是加号本身。
所以用 decodeURIComponent 解 %2B 得到的是加号,不是空格。
六、那 6 个字节是怎么来的
「中文」两个字,页面拆出来正好 6 行。
| 序号 | HEX | DEC | %XX |
|---|---|---|---|
| 1 | E4 | 228 | %E4 |
| 2 | B8 | 184 | %B8 |
| 3 | AD | 173 | %AD |
| 4 | E6 | 230 | %E6 |
| 5 | 96 | 150 | %96 |
| 6 | 87 | 135 | %87 |
UTF-8 里一个中文占 3 字节,所以两个汉字正好 6 字节、18 个字符。
emoji 占 4 字节,日文平假名占 3 字节,ASCII 占 1 字节。
这些数字全部由 TextEncoder 现算,页面里没有预先写死的表。
七、验证与踩坑:27 项自检加一处真实不一致
页面的自检是 22 条用例加 5 条边界,一共 27 项。
我点了一次运行自检,结果是 27/27 通过,页面自己记的耗时 0.50 毫秒。

不信页面自己报的数,我又用 Python 标准库的 urllib.parse.quote 重算了一遍。
脚本跑出来 30/30 项一致,含 22 组编码结果、5 组字节数、%25 解码、空格两种写法和保留字符集合。
失败路径我拿 %E4%B8 试的:一个百分号后面本该跟两个十六进制数字,这里只有两个,字节数不完整。
页面把浏览器抛的原文贴了出来:URIError: URI malformed。
同时它保留了上一次的有效结果,没有把解码框清空。

再往下是我这轮真揪出来的两处问题。
第一处是真实的不一致:输入一个孤立的代理字符,encodeURIComponent 直接抛 URIError,而同一页面的 TextEncoder 把它替换成 U+FFFD,也就是 EF BF BD 三个字节。
同一个输入,两个面板给出两种答案。这不算页面算错,是两个 API 的规范本来就不同。
但页面当时把错误文本当成了编码结果,统计行写着字符 23、UTF-8 字节 23,看上去像一串正经输出。
我把统计行改成「编码失败:URIError: URI malformed · 没有有效编码串」,错误就不再冒充结果。
第二处是排版:22 行表格和 6 行字节表被塞进固定高度的滚动容器,一屏是放得下,看全却要来回滚。
我把两处高度限制都去掉了,表格改成一次显示完整,截图也就是现在这样。
八、边界与取舍
下面这几条是我实测出来的真边界,不是免责声明式的套话。
第一,页面用 decodeURIComponent 解码,它不把加号当空格,所以表单编码串里的加号会原样留下。
第二,输入 10000 个「中」时,字节表要渲染 30000 行,页面耗时 178.9 毫秒,长输入下逐字节表会明显拖慢。
第三,自检只在页面加载和手动点击时跑一次,它不是持续监控。
第四,这一页做的是 URL 编码层,不做字符集转换,UTF-8 与 GBK 的对照不在这里。
第五,表单编码的空格规则只属于 application/x-www-form-urlencoded,网址路径里没有这一套。
九、直接下载试玩
这个 Demo 已经完整开源,源代码、本文档、五张截图和一个复算脚本都在公开仓库里。
- Demo Park 仓库:atomgit.com/deli007/dem…
- 本案例目录:atomgit.com/deli007/dem…
把 index.html 下载到本地,用浏览器打开就能改输入、切 API,看着字节和编码串一起变。
十、本地复现与下一步
本地跑两步就够。
git clone https://atomgit.com/deli007/demo_park.git
cd demo_park\codearts-url-encoding-lab
python -m http.server 8080
浏览器打开 http://localhost:8080/index.html ,把编码输入从「中文」换成「中 文」,能看到字节数从 6 涨到 13,空格那一位变成 %20。
复算脚本单独跑一次,命令在案例目录里。
python verify_url_encoding.py
下一步我打算补两件事:一是把历史编码相关的坑做成对照表,比如 %2525 这类双重编码在不同框架里怎么解;二是给逐字节表加个上限,超过一定长度只显示前后各若干字节,免得长输入把页面拖慢。
十一、使用码道体会
- 把判定用的输入直接写进需求,比写「覆盖常见场景」有用得多,22 组用例是我在需求里逐条列出来的。
- 结果必须让页面现场算,凡是让模型回忆表格和数值的地方,都要在验收时多留个心眼。
- 失败路径要连错误原文一起写进需求,这一条让页面在非法输入时给了我能核对的东西。
- 自检项要由页面自己报数,27/27 这种数字比「测试通过」四个字可信。
- 页面生成的布局未必合你的阅览习惯,表格滚不滚动这种小事,自己动手改比重新生成快。
- 同一个输入两个面板给出不同答案时,先去查规范差异,别急着改代码。
一句实在的话:码道把可运行的第一版做得很快,但这一页能不能当尺子用,取决于你把验收条件写得多具体。