在Node.js上利用文件系统路径工作

1,056 阅读7分钟

在这篇博文中,我们学习如何在Node.js上使用文件系统路径。


目录。


在这篇博文中,我们探讨了Node.js上的路径相关功能。

  • 大多数与路径相关的功能都在模块'node:path'
  • 全局变量process 有改变当前工作目录的方法(那是什么,很快就会解释)。
  • 模块'node:os' 有返回重要目录路径的函数。

访问'node:path' API的三种方式

模块'node:path' 经常被导入,如下所示。

import * as path from 'node:path';

在这篇博文中,这个导入语句偶尔会被省略掉。我们也省略了下面的导入。

import * as assert from 'node:assert/strict';

我们可以通过三种方式访问Node的路径API。

  • 我们可以访问特定平台版本的API。

    • path.posix 支持 Unixes,包括 macOS。
    • path.win32 支持Windows。
  • path 本身总是支持当前的平台。例如,这是一个在macOS上的REPL交互。

    > path.parse === path.posix.parse
    true
    

让我们看看解析文件系统路径的函数path.parse() ,在这两个平台上有什么不同。

> path.win32.parse(String.raw`C:\Users\jane\file.txt`)
{
  dir: 'C:\\Users\\jane',
  root: 'C:\\',
  base: 'file.txt',
  name: 'file',
  ext: '.txt',
}
> path.posix.parse(String.raw`C:\Users\jane\file.txt`)
{
  dir: '',
  root: '',
  base: 'C:\\Users\\jane\\file.txt',
  name: 'C:\\Users\\jane\\file',
  ext: '.txt',
}

我们解析一个Windows路径--首先通过path.win32 API正确解析,然后通过path.posix API。我们可以看到,在后一种情况下,路径没有被正确地分割成各个部分--例如,文件的基名应该是file.txt (关于其他属性的含义,稍后再谈)。

基本的路径概念和它们的API支持

路径段、路径分隔符、路径定界符

术语。

  • 一个非空的路径由一个或多个路径段组成,通常是目录或文件的名称。

  • 路径分隔器用于分隔路径中两个相邻的路径段。

    > path.posix.sep
    '/'
    > path.win32.sep
    '\\'
    
  • 路径分隔符分隔路径列表中的元素。

    > path.posix.delimiter
    ':'
    > path.win32.delimiter
    ';'
    

如果我们检查一下PATH这个外壳变量,就可以看到路径分隔符和路径定界符--它包含操作系统在外壳中输入命令时寻找可执行文件的路径。

这是一个macOS PATH的例子(shell变量$PATH )。

> process.env.PATH.split(/(?<=:)/)
[
  '/opt/homebrew/bin:',
  '/opt/homebrew/sbin:',
  '/usr/local/bin:',
  '/usr/bin:',
  '/bin:',
  '/usr/sbin:',
  '/sbin',
]

分隔符的长度为0,因为lookbehind断言 (?<=:) ,如果给定的位置前面有一个冒号,它就会匹配,但它并没有捕获任何东西。因此,路径分隔符':' ,包括在前面的路径中。

这是一个Windows PATH的例子(shell变量%Path% )。

> process.env.Path.split(/(?<=;)/)
[
  'C:\\Windows\\system32;',
  'C:\\Windows;',
  'C:\\Windows\\System32\\Wbem;',
  'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;',
  'C:\\Windows\\System32\\OpenSSH\\;',
  'C:\\ProgramData\\chocolatey\\bin;',
  'C:\\Program Files\\nodejs\\',
]

当前工作目录

许多shell有当前工作目录(CWD)的概念--"我当前所在的目录"。

  • 如果我们使用一个带有部分限定路径的命令,该路径就会针对CWD进行解析。
  • 如果我们省略了一个路径,而一个命令希望有一个路径,那么就会使用CWD。
  • 在Unixs和Windows上,改变CWD的命令是cd

process 是一个全局Node.js变量。它为我们提供了获取和设置CWD的方法。

  • process.cwd()返回CWD。
  • process.chdir(dirPath)将CWD改为dirPath
    • 必须有一个目录在dirPath
    • 这种改变并不影响shell,只影响当前运行的Node.js进程。

Node.js使用CWD来填补缺失的部分,只要路径不完全合格(完整)。这使我们能够在各种功能中使用部分限定的路径,例如:fs.readFileSync()

Unix上的当前工作目录

下面的代码演示了Unix上的process.chdir()process.cwd()

process.chdir('/home/jane');
assert.equal(
  process.cwd(), '/home/jane'
);

Windows上的当前工作目录

到目前为止,我们已经使用了Unix上的当前工作目录。Windows的工作方式不同。

  • 每个驱动器都有一个当前目录
  • 有一个当前驱动器

我们可以使用path.chdir() 来同时设置这两个。

process.chdir('C:\\Windows');
process.chdir('Z:\\tmp');

