Skip to content
encode2026-07-255 分钟阅读

URL 编码(正式名称是"百分号编码")是开发者每天都会遇到却很少深究的东西,直到某天出了问题:文件名里的空格变成了 %20,中文字符变成了一堵百分号墙,查询参数里的 + 莫名其妙在服务端变成了空格。本文讲清楚百分号编码到底怎么工作、为什么存在,以及如何在不引入 bug 的前提下处理那些棘手场景。

百分号编码的工作原理

URL 只能包含有限的 ASCII 字符集:字母、数字以及少量特殊字符(-_.~)。其他一切——空格、标点、非拉丁文字、emoji——在安全地出现在 URL 中之前都必须先编码。

机制很简单:

  1. 取出字符。
  2. 转换为 UTF-8 字节表示。
  3. 把每个字节替换为 % 加两位大写十六进制值。

对于单字节 ASCII 字符,这会生成一个百分号编码三元组。对于多字节字符(如中文、日文或 emoji),你会得到多个三元组——每个字节一个。

为什么空格变成 %20(或 +)

空格字符(ASCII 0x20)是最常被编码的字符,它有一种容易让人混淆的双重表示:

  • %20 —— 空格的标准百分号编码,在 URL 的任何位置(路径、查询、片段)都有效。
  • + —— 一种遗留的空格编码,只在 application/x-www-form-urlencoded 数据中有效(即表单提交和查询字符串)。
# 在 URL 路径中,空格必须是 %20:
https://example.com/files/my%20document.pdf

# 在查询字符串中,两种都行(但在某些解析器中含义不同):
https://example.com/search?q=hello%20world   # %20 = 空格
https://example.com/search?q=hello+world     # + = 空格(表单编码)

混乱的根源在于:+ 在 URL 路径里是字面量加号,但在查询字符串里表示空格。如果你在构建一个接收查询参数的 API,两种都要处理。如果你在生成 URL,挑一种约定并保持一致。

编码中文字符(及其他多字节 UTF-8)

这里事情变得有趣。字符 (U+4E2D)的 UTF-8 编码是三个字节:E4 B8 AD。所以它的百分号编码形式是:

中 → %E4%B8%AD

完整示例:

原文:    你好世界
UTF-8:   E4 BD A0  E5 A5 BD  E4 B8 96  E7 95 8C
编码后:  %E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C

每个中文字符百分号编码后变成 9 个字符(3 字节 × 每字节 3 字符)。这就是为什么包含中文的 URL 会迅速变长。

JavaScript 中的做法:

// encodeURIComponent 处理 UTF-8 百分号编码
encodeURIComponent('中')       // "%E4%B8%AD"
encodeURIComponent('你好')     // "%E4%BD%A0%E5%A5%BD"
encodeURIComponent('hello 世界') // "hello%20%E4%B8%96%E7%95%8C"

// decodeURIComponent 反向解码
decodeURIComponent('%E4%B8%AD') // "中"

其他常见特殊字符

| 字符 | 编码 | 场景 | |-----------|---------|---------| | 空格 | %20+ | 路径用 %20,表单用 + | | & | %26 | 查询值中必须编码(分隔参数) | | = | %3D | 查询值中必须编码(分隔键值) | | # | %23 | 必须编码(启动片段标识符) | | ? | %3F | 路径中必须编码(启动查询字符串) | | / | %2F | 路径段中必须编码(分隔路径段) | | @ | %40 | 路径中编码以避免 userinfo 混淆 | | | %E2%82%AC | 3 字节 UTF-8 | | 🎉 | %F0%9F%8E%89 | 4 字节 UTF-8(emoji) |

常见错误及规避方法

错误 1:双重编码

最常见的 bug。你编码了一个值,然后框架又编码了一次:

// 错误:双重编码
const param = encodeURIComponent('hello world');  // "hello%20world"
const url = `https://api.example.com/search?q=${encodeURIComponent(param)}`;
// 结果:q=hello%2520world(% 被编码成了 %25)
// 服务端收到:"hello%20world"(字面字符串,不是空格)

修复方法:在组装 URL 的那一层只编码一次。大多数 HTTP 库(axios、fetch 配 URLSearchParams)会替你处理编码:

// 正确:让 URLSearchParams 处理编码
const params = new URLSearchParams({ q: 'hello world', name: '张三' });
const url = `https://api.example.com/search?${params.toString()}`;
// 结果:q=hello+world&name=%E5%BC%A0%E4%B8%89

错误 2:混用 + 和 %20

如果后端把 + 解码为空格,而前端发的是 %20(或反过来),就会出现不匹配:

// 服务端期望表单编码(+ 表示空格)
// 但客户端发送:
fetch('/api?q=hello%20world')  // 有的服务端能正确解码 %20,有的不行

// 最稳妥:用 URLSearchParams 产出一致的编码
const params = new URLSearchParams();
params.set('q', 'hello world');
// 始终产出:q=hello+world

错误 3:编码整个 URL

不要盲目编码完整 URL——否则会把本该作为结构的 :///?& 也编码掉:

// 错误:破坏了 URL 结构
encodeURIComponent('https://example.com/path?q=hello world')
// "https%3A%2F%2Fexample.com%2Fpath%3Fq%3Dhello%20world"

// 正确:只编码动态部分
const baseUrl = 'https://example.com/path';
const query = encodeURIComponent('hello world');
const url = `${baseUrl}?q=${query}`;

错误 4:假设只有 ASCII

如果你的应用要处理国际化输入(中文用户名、阿拉伯文文件名、日文搜索词),必须使用 UTF-8 百分号编码。JavaScript 中老的 escape() 函数用的是非标准编码——永远不要用它:

// 永远不要用 escape() —— 已废弃且非标准
escape('中')  // "%u4E2D" —— 这不是合法的百分号编码

// 始终用 encodeURIComponent()
encodeURIComponent('中')  // "%E4%B8%AD" —— 正确的 UTF-8 百分号编码

实战示例

构建包含混合内容的 URL

function buildSearchUrl(query, category, page) {
  const params = new URLSearchParams({
    q: query,          // "北京 weather" → q=%E5%8C%97%E4%BA%AC+weather
    category: category, // "food & drink" → category=food+%26+drink
    page: page
  });
  return `https://example.com/search?${params.toString()}`;
}

buildSearchUrl('北京 weather', 'food & drink', 1);
// "https://example.com/search?q=%E5%8C%97%E4%BA%AC+weather&category=food+%26+drink&page=1"

安全地解码 URL 参数

function getUrlParams(url) {
  const { searchParams } = new URL(url);
  const params = {};
  for (const [key, value] of searchParams) {
    params[key] = value;  // URL API 已经解码好了
  }
  return params;
}

getUrlParams('https://example.com/search?q=%E4%BD%A0%E5%A5%BD&tag=a%2Bb');
// { q: "你好", tag: "a+b" }
// 注意:%2B 解码为字面量 +,而 + 解码为空格

处理带特殊字符的文件名

// 下载一个中文名文件
const filename = '年度报告 2026.pdf';
const encodedFilename = encodeURIComponent(filename);
// "%E5%B9%B4%E5%BA%A6%E6%8A%A5%E5%91%8A%202026.pdf"

const downloadUrl = `/api/files/${encodedFilename}`;
// 服务端必须解码路径段才能找到实际文件

速查表:哪里需要编码什么

| URL 组成部分 | 需要编码的字符 | |---------------|-------------------------------| | 路径段 | /?#[]@!$&'()*+,;=、空格、非 ASCII | | 查询参数值 | &=+#、空格、非 ASCII | | 片段 | 空格、非 ASCII(浏览器在这里比较宽松)|

最简单的规则:如果你要把一个动态值插入 URL,用 encodeURIComponent()(适用于路径段和查询值)或 URLSearchParams(适用于查询字符串)。剩下的细节交给平台处理。


需要快速编解码 URL 组件?DevToolkit Pro 的 URL 编解码器 工具完全在浏览器中处理百分号编码——粘贴字符串,立即得到编码结果,数据不会发送到任何服务器。它支持多字节 UTF-8 字符、+%20 的差异处理,以及完整 URL 编码,方便你在上线前验证输出。


ad