前言
Node.js 中 path 模块的 resolve 方法,我们日常中用的可能相对来说较多。它的作用是将路径或路径片段的序列解析为绝对路径
。但是可能大部分人没怎么了解过内部是怎么实现的,今天我们就来分析一下吧
path.resolve 作用
官网介绍:
path.resolve()
方法将路径或路径片段的序列解析为绝对路径。给定的路径序列从右到左处理,每个后续的
path
会被追加到前面,直到构建绝对路径。 例如,给定路径片段的序列:/foo
、/bar
、baz
,调用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
处理完后,最终路径还不是绝对路径,则再进行处理
总结
其实代码逻辑不难,只是文档上没写全,就会让使用的人会存疑。好了~下期👋🏻