当我们重新访问一个驱动器时,Node.js会记住该驱动器之前的当前目录。

assert.equal(
  process.cwd(), 'Z:\\tmp'
);
process.chdir('C:');
assert.equal(
  process.cwd(), 'C:\\Windows'
);

完全限定路径与部分限定路径,解析路径

  • 一个完全合格的路径不依赖于任何其他信息,可以按原样使用。
  • 部分限定的路径缺少信息。我们需要在使用它之前把它变成一个完全合格的路径。这可以通过将其与完全合格的路径进行解析来实现。

Unix上的完全和部分合格的路径

Unix只知道两种类型的路径。

  • 绝对路径是完全合格的,以斜线开头。

    /home/john/proj
    
  • 相对路径是部分限定的,以文件名或点开始。

    .   (current directory)
    ..  (parent directory)
    dir
    ./dir
    ../dir
    ../../dir/subdir
    

让我们用path.resolve()后面会详细解释)来解决相对路径和绝对路径的问题。其结果是绝对路径。

> const abs = '/home/john/proj';

> path.resolve(abs, '.')
'/home/john/proj'
> path.resolve(abs, '..')
'/home/john'
> path.resolve(abs, 'dir')
'/home/john/proj/dir'
> path.resolve(abs, './dir')
'/home/john/proj/dir'
> path.resolve(abs, '../dir')
'/home/john/dir'
> path.resolve(abs, '../../dir/subdir')
'/home/dir/subdir'

Windows上的完全和部分限定路径

Windows区分了四种路径(更多信息请参见微软的文档)。

  • 有绝对路径和相对路径。
  • 这两种路径都可以有一个盘符("卷标")或没有。

带有盘符的绝对路径是完全合格的。所有其他路径都是部分限定的。

对照完全限定的路径full解析一个没有盘符的绝对路径,会得到full 的盘符。

> const full = 'C:\\Users\\jane\\proj';

> path.resolve(full, '\\Windows')
'C:\\Windows'

对照一个完全合格的路径,解析一个没有盘符的相对路径,可以看成是对后者的更新。

> const full = 'C:\\Users\\jane\\proj';

> path.resolve(full, '.')
'C:\\Users\\jane\\proj'
> path.resolve(full, '..')
'C:\\Users\\jane'
> path.resolve(full, 'dir')
'C:\\Users\\jane\\proj\\dir'
> path.resolve(full, '.\\dir')
'C:\\Users\\jane\\proj\\dir'
> path.resolve(full, '..\\dir')
'C:\\Users\\jane\\dir'
> path.resolve(full, '..\\..\\dir')
'C:\\Users\\dir'

针对完全合格的路径full ,解决一个带盘符的相对路径rel ,取决于rel 的盘符。

  • full 相同的盘符?将relfull 对照解决。
  • full 不同的盘符?将relrel's drive的当前目录进行比对。

这看起来如下。

// Configure current directories for C: and Z:
process.chdir('C:\\Windows\\System');
process.chdir('Z:\\tmp');

const full = 'C:\\Users\\jane\\proj';

// Same drive letter
assert.equal(
  path.resolve(full, 'C:dir'),
  'C:\\Users\\jane\\proj\\dir'
);
assert.equal(
  path.resolve(full, 'C:'),
  'C:\\Users\\jane\\proj'
);

// Different drive letter
assert.equal(
  path.resolve(full, 'Z:dir'),
  'Z:\\tmp\\dir'
);
assert.equal(
  path.resolve(full, 'Z:'),
  'Z:\\tmp'
);

通过模块'node:os' 获取重要目录的路径

模块'node:os' 为我们提供了两个重要目录的路径。

  • os.homedir()返回当前用户的主目录的路径 - 例如。

    > os.homedir() // macOS
    '/Users/rauschma'
    > os.homedir() // Windows
    'C:\\Users\\axel'
    
  • os.tmpdir()返回操作系统的临时文件目录的路径--例如。

    > os.tmpdir() // macOS
    '/var/folders/ph/sz0384m11vxf5byk12fzjms40000gn/T'
    > os.tmpdir() // Windows
    'C:\\Users\\axel\\AppData\\Local\\Temp'
    

连接路径

有两个函数用于连接路径。

  • path.resolve() 总是返回完全合格的路径
  • path.join() 保留相对路径

path.resolve():串联路径以创建完全合格的路径

path.resolve(...paths: Array<string>): string

串联paths ,并返回一个完全合格的路径。它使用以下算法。

  • 从当前工作目录开始。
  • path[0] 与之前的结果进行比对。
  • path[1] 与之前的结果进行比对。
  • 对所有剩余的路径做同样的处理。
  • 返回最后的结果。

没有参数,path.resolve() ,返回当前工作目录的路径。

> process.cwd()
'/usr/local'
> path.resolve()
'/usr/local'

