人人都会AI编程

9.3 兄弟组件通信:状态提升、发布订阅模式

更新时间:2026-07-10

在 HTTP 服务器和客户端的开发中,处理 URL 是极其高频的操作。无论是解析请求路径、提取查询参数,还是构造跳转链接、拼接 API 地址,都离不开对 URL 字符串的解析与组装。Node.js 为此提供了两个核心模块:url 模块负责完整 URL 的解析与格式化,querystring 模块专注于查询字符串(即 ? 后面的部分)的解析与序列化。

随着 ECMAScript 标准的演进,Node.js 也内置了与浏览器一致的 WHATWG URL API(全局 URL 类),它正在逐步成为处理 URL 的主流方式。但传统的 urlquerystring 模块依然广泛存在于遗留代码中,并且在一些特定场景下有其独特的价值。因此,掌握新旧两套 API 对于实际开发十分必要。

9.3.1 传统 url 模块:解析与格式化

url 模块提供了两个主要的 API:url.parse() 用于将 URL 字符串解析成对象,url.format() 用于将对象还原为 URL 字符串,以及 url.resolve() 用于拼接基础 URL 和相对路径。

解析 URL:url.parse()

url.parse(urlString, parseQueryString, slashesDenoteHost) 接受一个 URL 字符串,返回一个包含各组成部分的对象:

const url = require('url');

const parsed = url.parse('https://user:pass@example.com:8080/path/to/page?query=string&lang=zh#section');
console.log(parsed);

输出的对象结构如下:

{
  protocol: 'https:',
  slashes: true,
  auth: 'user:pass',
  host: 'example.com:8080',
  port: '8080',
  hostname: 'example.com',
  hash: '#section',
  search: '?query=string&lang=zh',
  query: 'query=string&lang=zh',   // 默认是字符串
  pathname: '/path/to/page',
  path: '/path/to/page?query=string&lang=zh',
  href: 'https://user:pass@example.com:8080/path/to/page?query=string&lang=zh#section'
}

这里的字段非常直观,与 URL 的各个组成部分一一对应。需要注意的是,默认情况下 query 字段保留原始字符串。如果希望将查询字符串自动解析为对象,可以传入第二个参数 true

const parsed2 = url.parse('https://example.com?a=1&b=2', true);
console.log(parsed2.query); // { a: '1', b: '2' }

第三个参数 slashesDenoteHost 用于处理一些特殊格式,比如 //example.com 这样的协议相对 URL。通常很少用到。

格式化 URL:url.format()

url.format(urlObject) 执行与 parse 相反的操作,将对象合成为 URL 字符串:

const url = require('url');

const urlObj = {
  protocol: 'https',
  hostname: 'example.com',
  port: 3000,
  pathname: '/api/users',
  query: { page: 1, size: 20 }
};

console.log(url.format(urlObj));
// 输出:https://example.com:3000/api/users?page=1&size=20

format 会自动从 query 对象生成查询字符串,并将各字段按标准格式拼接。需要注意的是,如果同时提供了 hosthostname + porthost 会优先。建议始终使用 hostnameport 的组合,避免歧义。

URL 拼接:url.resolve()

url.resolve(from, to) 可以像浏览器解析相对路径那样,根据基础 URL 和相对路径生成绝对 URL:

const url = require('url');

console.log(url.resolve('/one/two', 'three'));       // /one/three
console.log(url.resolve('http://example.com/', '/api')); // http://example.com/api
console.log(url.resolve('http://example.com/one/', '/api')); // http://example.com/api
console.log(url.resolve('http://example.com/one/', 'two')); // http://example.com/one/two

这个方法在处理重定向或跳转链接时很方便,但不幸的是 url.resolve() 已经被标记为废弃,推荐使用 WHATWG URL API 的 new URL(to, from) 来替代,后面会讲到。

传统 url 模块的现状

url.parseurl.format 虽然没有被正式废弃,但 Node.js 官方文档明确指出推荐使用 WHATWG URL API,因为它的行为与浏览器完全一致,且更符合现代 Web 标准。这两个旧 API 仍然保留主要是为了向后兼容。在新项目中,应当优先使用 URL 类(new URL())。不过,维护老项目时理解它们依然重要。

