浅析path常用工具函数源码

更新日期: 2022-06-08阅读: 658标签: node

在开发过程中,会经常用到 Node.js ,它利用 V8 提供的能力,拓展了 JS 的能力。而在 Node.js 中,我们可以使用 JS 中本来不存在的 path 模块,为了我们更加熟悉的运用,让我们一起来了解一下吧~

本文 Node.js 版本为 16.14.0,本文的源码来自于此版本。希望大家阅读本文后,会对大家阅读源码有所帮助。

path 的常见使用场景

Path 用于处理文件和目录的路径,这个模块中提供了一些便于开发者开发的工具函数,来协助我们进行复杂的路径判断,提高开发效率。例如:

  1. 在项目中配置别名,别名的配置方便我们对文件更简便的引用,避免深层级逐级向上查找。
reslove: {
  alias: {
    // __dirname 当前文件所在的目录路径
    'src': path.resolve(__dirname, './src'),
    // process.cwd 当前工作目录
    '@': path.join(process.cwd(), 'src'),
  },
}
  1. webpack 中,文件的输出路径也可以通过我们自行配置生成到指定的位置。
module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js',
  },
};
  1. 又或者对于文件夹的操作
let fs = require("fs");
let path = require("path");

// 删除文件夹
let deleDir = (src) => {
    // 读取文件夹
    let children = fs.readdirSync(src);
    children.forEach(item => {
        let childpath = path.join(src, item);
        // 检查文件是否存在
        let file = fs.statSync(childpath).isFile();
        if (file) {
            // 文件存在就删除
            fs.unlinkSync(childpath)
        } else {
            // 继续检测文件夹
            deleDir(childpath)
        }
    })
    // 删除空文件夹
    fs.rmdirSync(src)
}
deleDir("../floor")

简单的了解了一下 path 的使用场景,接下来我们根据使用来研究一下它的执行机制,以及是怎么实现的。

path 的执行机制


  1. 引入 path 模块,调用 path 的工具函数的时候,会进入原生模块的处理逻辑。
  2. 使用 _load 函数根据你引入的模块名作为 ID,判断要加载的模块是原生 JS 模块后,会通过 loadNativeModule 函数,利用 id 从 _source (保存原生JS模块的源码字符串转成的 ASCII 码)中找到对应的数据加载原生 JS 模块。
  3. 执行 lib/path.js 文件,利用 process 判断操作系统,根据操作系统的不同,在其文件处理上可能会存在操作字符的差异化处理,但方法大致一样,处理完后返回给调用方。

常用工具函数简析

resolve 返回当前路径的绝对路径

resolve 将多个参数,依次进行拼接,生成新的绝对路径。

resolve(...args) {
  let resolvedDevice = '';
  let resolvedTail = '';
  let resolvedAbsolute = false;

  // 从右到左检测参数
  for (let i = args.length - 1; i >= -1; i--) {
    ......
  }

  // 规范化路径
  resolvedTail = normalizeString(resolvedTail, !resolvedAbsolute, '\\', isPathSeparator);

  return resolvedAbsolute ?
    `${resolvedDevice}\\${resolvedTail}` :
    `${resolvedDevice}${resolvedTail}` || '.';
}


根据参数获取路径,对接收到的参数进行遍历,参数的长度大于等于 0 时都会开始进行拼接,对拼接好的 path 进行非字符串校验,有不符合的参数则抛出 throw new ERR_INVALID_ARG_TYPE(name, 'string', value) , 符合要求则会对 path 进行长度判断,有值则 +=path 做下一步操作。

let path;