一个或多个相对路径被用于解析,从当前工作目录开始。

> path.resolve('.')
'/usr/local'
> path.resolve('..')
'/usr'
> path.resolve('bin')
'/usr/local/bin'
> path.resolve('./bin', 'sub')
'/usr/local/bin/sub'
> path.resolve('../lib', 'log')
'/usr/lib/log'

任何完全合格的路径都会取代之前的结果。

> path.resolve('bin', '/home')
'/home'

这使我们能够针对完全合格的路径解析部分合格的路径。

> path.resolve('/home/john', 'proj', 'src')
'/home/john/proj/src'

path.join():在保留相对路径的同时串联路径

path.join(...paths: Array<string>): string

paths[0] 开始,并将其余路径解释为升序或降序的指令。与path.resolve() 相比,这个函数保留了部分限定的路径。如果paths[0] 是部分限定的,则结果是部分限定的。如果它是完全限定的,则结果是完全限定的。

降序的例子。

> path.posix.join('/usr/local', 'sub', 'subsub')
'/usr/local/sub/subsub'
> path.posix.join('relative/dir', 'sub', 'subsub')
'relative/dir/sub/subsub'

双点上升。

> path.posix.join('/usr/local', '..')
'/usr'
> path.posix.join('relative/dir', '..')
'relative'

单点不做任何事情。

> path.posix.join('/usr/local', '.')
'/usr/local'
> path.posix.join('relative/dir', '.')
'relative/dir'

如果第一个参数之后的参数是完全限定的路径,它们会被解释为相对路径。

> path.posix.join('dir', '/tmp')
'dir/tmp'
> path.win32.join('dir', 'C:\\Users')
'dir\\C:\\Users'

使用两个以上的参数。

> path.posix.join('/usr/local', '../lib', '.', 'log')
'/usr/lib/log'

确保路径是规范化的,完全合格的,或相对的

path.normalize():确保路径是正常化的

path.normalize(path: string): string

在Unix中,path.normalize()

  • 移除单点的路径段 (.)。
  • 解决双点的路径段 (..)。
  • 将多个路径分隔符变成一个路径分隔符。

比如说。

// Fully qualified path
assert.equal(
  path.posix.normalize('/home/./john/lib/../photos///pet'),
  '/home/john/photos/pet'
);

// Partially qualified path
assert.equal(
  path.posix.normalize('./john/lib/../photos///pet'),
  'john/photos/pet'
);

在Windows上,path.normalize()

  • 移除单点的路径段 (.)。
  • 解决双点的路径段 (..)。
  • 将每个路径分隔符斜线(/)--合法的--转换为首选的路径分隔符(\)。
  • 将多个路径分隔符的序列转换为单反斜线。

比如说。

// Fully qualified path
assert.equal(
  path.win32.normalize('C:\\Users/jane\\doc\\..\\proj\\\\src'),
  'C:\\Users\\jane\\proj\\src'
);

// Partially qualified path
assert.equal(
  path.win32.normalize('.\\jane\\doc\\..\\proj\\\\src'),
  'jane\\proj\\src'
);

注意,path.join() ,只有一个参数,也是规范化的,与path.normalize() 工作相同。

> path.posix.normalize('/home/./john/lib/../photos///pet')
'/home/john/photos/pet'
> path.posix.join('/home/./john/lib/../photos///pet')
'/home/john/photos/pet'

> path.posix.normalize('./john/lib/../photos///pet')
'john/photos/pet'
> path.posix.join('./john/lib/../photos///pet')
'john/photos/pet'

path.resolve() (一个参数):确保路径被规范化并完全合格

我们已经遇到了 path.resolve().用一个参数来调用,它既能使路径正常化,又能确保它们是完全合格的。

在Unix上使用path.resolve()

> process.cwd()
'/usr/local'

> path.resolve('/home/./john/lib/../photos///pet')
'/home/john/photos/pet'
> path.resolve('./john/lib/../photos///pet')
'/usr/local/john/photos/pet'

在Windows上使用path.resolve()

> process.cwd()
'C:\\Windows\\System'

> path.resolve('C:\\Users/jane\\doc\\..\\proj\\\\src')
'C:\\Users\\jane\\proj\\src'
> path.resolve('.\\jane\\doc\\..\\proj\\\\src')
'C:\\Windows\\System\\jane\\proj\\src'

path.relative():创建相对路径

path.relative(sourcePath: string, destinationPath: string): string

返回一个相对路径,使我们从sourcePathdestinationPath

> path.posix.relative('/home/john/', '/home/john/proj/my-lib/README.md')
'proj/my-lib/README.md'
> path.posix.relative('/tmp/proj/my-lib/', '/tmp/doc/zsh.txt')
'../../doc/zsh.txt'

在Windows上,如果sourcePathdestinationPath 是在不同的驱动器上,我们会得到一个完全合格的路径。

