从底层看 path.resolve 实现

前言

Node.js 中 path 模块的 resolve 方法,我们日常中用的可能相对来说较多。它的作用是将路径或路径片段的序列解析为绝对路径。但是可能大部分人没怎么了解过内部是怎么实现的,今天我们就来分析一下吧

path.resolve 作用

官网介绍:

path.resolve() 方法将路径或路径片段的序列解析为绝对路径。

给定的路径序列从右到左处理,每个后续的 path 会被追加到前面,直到构建绝对路径。 例如,给定路径片段的序列:/foo/barbaz,调用 path.resolve('/foo', '/bar', 'baz') 将返回 /bar/baz,因为 'baz' 不是绝对路径,而 '/bar' + '/' + 'baz' 是。

如果在处理完所有给定的 path 片段之后,还没有生成绝对路径,则使用当前工作目录。

生成的路径被规范化,并删除尾部斜杠(除非路径解析为根目录)。

零长度的 path 片段被忽略。

如果没有传入 path 片段,则 path.resolve() 将返回当前工作目录的绝对路径

其实解释的也差不多了,但是这里是漏掉了一个相对路径的处理,如果这样执行:path.resolve('/directory1/', './directory2/', '../'),则其输出的是/directory1

因为这里从右至左进行了拼接,node 在处理的时候,发现相对路径,直接将dirctory2给回退了上一级目录,也就是dirctory1

背景

node 的文档写的确实不咋地,而我又想弄清楚它里面究竟的实现是怎么样的,所以就有了这篇文章

实现

我这边是直接把源码拿了下,并且做了下简化,如果有朋友们想看详细源码的话,可以移步至这查看:传送门

下面是可以跑的 demo:

js 复制代码
const CHAR_DOT = 46 /* . */
const CHAR_FORWARD_SLASH = 47 /* / */
const CHAR_BACKWARD_SLASH = 92 /* \ */
const {
  ArrayPrototypeJoin,
  ArrayPrototypeSlice,
  FunctionPrototypeBind,
  StringPrototypeCharCodeAt,
  StringPrototypeIndexOf,
  StringPrototypeLastIndexOf,
  StringPrototypeRepeat,
  StringPrototypeReplace,
  StringPrototypeSlice,
  StringPrototypeSplit,
  StringPrototypeToLowerCase,
} = primordials;

/**
 * 是否为路径切割符
 */
function isPosixPathSeparator(code) {
  return code === CHAR_FORWARD_SLASH;
}

// Resolves . and .. elements in a path with directory names
function normalizeString(path, allowAboveRoot, separator, isPathSeparator) {
  // allowAboveRoot 传进来的是否为绝对路径
  // separator 为 /
  let res = '';
  let lastSegmentLength = 0; // 收集到的最后一级目录的长度
  let lastSlash = -1; // 最后分隔符的位置
  // 当前未处理的 . 数量
  let dots = 0;
  // 当前遍历的字符对应的 ascii 码
  let code = 0;
  for (let i = 0; i <= path.length; ++i) {
    if (i < path.length)
      code = StringPrototypeCharCodeAt(path, i);
    else if (isPathSeparator(code))
      break;
    else
      code = CHAR_FORWARD_SLASH;

    // 当前遍历字符是否为 / \
    if (isPathSeparator(code)) {
      // path 的第一个字符为 / 或者只有 1 个 . 的情况直接跳过
      if (lastSlash === i - 1 || dots === 1) {
        // NOOP
      } else if (dots === 2) { // 当前有 .. 未处理,这里执行 ../ 进入上一层目录的操作
        if (res.length < 2 || lastSegmentLength !== 2 ||
            StringPrototypeCharCodeAt(res, res.length - 1) !== CHAR_DOT ||
            StringPrototypeCharCodeAt(res, res.length - 2) !== CHAR_DOT) {
          if (res.length > 2) {
            const lastSlashIndex = StringPrototypeLastIndexOf(res, separator);
            // 没有找到分隔符的情况下,直接给 res 赋值 ''
            if (lastSlashIndex === -1) {
              res = '';
              lastSegmentLength = 0;
            } else {
              // 去除最后一个目录(达到 .. 的作用)
              res = StringPrototypeSlice(res, 0, lastSlashIndex);
              // 更新最后一个目录的长度
              lastSegmentLength =
                res.length - 1 - StringPrototypeLastIndexOf(res, separator);
            }
            lastSlash = i;
            dots = 0;
            continue;
          } else if (res.length !== 0) {
            res = '';
            lastSegmentLength = 0;
            lastSlash = i;
            dots = 0;
            continue;
          }
        }
        if (allowAboveRoot) {
          res += res.length > 0 ? `${separator}..` : '..';
          lastSegmentLength = 2;
        }
      } else {
        if (res.length > 0)
          // 先前有收集了的情况下,给其拼接 separator
          res += `${separator}${StringPrototypeSlice(path, lastSlash + 1, i)}`;
        else
          // res 长度为 0 时,将最后斜杠的位置 + 1,到 i - 1 进行赋值给 res
          // slice(j, k) => [j, k) 字符串截取位置
          res = StringPrototypeSlice(path, lastSlash + 1, i);
        // 更新当前路径的最后一级目录的最后一个字符的索引
        lastSegmentLength = i - lastSlash - 1;
      }
      // 将当前的位置记录为最后斜杠的位置
      lastSlash = i;
      // dots 数清 0
      dots = 0;
    } else if (code === CHAR_DOT && dots !== -1) { // 当前的字符为 .,并且 dots 不为 -1
      // dots 数 + 1
      ++dots;
    } else { // 当前字符不是 / \ .
      dots = -1;
    }
  }
  return res;
}