if (i >= 0) {
  path = args[i];
  // internal/validators
  validateString(path, 'path');
  // path 长度为 0 的话,会直接跳出上述代码块的 for 循环
  if (path.length === 0) {
    continue;
  }
} else if (resolvedDevice.length === 0) {
  // resolvedDevice 的长度为 0,给 path 赋值为当前工作目录
  path = process.cwd();
} else {
  // 赋值为环境对象或者当前工作目录
  path = process.env[`=${resolvedDevice}`] || process.cwd();
  if (path === undefined ||
      (StringPrototypeToLowerCase(StringPrototypeSlice(path, 0, 2)) !==
      StringPrototypeToLowerCase(resolvedDevice) &&
      StringPrototypeCharCodeAt(path, 2) === CHAR_BACKWARD_SLASH)) {
    // 对 path 进行非空与绝对路径判断得出 path 路径
    path = `${resolvedDevice}\\`;
  }
}


尝试匹配根路径,判断是否是只有一个路径分隔符('')或者 path 为绝对路径,然后给绝对路径打标,并把 rootEnd 截取标识设为 1 (下标)。第二项若还是路径分隔符(''),就定义截取值为 2 (下标),并用 last 保存截取值,以便后续判断使用。

继续判断第三项是否是路径分隔符(''),如果是,那么为绝对路径, rootEnd 截取标识为 1 (下标),但也有可能是 UNC 路径(\servername\sharename,servername 服务器名。sharename 共享资源名称)。如果有其他值,截取值会继续进行自增读取后面的值,并用 firstPart 保存第三位的值,以便拼接目录时取值,并把 last 和截取值保持一致,以便结束判断。

const len = path.length;
let rootEnd = 0; // 路径截取结束下标
let device = ''; // 磁盘根 D:\、C:\
let isAbsolute = false; // 是否是磁盘根路径
const code = StringPrototypeCharCodeAt(path, 0);

// path 长度为 1
if (len === 1) {
  // 只有一个路径分隔符\为绝对路径
  if (isPathSeparator(code)) {
    rootEnd = 1;
    isAbsolute = true;
  }
} else if (isPathSeparator(code)) {
  // 可能是 UNC 根,从一个分隔符 \ 开始,至少有一个它就是某种绝对路径(UNC或其他)
  isAbsolute = true;
  // 开始匹配双路径分隔符
  if (isPathSeparator(StringPrototypeCharCodeAt(path, 1))) {
    let j = 2;
    let last = j;
    // 匹配一个或多个非路径分隔符
    while (j < len &&
            !isPathSeparator(StringPrototypeCharCodeAt(path, j))) {
      j++;
    }
    if (j < len && j !== last) {
      const firstPart = StringPrototypeSlice(path, last, j);
      last = j;
      // 匹配一个或多个路径分隔符
      while (j < len &&
              isPathSeparator(StringPrototypeCharCodeAt(path, j))) {
        j++;
      }
      if (j < len && j !== last) {
        last = j;
        while (j < len &&
                !isPathSeparator(StringPrototypeCharCodeAt(path, j))) {
          j++;
        }
        if (j === len || j !== last) {
          device =
            `\\\\${firstPart}\\${StringPrototypeSlice(path, last, j)}`;
          rootEnd = j;
        }
      }
    }
  } else {
    rootEnd = 1;
  }
// 检测磁盘根目录匹配 例:D:,C:\
} else if (isWindowsDeviceRoot(code) && StringPrototypeCharCodeAt(path, 1) === CHAR_COLON) {
  device = StringPrototypeSlice(path, 0, 2);
  rootEnd = 2;
  if (len > 2 && isPathSeparator(StringPrototypeCharCodeAt(path, 2))) {
    isAbsolute = true;
    rootEnd = 3;
  }
}

检测路径并生成,检测磁盘根目录是否存在或解析 resolvedAbsolute 是否为绝对路径。

// 检测磁盘根目录
if (device.length > 0) {
  // resolvedDevice 有值
  if (resolvedDevice.length > 0) {
    if (StringPrototypeToLowerCase(device) !==
        StringPrototypeToLowerCase(resolvedDevice))
      continue;
  } else {
    // resolvedDevice 无值并赋值为磁盘根目录
    resolvedDevice = device;
  }
}

