大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说浅析 path 常用工具函数源码,希望您对编程的造诣更进一步.
这是第 146 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:浅析 path 常用工具函数源码
在开发过程中,会经常用到 Node.js ,它利用 V8 提供的能力,拓展了 JS 的能力。而在 Node.js 中,我们可以使用 JS 中本来不存在的 path 模块,为了我们更加熟悉的运用,让我们一起来了解一下吧~
本文 Node.js 版本为 16.14.0,本文的源码来自于此版本。希望大家阅读本文后,会对大家阅读源码有所帮助。
path 的常见使用场景
Path 用于处理文件和目录的路径,这个模块中提供了一些便于开发者开发的工具函数,来协助我们进行复杂的路径判断,提高开发效率。例如:
- 在项目中配置别名,别名的配置方便我们对文件更简便的引用,避免深层级逐级向上查找。
reslove: {
alias: {
// __dirname 当前文件所在的目录路径
'src': path.resolve(__dirname, './src'),
// process.cwd 当前工作目录
'@': path.join(process.cwd(), 'src'),
},
}
代码100分
- 在 webpack 中,文件的输出路径也可以通过我们自行配置生成到指定的位置。
代码100分module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
},
};
- 又或者对于文件夹的操作
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 的执行机制
- 引入 path 模块,调用 path 的工具函数的时候,会进入原生模块的处理逻辑。
- 使用
_load
函数根据你引入的模块名作为 ID,判断要加载的模块是原生 JS 模块后,会通过loadNativeModule
函数,利用 id 从_source
(保存原生JS模块的源码字符串转成的 ASCII 码)中找到对应的数据加载原生 JS 模块。 - 执行 lib/path.js 文件,利用 process 判断操作系统,根据操作系统的不同,在其文件处理上可能会存在操作字符的差异化处理,但方法大致一样,处理完后返回给调用方。
常用工具函数简析
resolve 返回当前路径的绝对路径
resolve 将多个参数,依次进行拼接,生成新的绝对路径。
代码100分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 片段进行路径拼接
- 接收多个参数,利用特定分隔符作为定界符将所有的 path 参数连接在一起,生成新的规范化路径。
- 接收参数后进行校验,如果没有参数的话,会直接返回 ‘.’ ,反之进行遍历,通过内置
validateString
方法校验每个参数,如有一项不合规则直接throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
- window 下为反斜杠 (‘\’) , 而 linux 下为正斜杠 (‘/’),这里是
join
方法区分操作系统的一个不同点,而反斜杠 (‘\’) 有转义符的作用,单独使用会被认为是要转义斜杠后面的字符串,故此使用双反斜杠转义出反斜杠 (‘\’) 使用。 - 最后进行拼接后的字符串校验并格式化返回。
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)}`;
}
执行结果梳理
resolve | join | |
---|---|---|
无参数 | 当前文件的绝对路径 | . |
参数无绝对路径 | 当前文件的绝对路径按顺序拼接参数 | 拼接成的路径 |
首个参数为绝对路径 | 参数路径覆盖当前文件绝对路径并拼接后续非绝对路径 | 拼接成的绝对路径 |
后置参数为绝对路径 | 参数路径覆盖当前文件绝对路径并覆盖前置参数 | 拼接成的路径 |
首个参数为(./) | 有后续参数,当前文件的绝对路径拼接参数 无后续参数,当前文件的绝对路径 |
有后续参数,后续参数拼接成的路径 无后续参数,(./) |
后置参数有(./) | 解析后的绝对路径拼接参数 | 有后续参数,拼接成的路径拼接后续参数 无后续参数,拼接(/) |
首个参数为(../) | 有后续参数,覆盖当前文件的绝对路径的最后一级目录后拼接参数 无后续参数,覆盖当前文件的绝对路径的最后一级目录 |
有后续参数,拼接后续参数 无后续参数,(../) |
后置参数有(../) | 出现(../)的上层目录会被覆盖,后置出现多少个,就会覆盖多少层,上层目录被覆盖完后,返回(/),后续参数会拼接 | 出现(../)的上层目录会被覆盖,后置出现多少个,就会覆盖多少层,上层目录被覆盖完后,会进行参数拼接 |
总结
阅读了源码之后,resolve
方法会对参数进行处理,考虑路径的形式,在最后抛出绝对路径。在使用的时候,如果是进行文件之类的操作,推荐使用 resolve
方法,相比来看, resolve
方法就算没有参数也会返回一个路径,供使用者操作,在执行过程中会进行路径的处理。而 join
方法只是对传入的参数进行规范化拼接,对于生成一个新的路径比较实用,可以按照使用者意愿创建。不过每个方法都有优点,要根据自己的使用场景以及项目需求,去选择合适的方法。
参考文献
Node.js 模块系统源码探微
webpack原理-如何实现代码打包
推荐阅读
Web Components-LitElement实践
模块联邦浅析
性能优化——图片压缩、加载和格式选择
如何基于 WebComponents 封装 UI 组件库
Web Worker
开源作品
- 政采云前端小报
开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)
- 商品选择 sku 插件
开源地址 github.com/zcy-inc/sku…
招贤纳士
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 60 余个前端小伙伴,平均年龄 27 岁,近 4 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/11335.html