> path.win32.relative('Z:\\tmp\\', 'C:\\Users\\Jane\\')
'C:\\Users\\Jane'

这个函数也适用于相对路径。

> path.posix.relative('proj/my-lib/', 'doc/zsh.txt')
'../../doc/zsh.txt'

path.parse(): 创建一个带有路径部分的对象

type PathObject = {
  dir: string,
    root: string,
  base: string,
    name: string,
    ext: string,
};
path.parse(path: string): PathObject

提取path 的各个部分,并在一个对象中返回,其属性如下。

  • .base: 路径的最后一段
    • .ext: 基地的文件名扩展名
    • .name: 不含扩展名的基数。这一部分也被称为路径的
  • .root: 路径的开头(在第一段之前)。
  • .dir: 基地所在的目录 - 不含基地的路径

稍后,我们将看到函数path.format(),它是path.parse() 的逆运算。它将一个有路径部分的对象转换成一个路径。

例如:Unix上的path.parse()

这就是在Unix上使用path.parse() 的样子。

> path.posix.parse('/home/jane/file.txt')
{
  dir: '/home/jane',
  root: '/',
  base: 'file.txt',
  name: 'file',
  ext: '.txt',
}

下图直观地显示了各部分的范围。

  /      home/jane / file   .txt
| root |           | name | ext  |
| dir              | base        |

例如,我们可以看到,.dir 是没有底座的路径。而.base.name 加上.ext

例子:Windows上的path.parse()

这就是path.parse() 在Windows上的工作原理。

> path.win32.parse(String.raw`C:\Users\john\file.txt`)
{
  dir: 'C:\\Users\\john',
  root: 'C:\\',
  base: 'file.txt',
  name: 'file',
  ext: '.txt',
}

这是一个结果的图表。

  C:\    Users\john \ file   .txt
| root |            | name | ext  |
| dir               | base        |
path.basename(path, ext?)

返回path 的基数。

> path.basename('/home/jane/file.txt')
'file.txt'

可选的是,这个函数也可以删除后缀。

> path.basename('/home/jane/file.txt', '.txt')
'file'
> path.basename('/home/jane/file.txt', 'txt')
'file.'
> path.basename('/home/jane/file.txt', 'xt')
'file.t'

删除后缀是区分大小写的--即使在Windows上也是如此!

> path.win32.basename(String.raw`C:\Users\john\file.txt`, '.txt')
'file'
> path.win32.basename(String.raw`C:\Users\john\file.txt`, '.TXT')
'file.txt'
path.dirname(path)

返回path 的文件或目录的父目录。

> path.win32.dirname(String.raw`C:\Users\john\file.txt`)
'C:\\Users\\john'
> path.win32.dirname('C:\\Users\\john\\dir\\')
'C:\\Users\\john'

> path.posix.dirname('/home/jane/file.txt')
'/home/jane'
> path.posix.dirname('/home/jane/dir/')
'/home/jane'
path.extname(path)

返回path 的扩展名。

> path.extname('/home/jane/file.txt')
'.txt'
> path.extname('/home/jane/file.')
'.'
> path.extname('/home/jane/file')
''
> path.extname('/home/jane/')
''
> path.extname('/home/jane')
''

对路径进行分类

path.isAbsolute():一个给定的路径是绝对的吗?

path.isAbsolute(path: string): boolean

如果path 是绝对的,返回true ,否则返回false

Unix上的结果是直接的。

> path.posix.isAbsolute('/home/john')
true
> path.posix.isAbsolute('john')
false

在Windows上,"绝对 "不一定意味着 "完全合格"(只有第一个路径是完全合格的)。

> path.win32.isAbsolute('C:\\Users\\jane')
true
> path.win32.isAbsolute('\\Users\\jane')
true
> path.win32.isAbsolute('C:jane')
false
> path.win32.isAbsolute('jane')
false

path.format(): 从部件中创建路径

type PathObject = {
  dir: string,
    root: string,
  base: string,
    name: string,
    ext: string,
};
path.format(pathObject: PathObject): string

从一个路径对象中创建一个路径。

> path.format({dir: '/home/jane', base: 'file.txt'})
'/home/jane/file.txt'

例子:改变文件名的扩展名

我们可以用path.format() 来改变一个路径的扩展名。

function changeFilenameExtension(pathStr, newExtension) {
  if (!newExtension.startsWith('.')) {
    throw new Error(
      'Extension must start with a dot: '
      + JSON.stringify(newExtension)
    );
  }
  const parts = path.parse(pathStr);
  return path.format({
    ...parts,
    base: undefined, // prevent .base from overriding .name and .ext
    ext: newExtension,
  });
}

