这篇是正式阅读的第一篇,从最简单的path开始阅读,我记得之前在掘金收藏过node源码阅读的文章,想找来看看它们的写作思路,发现早就已经取消收藏了(大概是因为质量不高)。不过这样也好,不受到其他人思路的干扰,我的文章输出我自己的阅读收获,先尽量保持清晰。
path功能回顾
这个系列我希望能做到格式统一,第一部分先过一下API。我觉得这样做很有必要,对于不熟悉的功能可以先熟悉用法,对于熟悉的功能也可以做一个知识梳理,知道怎么用才能有机会去理解为什么这样用。
path是node中非常常用的一个模块,用来处理各种文件路径相关操作,path的API可以根据所在平台不同展现对应的行为,屏蔽了win和*nix系统之间差异。
path常见API及功能如下:
- path.basename(path[, ext])
- path.delimiter
- path.dirname(path)
- path.extname(path)
- path.format(pathObject)
- path.isAbsolute(path)
- path.join([…paths])
- path.normalize(path)
- path.parse(path)
- path.posix
- path.relative(from, to)
- path.resolve([…paths])
- path.sep
- path.toNamespacedPath(path)
- path.win32
具体的功能链接里面都有,下面分析源码时候会逐个讲解,这部分就不再重复。
path API 源码阅读
path源码的最后一行如下,导出的内容就是我们require时引入的内容,这里利用process模块识别当前操作系统是否为win32,根据操作系统导出不同内容。process也是node提供的一个模块,内部肯定是要调用原生API来处理,后面还会读到这里,所以在此不作具体分析。
win32很熟悉,就是windows环境。posix是一种线程标准(详见wiki),最早为unix系统使用,后来unix-like系统也开始遵守此标准,现在Windows其实也有兼容,但是通常还是用来作为 nix 系统的代指,个人理解这里可以理解为 nix 系统。具体二者之间的平台差异会在具体API下对比分析。
1 | module.exports = process.platform === 'win32' ? win32 : posix; |
之后来看win32和posix两个对象,其实很简单,里面只有一些属性和方法,这些就是暴露出的path相关API,我们可以在程序中使用path加点调用的,上面已经对其进行了一一列举,下面就依次逐个分析。(因为文档上的排列顺序是按照字母表的,这里为了对应沿用下来,具体在源码中的位置可以搜索查看)
path.basename(path[, ext])
这个方法用于获取path的最后一部分,类似于Unix的basename命令,第二个参数为扩展名,如果传入会返回去除扩展名之后的结果。直接引用官方示例:
1 | path.basename('/foo/bar/baz/asdf/quux.html'); // 返回: 'quux.html' |
实现方式其实两端差异并不是很大,先看posix平台下的逻辑,最后再来看差异的部分。
这个方法的逻辑想想也知道,匹配出最后一个分隔符(posix平台为/符号),截取后面的内容,如果传入了扩展名再截下扩展名部分,返回最终结果。
进入方法首先通过validateString校验参数,如果传入的不是string会抛出错误。之后这里首先处理有扩展名的情况(扩展名长度要小于路径长度,否则视为无效,按照无扩展处理),这里从后向前遍历path,通过charCodeAt获取code,判断是否为分隔符(posix平台为/符号),最终截取首次匹配到分隔符的位置的下一个位置开始,到扩展名前一个位置结束的内容,即为返回结果。对于没传入扩展名的情况,只要截取到结尾位置即可。
对于Windows系统这里有两处不同,首先是盘符判断,Windows系统可以有多个磁盘分区,对于从根目录开始的情况,路径前面会携带盘符信息如C:,这种情况需要排除前两个字符,从第三个字符开始处理。另外,Windows的分隔符为/或\两种都可以,所以这里需要判断两种情况,在源码中的isPathSeparator方法处理了win32的两种分隔符。
特别的,在js等很多编程语言里,
\
为转义字符,所以想表示\字符本身需要写作\\
。
path.delimiter
这是一个常量,返回对应操作系统的路径定界符,路径定界符就是指并列写多个路径时候用来分隔的符号,一个常见的场景就是配置环境变量时候,对于多个值中间的分隔,Windows上为;
,而在posix上则是:
。在源码中也就是两个导出的属性值常量。
在程序中,我们可以这样使用:
1 | // posix |
path.dirname(path)
这个方法用于获取path的目录名,类似于Unix的dirname命令,示例:
1 | path.dirname('/foo/bar/baz/asdf/quux'); // 返回: '/foo/bar/baz/asdf' |
这个方法在win32和posix上的实现方式有很大区别,首先来看比较简单的posix平台。
posix平台下的实现很简单,依旧是从后向前遍历,找到第一个分隔符截取前面内容即可,特别的,对于没匹配到的情况,绝对路径返回/
相对路径返回.
。
在win32平台下核心处理逻辑和posix是相同的,但是关于开头的盘符相关处理有一段特有的逻辑,通过多次遍历处理最终截取正确的dirname。
path.extname(path)
返回扩展名,包括.
符号,这里就是一个字符串匹配,找到最后一次出现.
的位置截取后面内容,如果.
出现在文件首部则返回空。
1 | path.extname('index.html'); // 返回: '.html' |
具体的匹配思路也没有什么特别之处,依旧是从后向前遍历,win32下需要处理首部的盘符信息。
path.format(pathObject)
path.isAbsolute(path)
判断是否为绝对路径,在posix上很简单,判断是否以/
开头即可,win32上复杂一些,以盘符开头的也符合绝对路径条件。
1 | // posix |
path.join([…paths])
path.normalize(path)
path.parse(path)
path.posix
返回path方法中的posix部分。在源码的结尾处有这样两行:
1 | posix.win32 = win32.win32 = win32; |
这里在导出的内容上都挂载了win32和posix两个对象的引用,无论在什么平台上,都可以通过path获取想要的对象,通过这个对象可以访问对应平台下的属性和方法。
path.relative(from, to)
path.resolve([…paths])
path.sep
一个常量,系统分隔符属性,在win32返回\
,在posix返回/
,实际上win32两种都支持,这里只返回\
,同样由于转义字符的原因,源码里为\\
。常见用法:
1 | // posix |
path.toNamespacedPath(path)
这个方法只在win32生效,posix环境是一个空方法,直接返回path。关于namespace相关的内容参见这个链接,相关的东西我没使用过,源码也只是根据格式对path做了匹配。
path.win32
见path.posix。
总结
这是第一篇node源码解读文章,写的是最简单的path模块,但是真正开始写起来,要比想象的困难很多,可能也是很长时间没写作的原因吧,完成最简单的一篇用了好多天,后面我可能会适当调整方法了。