// 绝对路径
if (resolvedAbsolute) {
  // 磁盘根目录存在结束循环
  if (resolvedDevice.length > 0)
    break;
} else {
  // 获取路径前缀进行拼接
  resolvedTail =
    `${StringPrototypeSlice(path, rootEnd)}\\${resolvedTail}`;
  resolvedAbsolute = isAbsolute;
  if (isAbsolute && resolvedDevice.length > 0) {
    // 磁盘根存在便结束循环
    break;
  }
}

join 根据传入的 path 片段进行路径拼接


  1. 接收多个参数,利用特定分隔符作为定界符将所有的 path 参数连接在一起,生成新的规范化路径。
  2. 接收参数后进行校验,如果没有参数的话,会直接返回 '.',反之进行遍历,通过内置 validateString 方法校验每个参数,如有一项不合规则直接 throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
  3. window 下为反斜杠(''), 而 linux 下为正斜杠('/'),这里是 join 方法区分操作系统的一个不同点,而反斜杠()有转义符的作用,单独使用会被认为是要转义斜杠后面的字符串,故此使用双反斜杠转义出反斜杠('')使用。
  4. 最后进行拼接后的字符串校验并格式化返回。
if (args.length === 0)
    return '.';

let joined;
let firstPart;
// 从左到右检测参数
for (let i = 0; i < args.length; ++i) {
  const arg = args[i];
  // internal/validators
  validateString(arg, 'path');
  if (arg.length > 0) {
    if (joined === undefined)
      // 把第一个字符串赋值给joined,并用firstPart变量保存第一个字符串以待后面使用
      joined = firstPart = arg;
    else
      // joined有值,进行+=拼接操作
      joined += `\\${arg}`;
  }
}

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

