在 Node.js 开发中,处理文件路径、统一资源定位符(URL)及其查询字符串是日常任务。pathurlquerystring 这三个核心模块提供了强大且跨平台的功能,使得开发者能够高效、安全地解析、格式化和操作这些关键的标识符数据。

核心思想:path 抽象操作系统文件路径差异;url 标准化 URL 的解析与构建;querystring 专注于查询参数的编码与解码。 它们共同构成了 Node.js 应用在文件系统和网络层面的基础数据处理能力。


一、path 模块:处理文件和目录路径

path 模块提供了用于处理文件和目录路径的工具函数。它无需 require() 即可使用(通常是 const path = require('path');),并且抽象了操作系统之间路径表示方式的差异。

1.1 核心概念

  • 跨平台兼容性path 模块设计用于处理 POSIX (Linux/macOS) 和 Windows 操作系统之间的路径差异。它提供了 path.sep(路径片段分隔符)和 path.delimiter(环境变量分隔符)来适应不同平台。
  • POSIX 风格路径:使用 / 作为分隔符。
  • Windows 风格路径:使用 \ 作为分隔符,且有驱动器盘符(如 C:\)。
  • path.posixpath.win32path 模块本身的方法会根据当前操作系统自动选择合适的实现。如果需要强制使用特定平台的路径处理逻辑,可以使用 path.posixpath.win32 属性。

1.2 常用属性和方法