9.3.2 querystring 模块:专门处理查询参数

querystring 模块专注于处理 URL 中 ? 后面的查询字符串部分。它提供了 querystring.parse()querystring.stringify() 两个方法。

解析查询字符串:querystring.parse()

const qs = require('querystring');

const parsed = qs.parse('name=张三&age=30&hobby=篮球&hobby=音乐');
console.log(parsed);
// 输出:{ name: '张三', age: '30', hobby: ['篮球', '音乐'] }

默认情况下,querystring.parse 会自动将相同键名的参数收集到数组中,非常实用。它还支持自定义分隔符和等号:

const parsed2 = qs.parse('name:张三;age:30', ';', ':');
console.log(parsed2); // { name: '张三', age: '30' }

以及一个 maxKeys 选项可以限制解析的参数数量,用于防范恶意提交的海量参数攻击:

const safe = qs.parse(longString, '&', '=', { maxKeys: 100 });

序列化查询字符串:querystring.stringify()

将对象转换为查询字符串:

const qs = require('querystring');

const str = qs.stringify({ name: '张三', age: 30, hobby: ['篮球', '音乐'] });
console.log(str);
// 输出:name=张三&age=30&hobby=篮球&hobby=音乐

同样可以指定分隔符和等号:

const str2 = qs.stringify({ a: 1, b: 2 }, ';', ':');
console.log(str2); // a:1;b:2

编码与解码

querystring 还提供了 escapeunescape 方法,分别用于对查询字符串值进行编码和解码。默认使用的是 encodeURIComponentdecodeURIComponent,但可以按需覆盖,例如使用更宽松的编码规则。

const qs = require('querystring');

const encoded = qs.escape('你好 世界');
console.log(encoded); // %E4%BD%A0%E5%A5%BD%20%E4%B8%96%E7%95%8C

局限性

querystring 模块只处理简单的键值对,不支持嵌套对象结构(如 a[b]=c 这种格式)。如果需要处理更复杂的参数序列化(例如嵌套对象或数组),通常需要 npm 上的 qs 库(注意区分,qs 库的功能远强于内置 querystring),或者直接使用 JSON 格式传输数据。

9.3.3 现代方式:WHATWG URL API

从 Node.js 7 开始,Node.js 引入了与浏览器完全一致的 URLURLSearchParams 类,并且它们已逐渐成为处理 URL 和查询字符串的推荐方式。这套 API 的行为遵循最新的 URL 标准,自动处理编码、解码、解析等细节,并且支持跨平台一致性。

URL 类:解析与构造

const myURL = new URL('https://user:pass@example.com:8080/path/to/page?page=1&size=10#section');

console.log(myURL.protocol); // 'https:'
console.log(myURL.hostname); // 'example.com'
console.log(myURL.port);     // '8080'
console.log(myURL.pathname); // '/path/to/page'
console.log(myURL.search);   // '?page=1&size=10'
console.log(myURL.hash);     // '#section'
console.log(myURL.origin);   // 'https://example.com:8080'

URL 对象的所有属性都是可读可写的,可以直接修改后通过 myURL.href 获取完整的新 URL,或者使用 myURL.toString()

除了解析绝对 URL,还可以通过基础 URL 来构造相对路径:

const base = 'https://example.com/api/v1';
const relative = '/users?active=true';
const resolved = new URL(relative, base);
console.log(resolved.href); // 'https://example.com/users?active=true'

这正是被用来替代 url.resolve() 的方式,并且逻辑完全符合官方 URL 标准。

URLSearchParams 类:处理查询字符串

URLSearchParams 专门用于解析和修改查询字符串。它既可以作为 URL 对象的一个属性(myURL.searchParams)访问,也可以单独使用。

const params = new URLSearchParams('page=1&size=10&sort=asc');

// 获取单个值
console.log(params.get('page')); // '1'

// 设置值(覆盖或新增)
params.set('page', '2');
params.append('filter', 'active');

// 获取同键多值
console.log(params.getAll('filter')); // ['active']