在 window 系统下,因为使用反斜杠()和 UNC (主要指局域网上资源的完整 Windows 2000 名称)路径的缘故,需要进行网络路径处理,('\')代表的是网络路径格式,因此在 win32 下挂载的 join 方法默认会进行截取操作。

如果匹配得到反斜杠(''), slashCount 就会进行自增操作,只要匹配反斜杠('')大于两个就会对拼接好的路径进行截取操作,并手动拼接转义后的反斜杠('')。

let needsReplace = true;
let slashCount = 0;
// 根据 StringPrototypeCharCodeAt 对首个字符串依次进行 code 码提取,并通过 isPathSeparator 方法与定义好的 code 码进行匹配
if (isPathSeparator(StringPrototypeCharCodeAt(firstPart, 0))) {
  ++slashCount;
  const firstLen = firstPart.length;
  if (firstLen > 1 &&
      isPathSeparator(StringPrototypeCharCodeAt(firstPart, 1))) {
    ++slashCount;
    if (firstLen > 2) {
      if (isPathSeparator(StringPrototypeCharCodeAt(firstPart, 2)))
        ++slashCount;
      else {
        needsReplace = false;
      }
    }
  }
}

if (needsReplace) {
  while (slashCount < joined.length &&
          isPathSeparator(StringPrototypeCharCodeAt(joined, slashCount))) {
    slashCount++;
  }

  if (slashCount >= 2)
    joined = `\\${StringPrototypeSlice(joined, slashCount)}`;
}

执行结果梳理

resolvejoin
无参数当前文件的绝对路径.
参数无绝对路径当前文件的绝对路径按顺序拼接参数拼接成的路径
首个参数为绝对路径参数路径覆盖当前文件绝对路径并拼接后续非绝对路径拼接成的绝对路径
后置参数为绝对路径参数路径覆盖当前文件绝对路径并覆盖前置参数拼接成的路径
首个参数为(./)有后续参数,当前文件的绝对路径拼接参数
无后续参数,当前文件的绝对路径
有后续参数,后续参数拼接成的路径
无后续参数,(./)
后置参数有(./)解析后的绝对路径拼接参数有后续参数,拼接成的路径拼接后续参数
无后续参数,拼接(/)
首个参数为(../)有后续参数,覆盖当前文件的绝对路径的最后一级目录后拼接参数
无后续参数,覆盖当前文件的绝对路径的最后一级目录
有后续参数,拼接后续参数
无后续参数,(../)
后置参数有(../)出现(../)的上层目录会被覆盖,后置出现多少个,就会覆盖多少层,上层目录被覆盖完后,返回(/),后续参数会拼接出现(../)的上层目录会被覆盖,后置出现多少个,就会覆盖多少层,上层目录被覆盖完后,会进行参数拼接

总结

阅读了源码之后, resolve 方法会对参数进行处理,考虑路径的形式,在最后抛出绝对路径。在使用的时候,如果是进行文件之类的操作,推荐使用 resolve 方法,相比来看, resolve 方法就算没有参数也会返回一个路径,供使用者操作,在执行过程中会进行路径的处理。而 join 方法只是对传入的参数进行规范化拼接,对于生成一个新的路径比较实用,可以按照使用者意愿创建。不过每个方法都有优点,要根据自己的使用场景以及项目需求,去选择合适的方法。

作者:方醒
来自:https://www.zoo.team/article/path-tool


链接: https://www.fly63.com/article/detial/11683

关于 Node.js 里 ES6 Modules 的一次更新说明

关于 Node.js 里 ES6 Modules 的一次更新说明,总结来说:CommonJS 与 ES6 Modules 之间的关键不同在于代码什么时候知道一个模块的结构和使用它。

用node.js开发一个可交互的命令行应用

在这个教程中,我们会开发一个命令行应用,它可以接收一个 CSV 格式的用户信息文件,教程的内容大纲:“Hello,World”,处理命令行参数,运行时的用户输入,异步网络会话,美化控制台的输出,封装成 shell 命令,JavaScript 之外

Node启动https服务器

首先你需要生成https证书,可以去付费的网站购买或者找一些免费的网站,可能会是key或者crt或者pem结尾的。不同格式之间可以通过OpenSSL转换

nodejs 异步转同步

nodej项目在微信环境开发,nodejs的异步特效,会导致请求没有完成就执行下面的代码,出现错误。经过多方查找,可以使用async模块来异步转同步,只有前一个function执行callback,下一个才会执行。

基于node服务器的大文件(G级)上传

3G的大文件分1500个2M二进度文件,通post方法发送给node服务,服务器全部接收到文件后,进组装生成你上文件。

为什么要把 JavaScript 放到服务器端上运行?

JavaScript比C的开发门槛要低,尽管服务器端JavaScript存在已经很多年了,但是后端部分一直没有市场,JavaScript在浏览器中有广泛的事件驱动方面的应用,考虑到高性能、符合事件驱动、没有历史包袱这3个主要原因,JavaScript成为了Node的实现语言。

了解node.js事件循环

node.js的第一个基本论点是I / O的性能消耗是很昂贵。因此,使用当前编程技术的最大浪费来自于等待I / O完成。有几种方法可以处理性能影响

Node.js 应用:Koa2 使用 JWT 进行鉴权

在前后端分离的开发中,通过 Restful API 进行数据交互时,如果没有对 API 进行保护,那么别人就可以很容易地获取并调用这些 API 进行操作。那么服务器端要如何进行鉴权呢?

Node.js 前端开发指南

我们经常跟Node.js打交道,即使你是一名前端开发人员 -- npm脚本,webpack配置,gulp任务,程序打包 或 运行测试等。即使你真的不需要深入理解这些任务,但有时候你会感到困惑,会因为缺少Node.js的一些核心概念而以非常奇怪的方式来编码。

happypack提升项目构建速度

运行在 Node.js 之上的 Webpack 是单线程模型的,也就是说 Webpack 需要处理的任务需要一件件挨着做,不能多个事情一起做。happypack把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!