assert.equal(
  changeFilenameExtension('/tmp/file.md', '.html'),
  '/tmp/file.html'
);
assert.equal(
  changeFilenameExtension('/tmp/file', '.html'),
  '/tmp/file.html'
);
assert.equal(
  changeFilenameExtension('/tmp/file/', '.html'),
  '/tmp/file.html'
);

如果我们知道原始文件名的扩展名,我们也可以使用正则表达式来改变文件名的扩展名。

> '/tmp/file.md'.replace(/\.md$/i, '.html')
'/tmp/file.html'
> '/tmp/file.MD'.replace(/\.md$/i, '.html')
'/tmp/file.html'

在不同的平台上使用相同的路径

有时我们想在不同的平台上使用相同的路径。那么我们就会面临两个问题。

  • 路径分隔符可能不同。
  • 文件结构可能不同:主目录和临时文件的目录可能在不同的位置,等等。

作为一个例子,考虑一个Node.js应用程序,它在一个有数据的目录上操作。让我们假设该应用可以配置两种路径。

  • 系统中任何地方的全限定路径
  • 数据目录内的路径

由于前面提到的问题。

  • 我们不能在平台之间重复使用完全限定的路径。

    • 有时我们需要绝对路径。这些必须在数据目录的每个 "实例 "中进行配置,并存储在外部(或存储在里面,被版本控制忽略)。这些路径保持不变,不随数据目录移动。
  • 我们可以重新使用指向数据目录的路径。这些路径可以存储在配置文件(无论是否在数据目录内)和应用程序代码中的常量中。要做到这一点。

    • 我们必须将它们存储为相对路径。
    • 我们必须确保每个平台上的路径分隔符是正确的。

    下一小节将解释如何实现这两个目标。

独立于平台的相对路径

与平台无关的相对路径可以存储为路径段的数组,并按以下方式转化为完全合格的平台特定路径。

const universalRelativePath = ['static', 'img', 'logo.jpg'];

const dataDirUnix = '/home/john/data-dir';
assert.equal(
  path.posix.resolve(dataDirUnix, ...universalRelativePath),
  '/home/john/data-dir/static/img/logo.jpg'
);

const dataDirWindows = 'C:\\Users\\jane\\data-dir';
assert.equal(
  path.win32.resolve(dataDirWindows, ...universalRelativePath),
  'C:\\Users\\jane\\data-dir\\static\\img\\logo.jpg'
);

为了创建相对平台特定的路径,我们可以使用。

const dataDir = '/home/john/data-dir';
const pathInDataDir = '/home/john/data-dir/static/img/logo.jpg';
assert.equal(
  path.relative(dataDir, pathInDataDir),
  'static/img/logo.jpg'
);

下面的函数将相对平台特定的路径转换为平台无关的路径。

import * as path from 'node:path';

function splitRelativePathIntoSegments(relPath) {
  if (path.isAbsolute(relPath)) {
    throw new Error('Path isn’t relative: ' + relPath);
  }
  relPath = path.normalize(relPath);
  const result = [];
  while (true) {
    const base = path.basename(relPath);
    if (base.length === 0) break;
    result.unshift(base);
    const dir = path.dirname(relPath);
    if (dir === '.') break;
    relPath = dir;
  }
  return result;
}

在Unix上使用splitRelativePathIntoSegments()

> splitRelativePathIntoSegments('static/img/logo.jpg')
[ 'static', 'img', 'logo.jpg' ]
> splitRelativePathIntoSegments('file.txt')
[ 'file.txt' ]

在Windows上使用splitRelativePathIntoSegments()

> splitRelativePathIntoSegments('static/img/logo.jpg')
[ 'static', 'img', 'logo.jpg' ]
> splitRelativePathIntoSegments('C:static/img/logo.jpg')
[ 'static', 'img', 'logo.jpg' ]

> splitRelativePathIntoSegments('file.txt')
[ 'file.txt' ]
> splitRelativePathIntoSegments('C:file.txt')
[ 'file.txt' ]

使用一个库来通过globs匹配路径

npm模块'minimatch'可以让我们根据被称为glob表达式glob模式globs的模式来匹配路径。

import minimatch from 'minimatch';
assert.equal(
  minimatch('/dir/sub/file.txt', '/dir/sub/*.txt'), true
);
assert.equal(
  minimatch('/dir/sub/file.txt', '/**/file.txt'), true
);

globs的使用情况。

  • 指定一个目录中的哪些文件应该被脚本处理。
  • 指定哪些文件应该被忽略。

更多的glob库。

  • multimatch扩展了minimatch,支持多种模式。
  • micromatch是 minimatch 和 multimatch 的替代品,它有类似的 API。
  • globby是一个基于fast-glob的库,它增加了便利的功能。

minimatch 的 API

项目的 readme 文件中记录了 minimatch 的整个 API。在本小节中,我们看一下最重要的功能。

Minimatch 将 globs 编译成 JavaScriptRegExp 对象并使用这些对象进行匹配。

minimatch():编译和匹配一次