1.2.1 path.seppath.delimiter

  • path.sep: 返回当前操作系统的路径分隔符(例如:'/' for POSIX, '\' for Windows)。
  • path.delimiter: 返回当前操作系统的路径环境变量分隔符(例如:':' for POSIX, ';' for Windows)。
1
2
3
4
const path = require('path');

console.log(`路径分隔符: ${path.sep}`); // macOS/Linux: /, Windows: \
console.log(`环境变量分隔符: ${path.delimiter}`); // macOS/Linux: :, Windows: ;

1.2.2 path.join([...paths])

将所有给定的 path 片段连接起来,并进行规范化。这是构建路径时最常用的方法,因为它会自动处理不同操作系统下的分隔符,并去除冗余的 ...

1
2
3
4
5
6
7
8
9
10
11
12
const path = require('path');

console.log(path.join('/foo', 'bar', 'baz/asdf', 'quux', '..'));
// 输出 (POSIX): /foo/bar/baz/asdf
// 输出 (Windows): \foo\bar\baz\asdf

console.log(path.join('foo', 'bar', '../baz'));
// 输出 (POSIX): foo/baz
// 输出 (Windows): foo\baz

console.log(path.join('foo', './bar', 'baz/'));
// 输出 (POSIX): foo/bar/baz/

1.2.3 path.resolve([...paths])

将一系列路径或路径片段解析为绝对路径。它会从右到左处理路径片段,直到构造出一个绝对路径。如果没有提供路径片段,它将返回当前工作目录的绝对路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require('path');

console.log(path.resolve('/foo/bar', './baz'));
// 输出 (POSIX): /foo/bar/baz
// 输出 (Windows): C:\foo\bar\baz (假设当前驱动器为 C)

console.log(path.resolve('/foo/bar', '/tmp/file/', '..', 'a/../subfile'));
// 输出 (POSIX): /tmp/subfile
// 输出 (Windows): C:\tmp\subfile

console.log(path.resolve('foo/bar', '/tmp/file', '..', 'subfile'));
// 输出 (POSIX): /tmp/subfile

console.log(path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif'));
// 输出: <当前工作目录>/wwwroot/static_files/gif/image.gif

1.2.4 path.basename(path[, ext])

返回路径的最后一部分,即文件名。可选的 ext 参数可以移除文件扩展名。

1
2
3
4
5
6
const path = require('path');

console.log(path.basename('/foo/bar/baz/asdf.html')); // asdf.html
console.log(path.basename('/foo/bar/baz/asdf.html', '.html')); // asdf
console.log(path.basename('/foo/bar/baz/')); // baz
console.log(path.basename('index.js')); // index.js

1.2.5 path.dirname(path)

返回路径的目录名部分。

1
2
3
4
5
const path = require('path');

console.log(path.dirname('/foo/bar/baz/asdf/quux.html')); // /foo/bar/baz/asdf
console.log(path.dirname('/foo/bar/baz')); // /foo/bar
console.log(path.dirname('index.js')); // . (当前目录)

1.2.6 path.extname(path)

返回路径的扩展名。

1
2
3
4
5
6
const path = require('path');

console.log(path.extname('index.html')); // .html
console.log(path.extname('index.tar.gz')); // .gz
console.log(path.extname('index.')); // .
console.log(path.extname('index')); // (空字符串)

1.2.7 path.parse(path)

返回一个对象,包含路径的 rootdirbaseextname 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const path = require('path');

console.log(path.parse('/home/user/dir/file.txt'));
/*
{
root: '/',
dir: '/home/user/dir',
base: 'file.txt',
ext: '.txt',
name: 'file'
}
*/

console.log(path.parse('C:\\path\\dir\\index.js'));
/*
{
root: 'C:\\',
dir: 'C:\\path\\dir',
base: 'index.js',
ext: '.js',
name: 'index'
}
*/

1.2.8 path.format(pathObject)

path.parse() 相反,它从一个对象中返回一个路径字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require('path');

console.log(path.format({
root: '/ignored', // 如果提供了 dir,root 将被忽略
dir: '/home/user/dir',
base: 'file.txt'
})); // /home/user/dir/file.txt

console.log(path.format({
root: '/',
name: 'file',
ext: '.txt'
})); // /file.txt (dir 优先级高于 root + name + ext)

1.2.9 path.isAbsolute(path)

判断 path 是否是绝对路径。

1
2
3
4
5
6
const path = require('path');

console.log(path.isAbsolute('/foo/bar')); // true
console.log(path.isAbsolute('/baz/..')); // true
console.log(path.isAbsolute('qux/')); // false
console.log(path.isAbsolute('C:\\foo')); // true (Windows)

1.3 适用场景

  • 文件系统操作:在读取、写入、创建或删除文件和目录时,构建正确的路径至关重要。
  • 配置管理:解析配置文件路径或应用程序资源路径。
  • 路由:虽然通常由框架处理,但在某些自定义路由逻辑中可能需要操作路径。
  • 跨平台兼容性:确保应用程序在不同操作系统上都能正确处理文件路径。

1.4 最佳实践

  • 始终使用 path.join()path.resolve():避免手动拼接路径字符串,这可以有效防止平台特定的路径分隔符问题和路径规范化错误。
  • 理解 path.join()path.resolve() 的区别
    • join 只是简单地连接和规范化,结果可能仍是相对路径。
    • resolve 总是返回一个绝对路径,如果给定的路径片段不足以构成绝对路径,它会使用当前工作目录。
  • 避免路径注入:在处理用户提供的路径时,要小心路径遍历漏洞。不要直接拼接用户输入到文件系统操作中,而是使用 path.resolve()path.normalize() 来验证和清理路径,并确保路径指向预期目录。

二、url 模块:解析和格式化 URL

url 模块提供了用于 URL 解析和格式化以及查询字符串操作的实用函数。Node.js 推荐使用符合 Web 标准的 URL 全局对象,而不是其遗留的 url.parse() 方法。

2.1 核心概念

  • URL (Uniform Resource Locator):统一资源定位符,用于标识和定位互联网上的资源。
  • URL 组件:一个 URL 通常由协议(protocol)、主机(host)、端口(port)、路径(pathname)、查询参数(search/query)和片段(hash/fragment)组成。
  • URLSearchParams:一个 Web 标准接口,用于处理 URL 的查询字符串。

2.2 推荐使用:URL 全局对象

URL 类是一个全局可用的 Web 标准 API,它提供了对 URL 组件的轻松访问和修改。

2.2.1 创建 URL 对象

new URL(input[, base])

  • input: 要解析的 URL 字符串。
  • base: 可选的基准 URL,用于解析相对 URL。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const urlString = 'https://user:pass@example.com:8080/path/to/page?id=123&name=test#section';
const myUrl = new URL(urlString);

console.log(myUrl);
/*
URL {
href: 'https://user:pass@example.com:8080/path/to/page?id=123&name=test#section',
origin: 'https://example.com:8080',
protocol: 'https:',
username: 'user',
password: 'pass',
host: 'example.com:8080',
hostname: 'example.com',
port: '8080',
pathname: '/path/to/page',
search: '?id=123&name=test',
searchParams: URLSearchParams { 'id' => '123', 'name' => 'test' },
hash: '#section'
}
*/

// 使用 base 解析相对 URL
const relativeUrl = new URL('foo/bar', 'https://example.org/');
console.log(relativeUrl.href); // https://example.org/foo/bar

2.2.2 URL 对象的属性

URL 对象提供了直接访问各个 URL 组件的属性,它们都是可读写的。

  • href: 完整的 URL 字符串。
  • protocol: 协议部分,带末尾的冒号。
  • username: 用户名部分。
  • password: 密码部分。
  • host: 主机名和端口。
  • hostname: 主机名。
  • port: 端口号。
  • pathname: 路径部分。
  • search: 查询字符串部分,带开头的问号。
  • searchParams: 一个 URLSearchParams 对象,用于操作查询参数。
  • hash: 片段标识符部分,带开头的井号。
  • origin: 只读属性,包含协议、主机名和端口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const myUrl = new URL('https://www.example.com/search?q=nodejs&page=1#result');

console.log(`协议: ${myUrl.protocol}`); // https:
console.log(`主机名: ${myUrl.hostname}`); // www.example.com
console.log(`端口: ${myUrl.port}`); // (空字符串,因为是默认端口)
console.log(`路径: ${myUrl.pathname}`); // /search
console.log(`查询字符串: ${myUrl.search}`); // ?q=nodejs&page=1
console.log(`片段: ${myUrl.hash}`); // #result

// 修改 URL
myUrl.pathname = '/new/path';
myUrl.port = '8080';
myUrl.hash = ''; // 移除片段
console.log(`修改后的 URL: ${myUrl.href}`); // https://www.example.com:8080/new/path?q=nodejs&page=1

2.2.3 URLSearchParams 对象

URLSearchParams 是处理查询字符串的强大工具,它提供了 get, set, append, delete, has, forEach 等方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const myUrl = new URL('https://example.com/api?user=alice&id=123&tags=node');
const params = myUrl.searchParams;

console.log(`用户: ${params.get('user')}`); // alice
console.log(`是否有 'id' 参数: ${params.has('id')}`); // true
console.log(`是否有 'status' 参数: ${params.has('status')}`); // false

params.set('user', 'bob'); // 修改
params.append('tags', 'javascript'); // 添加新的值 (如果键已存在,则追加)
params.delete('id'); // 删除
params.set('status', 'active'); // 添加新的键值对

console.log(`修改后的查询字符串: ${params.toString()}`); // user=bob&tags=node&tags=javascript&status=active
console.log(`完整的 URL: ${myUrl.href}`); // https://example.com/api?user=bob&tags=node&tags=javascript&status=active

// 遍历查询参数
params.forEach((value, name) => {
console.log(`${name}: ${value}`);
});

2.3 遗留方法 (不推荐用于新代码)

  • url.parse(urlStr[, parseQueryString[, slashesDenoteHost]]): 解析 URL 字符串并返回一个 URL 对象。
  • url.format(urlObject): 格式化一个 URL 对象为 URL 字符串。
  • url.resolve(from, to): 解析相对 URL。

这些方法在 Node.js 的早期版本中广泛使用,但现在已被 URL 全局对象取代,后者提供了更一致、更符合 Web 标准的 API。

2.4 url.fileURLToPath(url)url.pathToFileURL(path)

这两个辅助函数用于在文件路径和 file:// URLs 之间进行转换。

1
2
3
4
5
6
7
8
9
const url = require('url');
const path = require('path');

const filePath = '/tmp/my-file.txt';
const fileUrl = url.pathToFileURL(filePath);
console.log(`文件路径转 URL: ${fileUrl.href}`); // file:///tmp/my-file.txt

const retrievedPath = url.fileURLToPath(fileUrl);
console.log(`URL 转文件路径: ${retrievedPath}`); // /tmp/my-file.txt (POSIX)

2.5 URL 结构示意图

2.6 适用场景

  • HTTP 服务器与客户端:解析传入的请求 URL,构建传出的请求 URL。
  • Web 抓取/爬虫:解析页面中的链接。
  • API 交互:构建带参数的 API 请求 URL。
  • 路由:基于 URL 路径和查询参数进行路由匹配。

2.7 最佳实践与安全性考虑

  • 优先使用 URL 全局对象:它提供了更现代、更符合 Web 标准的 API,并且性能通常优于遗留的 url 模块方法。
  • 正确处理编码URL 对象会自动处理 URL 编码和解码。避免手动进行 encodeURIComponentdecodeURIComponent,除非你知道你在做什么。
  • 验证和清理用户输入:在应用程序中处理用户提供的 URL 时,务必进行严格的验证和清理,以防止开放重定向、XSS 或其他 URL 相关攻击。
  • 敏感信息:避免在 URL 的 usernamepassword 部分传递敏感信息,尤其是在通过 HTTP 而非 HTTPS 连接时。

三、querystring 模块:查询字符串的解析与序列化

querystring 模块提供了用于解析和格式化 URL 查询字符串的工具。尽管 URL 全局对象中的 URLSearchParams 提供了更现代的 API,但 querystring 模块在某些场景(如处理不符合 URLSearchParams 规范的旧有查询字符串格式,或特定库依赖)下仍有其用武之地。

3.1 核心概念

  • 查询字符串:URL 中 ? 后面,# 前面的部分,由一系列 键=值 对组成,并用 & 符号分隔。
  • URL 编码 (Percent-Encoding):为了确保查询字符串中的特殊字符(如空格、&=)不会破坏 URL 结构或引起歧义,它们会被转换成 %HH 的形式(例如,空格变为 %20)。

3.2 常用方法

3.2.1 querystring.parse(str[, sep[, eq[, options]]])

将 URL 查询字符串解析成一个键值对对象。

  • str: 要解析的查询字符串。
  • sep: 可选参数,用于指定键值对之间的分隔符,默认为 '&'
  • eq: 可选参数,用于指定键和值之间的分隔符,默认为 '='
  • options: 可选对象,可以包含 maxKeys (最大解析的键的数量,默认为 1000) 和 decodeURIComponent (自定义解码函数)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const querystring = require('querystring');

const query = 'name=Alice&age=30&city=New York&job=Software Engineer';
const parsed = querystring.parse(query);
console.log(parsed);
// { name: 'Alice', age: '30', city: 'New York', job: 'Software Engineer' }

const complexQuery = 'foo=bar&baz=qux&baz=quux&corge'; // 多个同名键,无值的键
const parsedComplex = querystring.parse(complexQuery);
console.log(parsedComplex);
// { foo: 'bar', baz: ['qux', 'quux'], corge: '' }

// 自定义分隔符
const customSepQuery = 'name:Alice;age:30';
const parsedCustom = querystring.parse(customSepQuery, ';', ':');
console.log(parsedCustom);
// { name: 'Alice', age: '30' }

3.2.2 querystring.stringify(obj[, sep[, eq[, options]]])

将一个对象序列化成 URL 查询字符串。

  • obj: 要序列化成查询字符串的对象。
  • sep: 可选参数,用于指定键值对之间的分隔符,默认为 '&'
  • eq: 可选参数,用于指定键和值之间的分隔符,默认为 '='
  • options: 可选对象,可以包含 encodeURIComponent (自定义编码函数)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const querystring = require('querystring');

const data = {
name: 'John Doe',
age: 25,
city: 'San Francisco',
hobbies: ['coding', 'reading']
};

const stringified = querystring.stringify(data);
console.log(stringified);
// name=John%20Doe&age=25&city=San%20Francisco&hobbies=coding&hobbies=reading

const customStringified = querystring.stringify(data, ';', ':');
console.log(customStringified);
// name:John%20Doe;age:25;city:San%20Francisco;hobbies:coding;hobbies:reading

3.2.3 querystring.escape(str)querystring.unscape(str)

用于对字符串进行 URL 编码和解码。

  • querystring.escape(str):与 encodeURIComponent 类似,但针对 querystring 模块的 stringify 方法进行了优化。
  • querystring.unscape(str):与 decodeURIComponent 类似,但针对 querystring 模块的 parse 方法进行了优化。
1
2
3
4
5
6
7
8
9
10
const querystring = require('querystring');

const unsafeString = 'hello world & other characters = !';
const escaped = querystring.escape(unsafeString);
console.log(`转义后: ${escaped}`);
// hello%20world%20%26%20other%20characters%20%3D%20!

const unescaped = querystring.unscape(escaped);
console.log(`反转义后: ${unescaped}`);
// hello world & other characters = !

3.3 适用场景

  • 处理旧版或非标准查询字符串:当遇到需要自定义分隔符或解析规则的查询字符串时,querystring 模块更灵活。
  • 特定第三方库的兼容性:某些库可能内部仍在使用 querystring 模块进行处理。
  • URLSearchParams 配合:在某些情况下,可能需要先用 querystring 处理一部分字符串,再将其结果集成到 URL 对象中。

3.4 最佳实践与安全性考虑

  • 优先使用 URLSearchParams:对于标准的 URL 查询字符串操作,URL 全局对象上的 searchParams 属性(即 URLSearchParams 对象)是推荐的首选。它是一个 Web 标准 API,提供更全面的功能和更好的可读性。
  • 注意字符编码querystring 模块默认使用 UTF-8 进行编码和解码。如果处理的查询字符串使用其他编码,可能会出现乱码。
  • 防止查询参数注入:在 querystring.stringify() 时,确保传递给它的对象不包含恶意构造的键或值,避免生成可能被解释为不同参数的查询字符串。

四、总结

pathurlquerystring 模块是 Node.js 应用程序中处理文件路径和网络地址不可或缺的工具。

  • path 模块:通过提供跨平台的文件路径操作方法,使得 Node.js 应用程序能够优雅地适应各种操作系统环境,确保路径构建和解析的正确性。
  • url 模块:主要通过其全局的 URL 对象和 URLSearchParams API,提供了强大且符合 Web 标准的 URL 解析、构建和查询字符串操作能力,是进行网络通信和构建 Web 应用程序的基础。
  • querystring 模块:虽然在许多场景下已被 URLSearchParams 取代,但它依然是处理自定义或遗留查询字符串格式的有效工具。

理解并正确使用这些模块,特别是遵循现代 Web 标准的最佳实践,对于构建健壮、安全和高效的 Node.js 应用程序至关重要。