// 迭代
for (const [key, value] of params) {
  console.log(`${key}: ${value}`);
}

// 删除
params.delete('sort');

// 输出序列化后的字符串
console.log(params.toString()); // 'page=2&size=10&filter=active'

使用 URLSearchParams 的一大优势是它内建编码,无需手动处理特殊字符:

const params = new URLSearchParams();
params.append('name', '张三');
params.append('message', '你好&世界');
console.log(params.toString()); // 'name=%E5%BC%A0%E4%B8%89&message=%E4%BD%A0%E5%A5%BD%26%E4%B8%96%E7%95%8C'

同时,它还提供了 has()entries()keys()values() 等方法,让遍历和检查参数变得非常方便。

URL 与查询字符串的配合使用

在实际开发中,通常结合两个类来操作完整的 URL:

const myURL = new URL('https://example.com/search');

// 通过 searchParams 直接修改查询参数
myURL.searchParams.set('q', 'Node.js');
myURL.searchParams.append('page', '1');

console.log(myURL.href); // 'https://example.com/search?q=Node.js&page=1'

如果需要从请求对象(例如 http.IncomingMessage)中解析带查询字符串的 URL,可以直接使用:

const http = require('http');

const server = http.createServer((req, res) => {
  // req.url 是带查询字符串的路径,如 '/api/users?id=123'
  const myURL = new URL(req.url, `http://${req.headers.host}`);
  const id = myURL.searchParams.get('id');
  // ...
});

注意这里需要提供基础 URL,因为 req.url 是一个相对路径。用 http://${req.headers.host} 作为基础 URL 可以完整解析。

9.3.4 旧版 API 与 WHATWG API 的对比与选择

| 特性 | 传统 url + querystring | WHATWG URL + URLSearchParams |
|------|------------------------|------------------------------|
| 遵循标准 | 旧版 RFC | WHATWG URL 标准 |
| 浏览器一致性 | 无 | 与浏览器相同 |
| API 风格 | 函数式(parse/format) | 面向对象(new URL) |
| 编码处理 | 部分需要手动 | 自动完整 |
| 修改 URL | 需重新 format | 对象属性直接修改 |
| 嵌套参数支持 | 不支持 | 不支持(但可通过迭代实现) |
| 状态 | 保留但不再鼓励 | 官方推荐 |

明确的选择原则:在新代码中,请使用 URLURLSearchParams。旧模块仅用于维护遗留系统,或者当环境无法支持全局 URL 类时才使用(极少数旧 Node 版本)。

9.3.5 实用技巧与常见误区

1. 正确处理查询字符串中的数组参数

浏览器和服务器框架通常采用 key[]=value1&key[]=value2key=value1&key=value2 来表示数组。querystring.parse 默认将重复键解析为数组,而 URLSearchParams.getAll(key) 可以获取重复键的所有值。前端和后端需要约定好序列化格式,避免数据丢失。

2. 谨慎解析不带协议的主机名

使用 new URL() 解析一个不带协议的地址(如 example.com/path)会抛出错误,因为缺少协议导致 URL 不合法。此时的解决方式是动态补充协议头:

const raw = 'example.com/path';
const url = new URL(raw.startsWith('//') ? `https:${raw}` : `https://${raw}`);

3. 注意 URL 编码对 hash 和路由的影响

前端单页应用的路由通常基于 hash 或 HTML5 History API。URLSearchParams 只处理 ? 后的部分,不会涉及 hash 后的内容。如果有特殊需求,需要手动处理 hash 字符串。

4. 使用 qs 库处理复杂嵌套查询

如果需要解析 name[first]=John&name[last]=Doe 这样的嵌套查询字符串,内置 API 无法做到。社区库 qs 提供了 qs.parse(str, { allowDots: true })qs.stringify(obj),可以处理数组、嵌套对象、点分隔路径等高级特性,但会引入额外依赖,权衡使用即可。


掌握了 URL 与查询字符串的处理,我们就能从容应对 HTTP 请求中的路径解析、参数提取、重定向构造等基础任务。在下一节中,我们将继续深入了解 http 模块的实际应用,包括搭建服务器、处理请求体、实现文件上传等核心网络编程技术。