minimatch(path: string, glob: string, options?: MinimatchOptions): boolean

如果glob 匹配path ,则返回true ,否则返回false

有两个有趣的选项。

  • .dot: boolean (默认: )false
    如果 ,通配符,如 和 匹配 "不可见 "的路径段(其名称以点开始)。true * **

    > minimatch('/usr/local/.tmp/data.json', '/usr/**/data.json')
    false
    > minimatch('/usr/local/.tmp/data.json', '/usr/**/data.json', {dot: true})
    true
    
    > minimatch('/tmp/.log/events.txt', '/tmp/*/events.txt')
    false
    > minimatch('/tmp/.log/events.txt', '/tmp/*/events.txt', {dot: true})
    true
    
  • .matchBase: boolean (默认: )false
    如果 ,不含斜线的模式将与路径的基名匹配。true

    > minimatch('/dir/file.txt', 'file.txt')
    false
    > minimatch('/dir/file.txt', 'file.txt', {matchBase: true})
    true
    

new minimatch.Minimatch(): 编译一次,匹配多次

minimatch.Minimatch ,使我们能够只编译一次glob到正则表达式,并进行多次匹配。

new Minimatch(pattern: string, options?: MinimatchOptions)

这就是这个类的使用方法。

import minimatch from 'minimatch';
const {Minimatch} = minimatch;
const glob = new Minimatch('/dir/sub/*.txt');
assert.equal(
  glob.match('/dir/sub/file.txt'), true
);
assert.equal(
  glob.match('/dir/sub/notes.txt'), true
);

glob表达式的语法

这个小节涵盖了语法的基本内容。但是还有更多的功能。这里记录了这些内容。

匹配Windows路径

即使在Windows上,glob段也是由斜线分隔的--但是它们同时匹配反斜线和斜线(这在Windows上是合法的路径分隔符)。

> minimatch('dir\\sub/file.txt', 'dir/sub/file.txt')
true

Minimatch不对路径进行规范化处理

Minimatch并没有为我们规范化路径。

> minimatch('./file.txt', './file.txt')
true
> minimatch('./file.txt', 'file.txt')
false
> minimatch('file.txt', './file.txt')
false

因此,如果我们不自己创建路径,我们就必须对其进行规范化。

> path.normalize('./file.txt')
'file.txt'

没有通配符的模式:路径分隔符必须排成一行

没有通配符的模式(可以更灵活地匹配)必须完全匹配。特别是路径分隔符必须排成一行。

> minimatch('/dir/file.txt', '/dir/file.txt')
true
> minimatch('dir/file.txt', 'dir/file.txt')
true
> minimatch('/dir/file.txt', 'dir/file.txt')
false

> minimatch('/dir/file.txt', 'file.txt')
false

也就是说,我们必须决定是绝对路径还是相对路径。

使用选项.matchBase ,我们可以用不带斜线的模式与路径的基名相匹配。

> minimatch('/dir/file.txt', 'file.txt', {matchBase: true})
true

星号 (*) 匹配任何(部分)单段。

通配符星号 (*) 匹配任何路径段或一个段的任何部分。

> minimatch('/dir/file.txt', '/*/file.txt')
true
> minimatch('/tmp/file.txt', '/*/file.txt')
true

> minimatch('/dir/file.txt', '/dir/*.txt')
true
> minimatch('/dir/data.txt', '/dir/*.txt')
true

星号不匹配名称以点开头的 "不可见文件"。如果我们想匹配这些文件,我们必须在星号前加上一个点。

> minimatch('file.txt', '*')
true
> minimatch('.gitignore', '*')
false
> minimatch('.gitignore', '.*')
true
> minimatch('/tmp/.log/events.txt', '/tmp/*/events.txt')
false

选项.dot 可以让我们关闭这种行为。

> minimatch('.gitignore', '*', {dot: true})
true
> minimatch('/tmp/.log/events.txt', '/tmp/*/events.txt', {dot: true})
true

双星号 (**) 匹配零个或多个片段

**/ 匹配零个或多个段。

> minimatch('/file.txt', '/**/file.txt')
true
> minimatch('/dir/file.txt', '/**/file.txt')
true
> minimatch('/dir/sub/file.txt', '/**/file.txt')
true

如果我们想匹配相对路径,该模式仍然不能以路径分隔符开始。

> minimatch('file.txt', '/**/file.txt')
false

双星号不匹配名称以点开头的 "不可见 "路径段。

> minimatch('/usr/local/.tmp/data.json', '/usr/**/data.json')
false

我们可以通过选项.dot 关闭这种行为。

> minimatch('/usr/local/.tmp/data.json', '/usr/**/data.json', {dot: true})
true

否定globbs

如果我们用感叹号开始一个glob,那么如果感叹号后面的模式不匹配,它就会匹配。