const posixCwd = (() => {
  if (process.platform === 'win32') {
    // Converts Windows' backslash path separators to POSIX forward slashes
    // and truncates any drive indicator
    const regexp = /\\/g;
    return () => {
      const cwd = StringPrototypeReplace(process.cwd(), regexp, '/');
      return StringPrototypeSlice(cwd, StringPrototypeIndexOf(cwd, '/'));
    };
  }

  // We're already on POSIX, no need for any transformations
  return () => process.cwd();
})();

const resolve = (...args) => {
  let resolvedPath = '';
  // 是否已经解析出绝对路径
  let resolvedAbsolute = false;
  // args[args.length - 1] 最后一个字符是否为 /
  let slashCheck = false;

  // 从右往左遍历,已经解析出绝对路径了的话,则不再继续进行遍历
  for (let i = args.length - 1; i >= 0 && !resolvedAbsolute; i--) {
    const path = args[i];
    // 判断当前是否为字符串
    // validateString(path, `paths[${i}]`);

    // Skip empty entries
    if (path.length === 0) {
      continue;
    }

    // 如果最后一个参数的最后一个字符为 /
    if (i === args.length - 1 &&
        isPosixPathSeparator(StringPrototypeCharCodeAt(path,
                                                       path.length - 1))) {
                                                        
      slashCheck = true;
    }

    // 对收集字符进行拼接
    // 前面已经收集过了,则将当前 path 直接进行拼接
    if (resolvedPath.length !== 0) {
      resolvedPath = `${path}/${resolvedPath}`;
    } else { // 前面未收集过,则将当前 path 直接赋值
      resolvedPath = path;
    }

    // 当前路径是否已经包含绝对路径,第0位字符是否为 /
    resolvedAbsolute =
      StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH;
  }

  // 如果未解析出绝对路径,则拼接当前的执行目录
  if (!resolvedAbsolute) {
    const cwd = posixCwd();
    resolvedPath = `${cwd}/${resolvedPath}`;
    resolvedAbsolute =
      StringPrototypeCharCodeAt(cwd, 0) === CHAR_FORWARD_SLASH;
  }

  // At this point the path should be resolved to a full absolute path, but
  // handle relative paths to be safe (might happen when process.cwd() fails)

  // Normalize the path
  // 将路径中非第0位的 .、.. 统一替换掉
  console.log('resolvedPath...before', resolvedPath);
  resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute, '/',
                                 isPosixPathSeparator);

  console.log('resolvedPath...after', resolvedPath);

  // 如果此时还未解析成绝对路径
  if (!resolvedAbsolute) {
    // 解析的路径长度为0,返回 .
    if (resolvedPath.length === 0) {
      return '.';
    }
    // 如果最后一位字符为 /,则把解析后的地址后面拼接 /
    if (slashCheck) {
      return `${resolvedPath}/`;
    }

    // 否则直接返回
    return resolvedPath;
  }

  if (resolvedPath.length === 0 || resolvedPath === '/') {
    return '/';
  }
  return slashCheck ? `/${resolvedPath}/` : `/${resolvedPath}`;
}

// 测试
console.log(resolve('/directory1/', './directory2/', '../'));

下面是代码流程: 1、resolve 方法将传入进行的路径参数,从右至左进行遍历,并且拼接路径,如果拼接路径已经构成绝对路径了话,就直接结束遍历,最终得到一个未完全处理的路径,如果该路径不是一个绝对路径,则会拼接当前工作目录进去,里面还有一些相对路径参数并未进行处理,表现请看:console.log('resolvedPath...before', resolvedPath); 输出 2、处理第一步的未完全路径,具体方法:normalizeString。里面干的事情就是处理相对路径字符,例如./../..,就是上文所说的相对路径回到上层目录逻辑 3、如果经过 normalizeString 处理完后,最终路径还不是绝对路径,则再进行处理

总结

其实代码逻辑不难,只是文档上没写全,就会让使用的人会存疑。好了~下期👋🏻

相关推荐
zqx_717 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己34 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai2 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端