jsernews 1.0.1

<~>

ts 231 days ago.

原文链接:https://blog.safia.rocks/post/167452569077/node-module-deep-dive-path

又一个星期一,又一个 Node 模块演练!作为这个系列文章的第二部分,我决定看看 Node 中的 path 模块。path 模块使开发人员能够处理文件和目录路径。您可能已经使用它来执行诸如确定路径最后部分或将多个路径连接在一起等事情。以下代码片段显示了 path 模块的使用示例。

> const path = require('path');
> path.basename('~/this/is/a/test/file.html');
'file.html'
> path.dirname('~/this/is/a/test/file.html');
'~/this/is/a/test'
> path.extname('~/this/is/a/test/file.html');
'.html'
> path.join('~', 'this', 'is', 'a', 'test');
'~/this/is/a/test'

如果您有兴趣探索 path 模块 API 的全部,你可以查看该模块的文档

好的!现在是时候深入代码本身了。就像上次一样,这里是一个链接到我将在这篇文章中阅读的代码版本。我将在文章包含部分嵌入式的代码片段,但也会使用指向特定行的链接引用代码库的相关部分。

在我开始深入代码本身之前,我快速浏览它以了解代码的常见模式。大部分情况下,模块中定义的许多函数迭代计算不同的字符串操作。有很多 for 循环和条件判断。另外需要注意的是从 path 模块公开 API 函数的方式。path 模块定义了两个对象 win32posix,用于存储每个平台的函数定义。在最终导出中,模块检测模块正在从哪个平台调用并导出与该平台匹配的函数。对于我读的代码,我特别关注为 posix 平台定义的函数,因为这是我最常开发和部署的平台。

if (process.platform === 'win32')
  module.exports = win32;
else
  module.exports = posix;

由于 path 模块 API 暴露了很多函数,因此我将着重介绍一些最常见的函数。我研究的第一个函数是 join 函数。您可能已经在 Express 服务器配置和基于 Electron 的桌面应用程序中看到了很多次。

join: function join() {
    if (arguments.length === 0)
      return '.';

对这段代码感兴趣的第一件事是它使用 JavaScript 中的 arguments 对象。arguments 对象是一个类似于数组的数据结构,它与函数的参数相对应。join 方法将路径列表作为其输入。与其为函数定义一个专门的参数,join 不如依赖于 arguments 对象,它使用户能够为函数提供任意数量的参数。

    var joined;
    for (var i = 0; i  0) {
        if (joined === undefined)
          joined = arg;
        else
          joined += '/' + arg;
      }
    }

该函数迭代每个参数,检查它是否为有效路径,并将其连接到名为 joined 的连接字符串。assertPath 是一个简单的函数,用于检查传递给 assertPath 函数的参数是否为字符串。

function assertPath(path) {
  if (typeof path !== 'string') {
    throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'path', 'string');
  }
}

最后,函数对连接路径进行规格化并返回。

    if (joined === undefined)
      return '.';
    return posix.normalize(joined);

在我看来,这是一个非常直截了当的函数,也是绝对属于标准库的代码的一个很好的例子。

之后我想查看的是 dirnamebasename 函数。

dirname: function dirname(path) {
  assertPath(path);
  if (path.length === 0)
    return '.';

函数开始于检测作为参数传入的 path 的有效性。

    var code = path.charCodeAt(0);
    var hasRoot = (code === 47/*/*/);

函数检查提供的路径中的第一个字符是否为 /,表示提供的路径是从文件系统的根目录开始的绝对路径(因为我们位于POSIX系统上)。稍后会使用此检查结果来确定是否保留最终字符串中根路径 / 的存在。

    if (end === -1)
      return hasRoot ? '/' : '.';
    if (hasRoot && end === 1)
      return '//';

函数的主体是一个 for 循环,它从后向前遍历 path 以寻找 / 字符。如果找到了,它将跳出 for 循环并将其索引存储在名为 end 的变量中。稍后,它会使用 end 来在提供的路径中获取适当的子字符串。

    return path.slice(0, end);

因此,基本上 dirname 函数的逻辑如下:从后向前的遍历 path,直到碰到 / 字符。如果是这样,这意味着您已遍历路径里文件名的所有字符,其余字符则对应于目录名称。

basename 函数逻辑相对更多一点。它有两个参数:path 查找基本名称的路径和可选的 ext,从基本名称截断的扩展名。按照惯例,它通过检测传入参数的有效性开始。

basename: function basename(path, ext) {
    if (ext !== undefined && typeof ext !== 'string')
      throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'ext', 'string');
    assertPath(path);

如果用户提供 ext 参数,与迭代相关的逻辑有点复杂,所以我首先查看了在没有 ext 参数的情况下调用 basename 的逻辑。

      for (i = path.length - 1; i >= 0; --i) {
        if (path.charCodeAt(i) === 47/*/*/) {
          // If we reached a path separator that was not part of a set of path
          // separators at the end of the string, stop now
          if (!matchedSlash) {
            start = i + 1;
            break;
          }
        } else if (end === -1) {
          // We saw the first non-path separator, mark this as the end of our
          // path component
          matchedSlash = false;
          end = i + 1;
        }
      }

      if (end === -1)
        return '';
      return path.slice(start, end);

函数从后向前遍历字符串(类似于 dirname 函数),直到找到 / 字符的第一个实例。这一次,它使用该字符的索引作为路径上切片的开始。

用户提供 ext 参数的情况的逻辑更加复杂,因为它必须跟踪字符串中的扩展名。

for (i = path.length - 1; i >= 0; --i) {
        const code = path.charCodeAt(i);
        if (code === 47/*/*/) {
          // If we reached a path separator that was not part of a set of path
          // separators at the end of the string, stop now
          if (!matchedSlash) {
            start = i + 1;
            break;
          }
        } else {

它从后向前遍历字符串以查找第一个 / 字符并将其存储为切片的起点并跳出循环。如果在它所处的位置没有找到 / 字符,它会交替地计算一些逻辑来确定扩展名结束的位置,并将其作为字符串的结尾存储。

        if (firstNonSlashEnd === -1) {
            // We saw the first non-path separator, remember this index in case
            // we need it if the extension ends up not matching
            matchedSlash = false;
            firstNonSlashEnd = i + 1;
          }
          if (extIdx >= 0) {
            // Try to match the explicit extension
            if (code === ext.charCodeAt(extIdx)) {
              if (--extIdx === -1) {
                // We matched the extension, so mark this as the end of our path
                // component
                end = i;
              }
            } else {
              // Extension does not match, so our result is the entire path
              // component
              extIdx = -1;
              end = firstNonSlashEnd;
            }

就是这样!大部分由 path 模块公开的函数都是简单的字符串操作(以及大量的边缘处理)。通过代码快速阅读发现,基于 win32 的文件系统有关的函数肯定复杂得多,特别是在处理驱动器(C:D:)和转义方面(有趣!)。