> minimatch('file.txt', '!**/*.txt')
false
> minimatch('file.js', '!**/*.txt')
true

替代模式

在大括号内的逗号分隔的模式,如果其中一个模式匹配,就会匹配。

> minimatch('file.txt', 'file.{txt,js}')
true
> minimatch('file.js', 'file.{txt,js}')
true

整数的范围

一对用双点分隔的整数定义了一个整数范围,如果其中的任何元素匹配,则匹配。

> minimatch('file1.txt', 'file{1..3}.txt')
true
> minimatch('file2.txt', 'file{1..3}.txt')
true
> minimatch('file3.txt', 'file{1..3}.txt')
true
> minimatch('file4.txt', 'file{1..3}.txt')
false

也支持用零填充。

> minimatch('file1.txt', 'file{01..12}.txt')
false
> minimatch('file01.txt', 'file{01..12}.txt')
true
> minimatch('file02.txt', 'file{01..12}.txt')
true
> minimatch('file12.txt', 'file{01..15}.txt')
true

使用file: URLs来引用文件

在Node.js中,有两种常见的方法来引用文件。

  • 字符串中的路径
  • 带有协议的URL 的实例file:

比如说

assert.equal(
  fs.readFileSync(
    '/tmp/data.txt', {encoding: 'utf-8'}),
  'Content'
);
assert.equal(
  fs.readFileSync(
    new URL('file:///tmp/data.txt'), {encoding: 'utf-8'}),
  'Content'
);

URL

在本节中,我们仔细看看URL 类。关于这个类的更多信息。

在这篇博文中,我们通过一个全局变量访问类URL ,因为它在其他网络平台上是这样使用的。但它也可以被导入。

import {URL} from 'node:url';

URIs vs. 相对引用

URLs是URIs的一个子集。RFC 3986,URI的标准,区分了两种URI-引用

  • 一个URI一个方案开始,后面是一个冒号分隔符。
  • 所有其他URI引用都是相对引用

URL 的构造函数

URL 类可以通过两种方式进行实例化。

  • new URL(uri: string)

    uri 必须是一个URI。它指定了新实例的URI。

  • new URL(uriRef: string, baseUri: string)

    baseUri 必须是一个URI。如果 是一个相对引用,它将被解析为与 相对应,其结果成为新实例的 URI。uriRef baseUri

    如果uriRef 是一个URI,它将完全取代baseUri ,成为实例所基于的数据。

在这里我们可以看到这个类的作用。

// If there is only one argument, it must be a proper URI
assert.equal(
  new URL('https://example.com/public/page.html').toString(),
  'https://example.com/public/page.html'
);
assert.throws(
  () => new URL('../book/toc.html'),
  /^TypeError \[ERR_INVALID_URL\]: Invalid URL$/
);

// Resolve a relative reference against a base URI 
assert.equal(
  new URL(
    '../book/toc.html',
    'https://example.com/public/page.html'
  ).toString(),
  'https://example.com/book/toc.html'
);

解决对URL 实例的相对引用问题

让我们重新审视一下URL 构造函数的这个变体。

new URL(uriRef: string, baseUri: string)

参数baseUri 被强制为字符串。因此,任何对象都可以被使用--只要它在被胁迫为字符串时成为一个有效的URL。

const obj = { toString() {return 'https://example.com'} };
assert.equal(
  new URL('index.html', obj).href,
  'https://example.com/index.html'
);

这使我们能够解决对URL 实例的相对引用。

const url = new URL('https://example.com/dir/file1.html');
assert.equal(
  new URL('../file2.html', url).href,
  'https://example.com/file2.html'
);

以这种方式使用,构造函数与path.resolve() 宽泛地相似。

URL 实例的属性

URL 的实例有以下属性。

type URL = {
  protocol: string,
  username: string,
  password: string,
  hostname: string,
  port: string,
  host: string,
  readonly origin: string,
  
  pathname: string,
  
  search: string,
  readonly searchParams: URLSearchParams,
  hash: string,

  href: string,
  toString(): string,
  toJSON(): string,
}

将URL转换为字符串

有三种常见的方法,我们可以将URL转换为字符串。

const url = new URL('https://example.com/about.html');

assert.equal(
  url.toString(),
  'https://example.com/about.html'
);
assert.equal(
  url.href,
  'https://example.com/about.html'
);
assert.equal(
  url.toJSON(),
  'https://example.com/about.html'
);

方法.toJSON() 使我们能够在JSON数据中使用URLs。

const jsonStr = JSON.stringify({
  pageUrl: new URL('https://2ality.com/p/subscribe.html')
});
assert.equal(
  jsonStr, '{"pageUrl":"https://2ality.com/p/subscribe.html"}'
);

获取URL 的属性

URL 实例的属性不是自己的数据属性,它们是通过getters和setters实现的。在下一个例子中,我们使用实用函数pickProps() (其代码在最后显示),将这些getters返回的值复制到一个普通对象中。

