JavaScript URL Decode — decodeURIComponent()
直接在浏览器中使用免费的 URL Decode Online,无需安装。
在线试用 URL Decode Online →百分号编码的字符串在 JavaScript 代码中无处不在——搜索查询以 q=standing+desk%26price%3A200 的形式到达, OAuth 重定向以 next=https%3A%2F%2Fdashboard.internal%2F 的形式, 存储路径以 reports%2F2025%2Fq1.pdf 的形式。 JavaScript 中的 URL 解码归结为从三个内置函数中选择正确的一个: decodeURIComponent()、 decodeURI() 和 URLSearchParams——它们之间的选择是我在生产代码库中 见过的大多数静默数据损坏的根本原因,特别是 + 作为空格的边界情况和双重解码问题。 如需在无需编写任何代码的情况下快速一次性解码, ToolDeck 的 URL Decoder 可在浏览器中即时处理。本 JavaScript URL 解码教程深入介绍了所有三个函数(ES2015+ / Node.js 10+): 何时使用每个函数、它们在空格和保留字符方面的差异、从文件和 HTTP 请求解码、安全的错误处理, 以及导致最隐蔽生产 bug 的四种常见错误。
- ✓decodeURIComponent() 解码所有百分号编码序列——它是解码单个查询参数值和路径段的正确选择
- ✓decodeURI() 保留 / ? & = # : 等结构性 URI 字符——仅在解码完整 URL 字符串时使用,绝不用于单个值
- ✓URLSearchParams.get() 返回已解码的值——在其之上调用 decodeURIComponent() 会导致双重解码
- ✓decodeURIComponent() 不会将 + 解码为空格——对于 application/x-www-form-urlencoded 格式的数据,使用 URLSearchParams 或在解码前替换 +
- ✓当输入来自用户数据时,始终将 decodeURIComponent() 包裹在 try/catch 中——单独的 % 或不完整的序列会抛出 URIError
什么是 URL 解码?
百分号编码(正式定义于 RFC 3986)将 URL 中不安全或具有结构意义的字符替换为 % 符号加两位十六进制数字——即该字符的 UTF-8 字节值。 URL 解码则是逆转这一过程:每个 %XX 序列被转换回其原始字节, 所得字节序列被解释为 UTF-8 文本。空格从 %20 解码,斜杠从 %2F,非 ASCII 字符 ü 从 %C3%BC(其两字节 UTF-8 表示)解码。
从不被编码的字符称为非保留字符——字母 A–Z 和 a–z、数字 0–9,以及 - _ . ~。其他所有字符要么在 URL 中具有 结构性作用(如 / 分隔路径段,或 & 分隔查询参数),要么在作为数据使用时 必须被编码。实际结果是:像 status=active&tier=premium 这样的搜索过滤器, 当作为单个查询参数值编码时,收到时看起来与原始内容完全不同。
// 百分号编码——在 HTTP 请求或 webhook 载荷中收到的格式 "q=price%3A%5B200+TO+800%5D%20AND%20brand%3ANorthwood%20%26%20status%3Ain-stock"
// URL 解码后——原始的 Elasticsearch 查询过滤器 "q=price:[200 TO 800] AND brand:Northwood & status:in-stock"
decodeURIComponent() — 解码值的标准函数
decodeURIComponent() 是 JavaScript 中 URL 解码的主力函数。它解码输入字符串中的每个 %XX 序列——包括在 URL 中具有结构意义的字符, 如 %2F(斜杠)、 %3F(问号)、 %26(&符号)和 %3D(等号)。这使其成为解码单个查询参数值和 路径段的正确选择,但对于解码完整 URL 则是错误选择——在完整 URL 中,这些结构性字符必须保持编码状态。 它是一个全局函数:在任何 JavaScript 环境中都无需导入。
最小工作示例
// 解码单个查询参数值——常见情况
const city = decodeURIComponent('S%C3%A3o%20Paulo') // 'São Paulo'
const district = decodeURIComponent('It%C3%A1im%20Bibi') // 'Itáim Bibi'
const category = decodeURIComponent('office%20furniture') // 'office furniture'
const filter = decodeURIComponent('price%3A%5B200+TO+800%5D') // 'price:[200+TO+800]'
// 注意:+ 不会被解码为空格——参见比较表中的 + 部分
console.log(city) // São Paulo
console.log(district) // Itáim Bibi
console.log(category) // office furniture
console.log(filter) // price:[200+TO+800]解码从查询参数中提取的重定向 URL
// 重定向 URL 在嵌入为参数值时被百分号编码
// 接收端:提取它,然后使用它——使用 URLSearchParams 无需手动解码
const incomingUrl = 'https://auth.company.com/callback' +
'?next=https%3A%2F%2Fdashboard.internal%2Freports%3Fview%3Dweekly%26team%3Dplatform' +
'&session_id=sid_7x9p2k'
const url = new URL(incomingUrl)
const rawNext = url.searchParams.get('next') // URLSearchParams 自动解码
const sessionId = url.searchParams.get('session_id') // 'sid_7x9p2k'
// rawNext 已经解码:'https://dashboard.internal/reports?view=weekly&team=platform'
// 不要再次调用 decodeURIComponent(rawNext)——那会是双重解码
const nextUrl = new URL(rawNext!)
console.log(nextUrl.hostname) // dashboard.internal
console.log(nextUrl.searchParams.get('view')) // weekly
console.log(nextUrl.searchParams.get('team')) // platform解码非 ASCII 和 Unicode 路径段
// 包含国际化路径段的 REST API
// 原始字符的每个 UTF-8 字节分别被百分号编码
const encodedSegments = [
'%E5%8C%97%E4%BA%AC', // 北京 (Beijing) — 每个字符 3 字节
'%E6%B7%B1%E5%9C%B3', // 深圳 (Shenzhen) — 每个字符 3 字节
'caf%C3%A9', // café — é 作为预组合 NFC
'S%C3%A3o%20Paulo', // São Paulo
]
encodedSegments.forEach(seg => {
console.log(decodeURIComponent(seg))
})
// 北京
// 深圳
// café
// São Paulo
// 从存储 API URL 中提取文件键
// 对象键包含 / 因此在路径段内被编码为 %2F
const storageUrl = 'https://storage.api.example.com/v1/objects/reports%2F2025%2Fq1-financials.pdf'
const rawKey = new URL(storageUrl).pathname.replace('/v1/objects/', '')
// .pathname 解码 URL 级别的编码,但 %2F(在 URL 级别为 %252F)保留
// 使用 decodeURIComponent 进行最后一步:
const fileKey = decodeURIComponent(rawKey) // 'reports/2025/q1-financials.pdf'
console.log(fileKey)decodeURIComponent() 当输入包含 % 后面没有跟两个有效十六进制数字时会抛出 URIError——例如字符串末尾的单独 % 或像 %GH 这样的序列。 在解码用户提供的输入时,始终将其包裹在 try/catch 中。安全的解码模式在下面的错误处理部分有所介绍。JavaScript URL 解码函数——字符参考
三个内置解码函数在解码哪些编码序列方面存在差异。 下表显示了在实践中最重要的字符的行为:
| 编码 | 字符 | decodeURIComponent() | decodeURI() | URLSearchParams |
|---|---|---|---|---|
| %20 | 空格 | 空格 ✅ | 空格 ✅ | 空格 ✅ |
| + | + (表单) | + (不变) | + (不变) | 空格 ✅ |
| %2B | + 字面量 | + ✅ | + ✅ | + ✅ |
| %26 | & | & ✅ | & (不变) ❌ | & ✅ |
| %3D | = | = ✅ | = (不变) ❌ | = ✅ |
| %3F | ? | ? ✅ | ? (不变) ❌ | ? ✅ |
| %23 | # | # ✅ | # (不变) ❌ | # ✅ |
| %2F | / | / ✅ | / (不变) ❌ | / ✅ |
| %3A | : | : ✅ | : (不变) ❌ | : ✅ |
| %40 | @ | @ ✅ | @ (不变) ❌ | @ ✅ |
| %25 | % | % ✅ | % ✅ | % ✅ |
| %C3%BC | ü | ü ✅ | ü ✅ | ü ✅ |
两个关键行是 + 和结构性字符 (%26、 %3D、 %3F)。URLSearchParams 将 + 解码为空格,因为它遵循 application/x-www-form-urlencoded 规范——对于 HTML 表单提交是正确的,但与 decodeURIComponent() 对同一字符的处理不同。 而 decodeURI() 静默跳过 %26、%3D 和 %3F——看起来是正确的,直到某个值实际上 包含编码的 & 或 = 符号。
decodeURI() — 解码完整 URL 而不破坏其结构
decodeURI() 是 encodeURI() 的对应函数。 它解码完整 URL 字符串,同时保留在 URI 中具有结构意义的字符: ; , / ? : @ & = + $ #。 这些字符保持其百分号编码形式(或如果在输入中以未编码形式出现则保持为字面字符)。 这使 decodeURI() 适合对路径或主机名中 可能包含非 ASCII 字符的完整 URL 进行清理,而不会意外破坏查询字符串结构。
清理包含非 ASCII 路径段的 URL
// 包含国际化路径段和结构化查询字符串的 CDN URL // decodeURI() 解码非 ASCII 路径,但保留 ? & = 完整 const encodedUrl = 'https://cdn.example.com/assets/%E5%8C%97%E4%BA%AC%2F2025%2Fq1-report.pdf' + '?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600' const readable = decodeURI(encodedUrl) console.log(readable) // https://cdn.example.com/assets/北京/2025/q1-report.pdf?token=eyJ0eXAiOiJKV1QiLCJhbGci&expires=1735689600 // ↑ 非 ASCII 已解码;? & = 已保留——URL 在结构上保持有效 // decodeURIComponent 会破坏 URL——: / ? & = 全部一次性解码 const broken = decodeURIComponent(encodedUrl) // 在这里看起来相同,但像 'https%3A%2F%2F...' 这样的 URL 会被破坏
URL 构造函数通常比 decodeURI() 更好。new URL(str) 规范化输入、验证其结构,并将 .pathname、 .searchParams 和 .hostname 作为已解码属性暴露出来。 将 decodeURI() 保留用于只需要字符串结果且无法使用 URL 构造函数的情况(例如,在没有全局 URL 类的非常旧的 Node.js 6 环境中)。URLSearchParams — 查询字符串的自动解码
URLSearchParams 是现代 JavaScript 中 解析查询字符串的惯用方式。每个由 .get()、 .getAll() 或迭代返回的值都会被自动解码—— 包括将 + 解码为 form 编码数据的空格。 它在所有现代浏览器和 Node.js 10+ 中全局可用,无需导入,并处理手动字符串分割会出错的边界情况。
解析传入的查询字符串
// 解析 webhook 回调或 OAuth 重定向查询字符串
const rawSearch =
'?event_id=evt_9c2f4a1b' +
'&product_name=Standing+Desk+Pro' +
'&filter=price%3A%5B200+TO+800%5D' +
'&tag=ergonomic&tag=adjustable' +
'&redirect=https%3A%2F%2Fdashboard.internal%2Forders%3Fview%3Dpending'
const params = new URLSearchParams(rawSearch)
console.log(params.get('event_id')) // 'evt_9c2f4a1b'
console.log(params.get('product_name')) // 'Standing Desk Pro' ← + 解码为空格
console.log(params.get('filter')) // 'price:[200+TO+800]' ← %3A 和 %5B 已解码
console.log(params.getAll('tag')) // ['ergonomic', 'adjustable']
console.log(params.get('redirect')) // 'https://dashboard.internal/orders?view=pending'
// 迭代所有参数
for (const [key, value] of params) {
console.log(`${key}: ${value}`)
}
// event_id: evt_9c2f4a1b
// product_name: Standing Desk Pro
// filter: price:[200+TO+800]
// tag: ergonomic
// tag: adjustable
// redirect: https://dashboard.internal/orders?view=pending从当前浏览器 URL 解析查询参数
// 当前 URL: https://app.example.com/search
// ?q=standing+desk
// &category=office+furniture
// &sort=price_asc
// &page=2
interface SearchFilters {
query: string | null
category: string | null
sort: string
page: number
}
function getSearchFilters(): SearchFilters {
const params = new URLSearchParams(window.location.search)
return {
query: params.get('q'), // 'standing desk'
category: params.get('category'), // 'office furniture'
sort: params.get('sort') ?? 'relevance',
page: Number(params.get('page') ?? '1'),
}
}
const filters = getSearchFilters()
console.log(filters.query) // standing desk (+ 自动解码)
console.log(filters.category) // office furniture从文件和 API 响应解码 URL 编码数据
在实际项目中,有两种场景会不断出现:处理磁盘上包含百分号编码数据的文件(访问日志、 数据导出、webhook 捕获文件),以及在 Node.js 服务器中解析传入 HTTP 请求的 URL。 两者遵循相同的原则——使用 URL 构造函数或 URLSearchParams 而非手动字符串分割—— 但细节有所不同。
从文件读取和解码 URL 编码记录
import { createReadStream } from 'fs'
import { createInterface } from 'readline'
// 文件:orders-export.txt——每行一条 URL 编码记录
// customer_name=%E5%BC%A0%E4%BC%9F&order_id=ord_9c2f4a&product=Standing+Desk+Pro&total=149.99%20CNY
// customer_name=%E6%9D%8E%E5%A8%9C&order_id=ord_7b3a1c&product=Ergonomic+Chair&total=89.00%20CNY
interface Order {
customerName: string | null
orderId: string | null
product: string | null
total: string | null
}
async function parseOrdersFile(filePath: string): Promise<Order[]> {
const fileStream = createReadStream(filePath, { encoding: 'utf-8' })
const rl = createInterface({ input: fileStream, crlfDelay: Infinity })
const orders: Order[] = []
for await (const line of rl) {
if (!line.trim()) continue
// URLSearchParams 自动将 + 解码为空格并处理 %XX 序列
const params = new URLSearchParams(line)
orders.push({
customerName: params.get('customer_name'), // '张伟'
orderId: params.get('order_id'), // 'ord_9c2f4a'
product: params.get('product'), // 'Standing Desk Pro'
total: params.get('total'), // '149.99 CNY'
})
}
return orders
}
const orders = await parseOrdersFile('./orders-export.txt')
console.log(orders[0])
// { customerName: '张伟', orderId: 'ord_9c2f4a', product: 'Standing Desk Pro', total: '149.99 CNY' }解析 Nginx 访问日志以解码搜索查询
import { createReadStream } from 'fs'
import { createInterface } from 'readline'
// Nginx 访问日志行如下所示:
// 192.168.1.42 - - [11/Mar/2026:10:23:01 +0000] "GET /api/search?q=standing%20desk%26brand%3ANorthwood&sort=price_asc HTTP/1.1" 200 1842
async function extractSearchQueries(logFile: string): Promise<string[]> {
const rl = createInterface({ input: createReadStream(logFile), crlfDelay: Infinity })
const queries: string[] = []
for await (const line of rl) {
// 从日志行中提取请求路径
const match = line.match(/"GET ([^ ]+) HTTP/)
if (!match) continue
try {
const requestUrl = new URL(match[1], 'http://localhost')
const query = requestUrl.searchParams.get('q')
if (query) queries.push(query) // URLSearchParams 自动解码
} catch {
// 跳过格式错误的行——访问日志可能包含截断的条目
}
}
return queries
}
const queries = await extractSearchQueries('/var/log/nginx/access.log')
console.log(queries)
// ['standing desk&brand:Northwood', 'ergonomic chair', 'monitor arm 27 inch']在 Node.js HTTP 服务器中解析查询参数
import http from 'http'
// 传入 URL: /api/products?q=standing+desk&warehouse=eu%2Dwest%2D1&minStock=10&cursor=eyJpZCI6MTIzfQ%3D%3D
const server = http.createServer((req, res) => {
// 构造完整 URL——第二个参数是 URL 构造函数所需的基础地址
const requestUrl = new URL(req.url!, 'http://localhost')
const searchQuery = requestUrl.searchParams.get('q') // 'standing desk'
const warehouseId = requestUrl.searchParams.get('warehouse') // 'eu-west-1'
const minStock = Number(requestUrl.searchParams.get('minStock') ?? '0')
const cursor = requestUrl.searchParams.get('cursor') // 'eyJpZCI6MTIzfQ=='
if (!warehouseId) {
res.writeHead(400, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'warehouse parameter is required' }))
return
}
const cursorData = cursor ? JSON.parse(Buffer.from(cursor, 'base64').toString()) : null
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ searchQuery, warehouseId, minStock, cursorData }))
})
server.listen(3000)在开发过程中当你需要检查编码后的 URL——以便在编写解析代码之前了解 webhook 发送的内容—— 将其直接粘贴到 ToolDeck 的 URL Decoder 中,无需运行脚本即可立即看到解码形式。
命令行 URL 解码
对于 shell 脚本、CI 流水线或快速一次性检查编码字符串,有几种方法无需编写完整脚本即可使用。 Node.js 单行命令跨平台通用;在 macOS 和 Linux 上, python3 也始终可用。
# ── Node.js 单行命令 ──────────────────────────────────────────────────
# 解码单个百分号编码值
node -e "console.log(decodeURIComponent(process.argv[1]))" "S%C3%A3o%20Paulo%20%26%20Rio"
# São Paulo & Rio
# 解析查询字符串并打印每个键=值对(已解码)
node -e "
const params = new URLSearchParams(process.argv[1])
for (const [k, v] of params) console.log(`${k} = ${v}`)
" "q=standing+desk&warehouse=eu%2Dwest%2D1&minStock=10"
# q = standing desk
# warehouse = eu-west-1
# minStock = 10
# 解码并美化打印 URL 编码的 JSON 体(webhook 调试中常见)
node -e "
const raw = decodeURIComponent(process.argv[1])
console.log(JSON.stringify(JSON.parse(raw), null, 2))
" '%7B%22event%22%3A%22purchase%22%2C%22amount%22%3A149.99%2C%22currency%22%3A%22CNY%22%7D'
# {
# "event": "purchase",
# "amount": 149.99,
# "currency": "CNY"
# }
# ── Python 单行命令(在大多数 macOS/Linux 系统上可用)─────────────
# unquote_plus 也将 + 解码为空格——对于 form 编码数据是正确的
python3 -c "
from urllib.parse import unquote_plus
import sys
print(unquote_plus(sys.argv[1]))
" "Standing+Desk+%26+Ergonomic+Chair"
# Standing Desk & Ergonomic Chair
# ── curl——从响应头记录并解码重定向 URL ──────────
curl -sI "https://api.example.com/short/abc123" | grep -i location | node -e "
const line = require('fs').readFileSync('/dev/stdin', 'utf8')
const url = line.replace(/^location:s*/i, '').trim()
console.log(decodeURIComponent(url))
"使用 decode-uri-component 进行优雅解码
内置的 decodeURIComponent() 对任何格式错误 的序列都会抛出 URIError——包括末尾没有跟两个 十六进制数字的 %。在处理用户提供的或第三方 URL 的生产代码中——搜索查询日志、来自电子邮件营销活动的点击 URL、爬取的网页数据——格式错误的 序列频繁出现,足以导致崩溃。 decode-uri-component 包(每周 npm 下载量约 3000 万次)通过返回原始序列而不是抛出异常来优雅地处理格式错误的序列。
npm install decode-uri-component # or pnpm add decode-uri-component
import decodeUriComponent from 'decode-uri-component'
// 原生函数对格式错误的输入抛出异常——可能导致请求处理程序崩溃
try {
decodeURIComponent('product%name%') // ❌ URIError: URI malformed
} catch (e) {
console.error('原生函数抛出:', (e as Error).message)
}
// decode-uri-component 返回原始序列而不是抛出
console.log(decodeUriComponent('product%name%'))
// product%name% ← 格式错误的序列保持原样,不抛出异常
// 有效序列被正确解码
console.log(decodeUriComponent('S%C3%A3o%20Paulo%20%26%20Rio'))
// São Paulo & Rio
// 混合:有效序列被解码,无效序列被保留
console.log(decodeUriComponent('Beijing%20Office%20%ZZ%20HQ'))
// Beijing Office %ZZ HQ ← %ZZ 不是有效的十六进制——保持原样
// 在日志处理流水线中的实际使用
const rawSearchQueries = [
'standing%20desk%20ergonomic',
'price%3A%5B200+TO+800%5D',
'50%25+off', // ← 使用 decodeURIComponent 会抛出(裸 %)
'wireless%20keyboard%20%26%20mouse',
]
const decoded = rawSearchQueries.map(q => decodeUriComponent(q))
console.log(decoded)
// ['standing desk ergonomic', 'price:[200+TO+800]', '50%25+off', 'wireless keyboard & mouse']在你希望立即知道是否收到意外输入的应用代码中,使用带 try/catch 的 decodeURIComponent()。在数据流水线、 日志处理器和 webhook 处理程序中使用 decode-uri-component,在那些你希望即使 某些输入包含无效编码也继续处理的场景中。
处理格式错误的百分号编码字符串
单独的 %、不完整的序列如 %A,或无效的对如 %GH 都会导致 decodeURIComponent() 抛出 URIError: URI malformed。任何用户控制的 输入——搜索查询、URL 片段、包含 URL 的表单字段、来自电子邮件营销链接的参数——都可能包含这些序列。 对于任何面向外部的代码,安全包装器是必不可少的。
常见场景的安全解码包装器
// ── 1. 基本安全解码——出错时返回原始字符串 ─────────────
function safeDecode(encoded: string): string {
try {
return decodeURIComponent(encoded)
} catch {
return encoded // 解码失败时返回原始输入——永不崩溃
}
}
// ── 2. 安全解码 + 将 + 处理为空格(用于 form 编码值)───────
function safeFormDecode(formEncoded: string): string {
try {
return decodeURIComponent(formEncoded.replace(/+/g, ' '))
} catch {
return formEncoded.replace(/+/g, ' ') // 即使其余失败也至少替换 +
}
}
// ── 3. 安全的完整查询字符串解析器→普通对象 ─────────────────
function parseQueryString(queryString: string): Record<string, string> {
const result: Record<string, string> = {}
try {
const params = new URLSearchParams(queryString)
for (const [key, value] of params) {
result[key] = value // URLSearchParams 在内部处理所有解码
}
} catch {
// 对完全格式错误的输入静默返回空对象
}
return result
}
// 使用
console.log(safeDecode('S%C3%A3o%20Paulo')) // São Paulo
console.log(safeDecode('search%20for%2050%25+off')) // search for 50% off(裸 %)
// → 实际上没问题;% 这里是 %25
console.log(safeDecode('malformed%string%')) // malformed%string%(不抛出)
console.log(safeFormDecode('standing+desk+pro')) // standing desk pro
console.log(parseQueryString('q=hello+world&tag=node%20js&page=2'))
// { q: 'hello world', tag: 'node js', page: '2' }常见错误
我见过这四种模式在生产环境中导致静默数据损坏或意外崩溃——通常只有当值恰好包含特殊字符时 才会出现,这意味着它们能通过单元测试,只在真实用户数据中才会暴露。
错误 1 — 对 URLSearchParams 值进行双重解码
问题: URLSearchParams.get() 返回一个已解码的字符串。 在其上调用 decodeURIComponent() 会对值进行双重解码—— 将解码输出中任何剩余的 % 变为 %25,从而损坏数据。 修复: 直接使用 URLSearchParams.get() 的值——无需额外解码。
// ❌ URLSearchParams 已经解码——再次解码会损坏包含 % 的值
const qs = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rawRate = qs.get('rate') // '50%' ← 已经解码
const wrongRate = decodeURIComponent(rawRate)
// '50%25' ← % 在第二次解码时变成了 %25——现在又错了// ✅ 直接使用 URLSearchParams.get()——解码是自动的
const qs = new URLSearchParams('rate=50%25&redirect=https%3A%2F%2Fdashboard.internal%2F')
const rate = qs.get('rate') // '50%' ← 正确,无需额外步骤
const redirect = qs.get('redirect') // 'https://dashboard.internal/'
const parsed = new URL(redirect!) // 可以安全构造——已经解码
console.log(parsed.hostname) // dashboard.internal错误 2 — 未将 + 解码为 form 编码数据的空格
问题: 表单提交和某些 OAuth 库将空格编码为 + (application/x-www-form-urlencoded)。对此数据调用 decodeURIComponent() 会将 + 保留为字面加号。 修复: 使用 URLSearchParams 解析 form 编码数据, 或在调用 decodeURIComponent() 前将 + 替换为空格。
// ❌ decodeURIComponent 将 + 视为字面加号,而非空格
// 表单体:customer_name=张伟&product=Standing+Desk+Pro&qty=2
const formBody = 'customer_name=%E5%BC%A0%E4%BC%9F&product=Standing+Desk+Pro&qty=2'
const [, rawVal] = formBody.split('&')[0].split('=')
const name = decodeURIComponent(rawVal)
console.log(name) // '张伟' ← 这里没问题,但 + 不会转换为空格// ✅ 使用 URLSearchParams——遵循 application/x-www-form-urlencoded 规范
const formBody = 'customer_name=%E5%BC%A0%E4%BC%9F&product=Standing+Desk+Pro&qty=2'
const params = new URLSearchParams(formBody)
console.log(params.get('customer_name')) // '张伟' ← 正确解码
console.log(params.get('product')) // 'Standing Desk Pro' ← + 正确解码为空格错误 3 — 对单个查询参数值使用 decodeURI()
问题: decodeURI() 不解码 %26 (&)、 %3D (=) 或 %3F (?)——这些字符在参数值内部经常被编码。 用它解码单个值会使这些序列保持完整,静默产生错误的输出。 修复: 对单个值使用 decodeURIComponent(); 将 decodeURI() 保留用于完整 URL 字符串。
// ❌ decodeURI 不解码 & 和 =——值保持错误状态 const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west' const filter = decodeURI(encodedFilter) console.log(filter) // 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west' ← %3D 和 %26 未解码
// ✅ decodeURIComponent 解码所有序列,包括 & 和 = const encodedFilter = 'status%3Dactive%26tier%3Dpremium%26region%3Deu-west' const filter = decodeURIComponent(encodedFilter) console.log(filter) // 'status=active&tier=premium®ion=eu-west' ← 正确解码
错误 4 — 对用户输入的 decodeURIComponent 未包裹 try/catch
问题: 单独的 % 后面没有两个十六进制数字—— 在用户输入的搜索查询中很常见("50% 折扣"、"100% 棉")——导致 decodeURIComponent() 抛出 URIError: URI malformed, 如果未捕获会导致请求处理程序崩溃。 修复: 当输入来自用户数据、URL 片段或外部系统时, 始终包裹在 try/catch 中。
// ❌ 用户在搜索框输入 '100% 棉'——裸 % 导致服务器崩溃
// GET /api/search?q=100%25+棉 ← 这个具体情况没问题(%25 = %)
// GET /api/search?q=100%+棉 ← 这个会崩溃(% 后面没有 2 个十六进制数字)
app.get('/api/search', (req, res) => {
const query = decodeURIComponent(req.query.q as string)
// ↑ 对 '100% 棉' 抛出 URIError: URI malformed → 未处理的 500 错误
})// ✅ 包裹在 try/catch 中——解码失败时回退到原始输入
app.get('/api/search', (req, res) => {
let query: string
try {
query = decodeURIComponent(req.query.q as string)
} catch {
query = req.query.q as string // 使用原始值而不是崩溃
}
// 继续安全处理——query 要么是已解码的,要么是原始的
})decodeURIComponent vs decodeURI vs URLSearchParams——快速对比
| 方法 | 解码 %XX | 将 + 解码为空格 | 格式错误时抛出 | 解码 & = ? # | 使用场景 | 需要安装 |
|---|---|---|---|---|---|---|
| decodeURIComponent() | ✅ 全部 | ❌ 否 | ✅ URIError | ✅ 是 | 单个值和路径段 | No |
| decodeURI() | ✅ 大多数 | ❌ 否 | ✅ URIError | ❌ 否 | 完整 URL 字符串 | No |
| URLSearchParams | ✅ 全部 | ✅ 是 | ❌ 静默 | ✅ 是 | 带自动解码的查询字符串解析 | No |
| URL constructor | ✅ 全部 | ✅ 是 | ✅ TypeError | ✅ 是 | 完整 URL 解析和规范化 | No |
| decode-uri-component | ✅ 全部 | ❌ 否 | ❌ 静默 | ✅ 是 | 批量解码,容错格式错误输入 | npm install |
| querystring.unescape() | ✅ 全部 | ❌ 否 | ❌ 静默 | ✅ 是 | 旧版 Node.js(v16 已废弃) | No (built-in) |
在绝大多数情况下,选择归结为三种场景。使用 URLSearchParams 解析查询字符串——它自动处理 解码、+ 作为空格的规则以及重复键。使用带 try/catch 的 decodeURIComponent() 处理单个值或路径段, 特别是当你期望段内有 %2F 编码的斜杠时。仅当你有完整 URL 字符串, 其路径中包含非 ASCII 字符,并且需要结构性字符 (/ ? & =) 在输出中保持编码状态时, 才使用 decodeURI()。
常见问题
相关工具
如需在不编写任何代码的情况下一键解码,将你的百分号编码字符串直接粘贴到 ToolDeck 的 URL Decoder 中——它在浏览器中即时解码,结果可以直接复制到你的代码或终端中。
Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.
Marcus specialises in JavaScript performance, build tooling, and the inner workings of the V8 engine. He has spent years profiling and optimising React applications, working on bundler configurations, and squeezing every millisecond out of critical rendering paths. He writes about Core Web Vitals, JavaScript memory management, and the tools developers reach for when performance really matters.