const props = pickProps(
  new URL('https://jane:pw@example.com:80/news.html?date=today#misc'),
  'protocol', 'username', 'password', 'hostname', 'port', 'host',
  'origin', 'pathname', 'search', 'hash', 'href'
);
assert.deepEqual(
  props,
  {
    protocol: 'https:',
    username: 'jane',
    password: 'pw',
    hostname: 'example.com',
    port: '80',
    host: 'example.com:80',
    origin: 'https://example.com:80',
    pathname: '/news.html',
    search: '?date=today',
    hash: '#misc',
    href: 'https://jane:pw@example.com:80/news.html?date=today#misc'
  }
);
function pickProps(input, ...keys) {
  const output = {};
  for (const key of keys) {
    output[key] = input[key];
  }
  return output;
}

唉,路径名是一个单一的原子单元。也就是说,我们不能使用类URL 来访问它的各个部分(基础、扩展等)。

设置URL的部分内容

我们还可以通过设置属性(如.hostname )来改变URL的部分。

const url = new URL('https://example.com');
url.hostname = '2ality.com';
assert.equal(
  url.href, 'https://2ality.com/'
);

我们可以使用设置器从部件中创建URL(Haroen Viaene的想法)。

// Object.assign() invokes setters when transferring property values
const urlFromParts = (parts) => Object.assign(
  new URL('https://example.com'), // minimal dummy URL
  parts // assigned to the dummy
);

const url = urlFromParts({
  protocol: 'https:',
  hostname: '2ality.com',
  pathname: '/p/about.html',
});
assert.equal(
  url.href, 'https://2ality.com/p/about.html'
);

通过.searchParams 管理搜索参数

我们可以使用属性.searchParams 来管理URLs的搜索参数。它的值是一个 URLSearchParams.

我们可以用它来读取搜索参数。

const url = new URL('https://example.com/?topic=js');
assert.equal(
  url.searchParams.get('topic'), 'js'
);
assert.equal(
  url.searchParams.has('topic'), true
);

我们还可以通过它改变搜索参数。

url.searchParams.append('page', '5');
assert.equal(
  url.href, 'https://example.com/?topic=js&page=5'
);

url.searchParams.set('topic', 'css');
assert.equal(
  url.href, 'https://example.com/?topic=css&page=5'
);

在URL和文件路径之间进行转换

在文件路径和URL之间手动转换是很诱人的。例如,我们可以尝试通过myUrl.pathnameURL 实例myUrl 转换为一个文件路径。然而这并不总是奏效--最好是使用这个函数

url.fileURLToPath(url: URL | string): string

下面的代码将该函数的结果与.pathname 的值进行比较。

import * as assert from 'assert';
import * as url from 'node:url';

//::::: Unix :::::

const url1 = new URL('file:///tmp/with%20space.txt');
assert.equal(
  url1.pathname, '/tmp/with%20space.txt');
assert.equal(
  url.fileURLToPath(url1), '/tmp/with space.txt');

const url2 = new URL('file:///home/thor/Mj%C3%B6lnir.txt');
assert.equal(
  url2.pathname, '/home/thor/Mj%C3%B6lnir.txt');
assert.equal(
  url.fileURLToPath(url2), '/home/thor/Mjölnir.txt');

//::::: Windows :::::

const url3 = new URL('file:///C:/dir/');
assert.equal(
  url3.pathname, '/C:/dir/');
assert.equal(
  url.fileURLToPath(url3), 'C:\\dir\\');

这个函数url.fileURLToPath() 的逆函数。

url.pathToFileURL(path: string): URL

它将path 转换为一个文件URL。

> url.pathToFileURL('/home/john/Work Files').href
'file:///home/john/Work%20Files'

URLs的用例:访问相对于当前模块的文件

URLs的一个重要用例是访问当前模块的兄弟姐妹的文件。

function readData() {
  const url = new URL('data.txt', import.meta.url);
  return fs.readFileSync(url, {encoding: 'UTF-8'});
}

这个函数使用 import.meta.url它包含了当前模块的URL(在Node.js上通常是一个file: URL)。

使用fetch() ,可以使以前的代码更具有跨平台性。然而,从Node.js 18.5开始,fetch() 还不能用于file: URLs。

> await fetch('file:///tmp/file.txt')
TypeError: fetch failed
  cause: Error: not implemented... yet...

URLs的用例:检测当前模块是否作为脚本运行

博文 "Node.js:检查ESM模块是否为'main'"。

路径 vs.file: URLs

当shell脚本接收对文件的引用或输出对文件的引用时(例如通过在屏幕上记录它们),它们几乎都是路径。然而,有两种情况下我们需要URL(正如前面几个小节所讨论的)。

  • 访问相对于当前模块的文件
  • 检测当前模块是否以脚本形式运行