淺析 path 常用工具函數(shù)源碼
淺析 path 常用工具函數(shù)源碼
https://www.zoo.team/article/path-tool
前言
在開發(fā)過程中,會經(jīng)常用到 Node.js (https://nodejs.org/dist/latest-v16.x/docs/api) ,它利用 V8 提供的能力,拓展了 JS 的能力。而在 Node.js 中,我們可以使用 JS 中本來不存在的 path (https://github.com/nodejs/node/blob/v16.14.0/lib/path.js) 模塊,為了我們更加熟悉的運用,讓我們一起來了解一下吧~
本文 Node.js 版本為 16.14.0,本文的源碼來自于此版本。希望大家閱讀本文后,會對大家閱讀源碼有所幫助。
path 的常見使用場景
Path 用于處理文件和目錄的路徑,這個模塊中提供了一些便于開發(fā)者開發(fā)的工具函數(shù),來協(xié)助我們進行復(fù)雜的路徑判斷,提高開發(fā)效率。例如:
在項目中配置別名,別名的配置方便我們對文件更簡便的引用,避免深層級逐級向上查找。
reslove: {
alias: {
// __dirname 當(dāng)前文件所在的目錄路徑
'src': path.resolve(__dirname, './src'),
// process.cwd 當(dāng)前工作目錄
'@': path.join(process.cwd(), 'src'),
},
}
在 webpack 中,文件的輸出路徑也可以通過我們自行配置生成到指定的位置。
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 {
// 繼續(xù)檢測文件夾
deleDir(childpath)
}
})
// 刪除空文件夾
fs.rmdirSync(src)
}
deleDir("../floor")
簡單的了解了一下 path 的使用場景,接下來我們根據(jù)使用來研究一下它的執(zhí)行機制,以及是怎么實現(xiàn)的。
path 的執(zhí)行機制
引入 path 模塊,調(diào)用 path 的工具函數(shù)的時候,會進入原生模塊的處理邏輯。
使用 _load 函數(shù)根據(jù)你引入的模塊名作為 ID,判斷要加載的模塊是原生 JS 模塊后,會通過 loadNativeModule 函數(shù),利用 id 從 _source (保存原生JS模塊的源碼字符串轉(zhuǎn)成的 ASCII 碼)中找到對應(yīng)的數(shù)據(jù)加載原生 JS 模塊。
執(zhí)行 lib/path.js 文件,利用 process 判斷操作系統(tǒng),根據(jù)操作系統(tǒng)的不同,在其文件處理上可能會存在操作字符的差異化處理,但方法大致一樣,處理完后返回給調(diào)用方。
常用工具函數(shù)簡析
resolve 返回當(dāng)前路徑的絕對路徑
resolve 將多個參數(shù),依次進行拼接,生成新的絕對路徑。
resolve(...args) {
let resolvedDevice = '';
let resolvedTail = '';
let resolvedAbsolute = false;
// 從右到左檢測參數(shù)
for (let i = args.length - 1; i >= -1; i--) {
......
}
// 規(guī)范化路徑
resolvedTail = normalizeString(resolvedTail, !resolvedAbsolute, '\\', isPathSeparator);
return resolvedAbsolute ?
`${resolvedDevice}\\${resolvedTail}` :
`${resolvedDevice}${resolvedTail}` || '.';
}
根據(jù)參數(shù)獲取路徑,對接收到的參數(shù)進行遍歷,參數(shù)的長度大于等于 0 時都會開始進行拼接,對拼接好的 path 進行非字符串校驗,有不符合的參數(shù)則拋出 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 循環(huán)
if (path.length === 0) {
continue;
}
} else if (resolvedDevice.length === 0) {
// resolvedDevice 的長度為 0,給 path 賦值為當(dāng)前工作目錄
path = process.cwd();
} else {
// 賦值為環(huán)境對象或者當(dāng)前工作目錄
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 為絕對路徑,然后給絕對路徑打標(biāo),并把 rootEnd 截取標(biāo)識設(shè)為 1 (下標(biāo))。第二項若還是路徑分隔符 ('\'),就定義截取值為 2 (下標(biāo)),并用 last 保存截取值,以便后續(xù)判斷使用。
繼續(xù)判斷第三項是否是路徑分隔符 ('\'),如果是,那么為絕對路徑,rootEnd 截取標(biāo)識為 1 (下標(biāo)),但也有可能是 UNC (https://baike.baidu.com/item/UNC%E8%B7%AF%E5%BE%84/3231808) 路徑(\servername\sharename,servername 服務(wù)器名。sharename 共享資源名稱)。如果有其他值,截取值會繼續(xù)進行自增讀取后面的值,并用 firstPart 保存第三位的值,以便拼接目錄時取值,并把 last 和截取值保持一致,以便結(jié)束判斷。
const len = path.length;
let rootEnd = 0; // 路徑截取結(jié)束下標(biāo)
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) {
// 磁盤根目錄存在結(jié)束循環(huán)
if (resolvedDevice.length > 0)
break;
} else {
// 獲取路徑前綴進行拼接
resolvedTail =
`${StringPrototypeSlice(path, rootEnd)}\\${resolvedTail}`;
resolvedAbsolute = isAbsolute;
if (isAbsolute && resolvedDevice.length > 0) {
// 磁盤根存在便結(jié)束循環(huán)
break;
}
}
join 根據(jù)傳入的 path 片段進行路徑拼接
接收多個參數(shù),利用特定分隔符作為定界符將所有的 path 參數(shù)連接在一起,生成新的規(guī)范化路徑。
接收參數(shù)后進行校驗,如果沒有參數(shù)的話,會直接返回 '.',反之進行遍歷,通過內(nèi)置 validateString 方法校驗每個參數(shù),如有一項不合規(guī)則直接 throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
window 下為反斜杠 ('\'), 而 linux 下為正斜杠 ('/'),這里是 join 方法區(qū)分操作系統(tǒng)的一個不同點,而反斜杠 () 有轉(zhuǎn)義符的作用,單獨使用會被認(rèn)為是要轉(zhuǎn)義斜杠后面的字符串,故此使用雙反斜杠轉(zhuǎn)義出反斜杠 ('\') 使用。
最后進行拼接后的字符串校驗并格式化返回。
if (args.length === 0)
return '.';
let joined;
let firstPart;
// 從左到右檢測參數(shù)
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 系統(tǒng)下,因為使用反斜杠 ('\') 和 UNC (主要指局域網(wǎng)上資源的完整 Windows 2000 名稱) 路徑的緣故,需要進行網(wǎng)絡(luò)路徑處理,('\\') 代表的是網(wǎng)絡(luò)路徑格式,因此在 win32 下掛載的 join方法默認(rèn)會進行截取操作。
如果匹配得到反斜杠 ('\'),slashCount 就會進行自增操作,只要匹配反斜杠 ('\') 大于兩個就會對拼接好的路徑進行截取操作,并手動拼接轉(zhuǎn)義后的反斜杠 ('\')。
let needsReplace = true;
let slashCount = 0;
// 根據(jù) 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)}`;
}
執(zhí)行結(jié)果梳理
resolve | join | |
---|---|---|
無參數(shù) | 當(dāng)前文件的絕對路徑 | . |
參數(shù)無絕對路徑 | 當(dāng)前文件的絕對路徑按順序拼接參數(shù) | 拼接成的路徑 |
首個參數(shù)為絕對路徑 | 參數(shù)路徑覆蓋當(dāng)前文件絕對路徑并拼接后續(xù)非絕對路徑 | 拼接成的絕對路徑 |
后置參數(shù)為絕對路徑 | 參數(shù)路徑覆蓋當(dāng)前文件絕對路徑并覆蓋前置參數(shù) | 拼接成的路徑 |
首個參數(shù)為 (./) | 有后續(xù)參數(shù),當(dāng)前文件的絕對路徑拼接參數(shù) 無后續(xù)參數(shù),當(dāng)前文件的絕對路徑 | 有后續(xù)參數(shù),后續(xù)參數(shù)拼接成的路徑 無后續(xù)參數(shù),(./) |
后置參數(shù)有 (./) | 解析后的絕對路徑拼接參數(shù) | 有后續(xù)參數(shù),拼接成的路徑拼接后續(xù)參數(shù) 無后續(xù)參數(shù),拼接 (/) |
首個參數(shù)為(../) | 有后續(xù)參數(shù),覆蓋當(dāng)前文件的絕對路徑的最后一級目錄后拼接參數(shù) 無后續(xù)參數(shù),覆蓋當(dāng)前文件的絕對路徑的最后一級目錄 | 有后續(xù)參數(shù),拼接后續(xù)參數(shù) 無后續(xù)參數(shù),(../) |
后置參數(shù)有(../) | 出現(xiàn) (../) 的上層目錄會被覆蓋,后置出現(xiàn)多少個,就會覆蓋多少層,上層目錄被覆蓋完后,返回 (/),后續(xù)參數(shù)會拼接 | 出現(xiàn) (../) 的上層目錄會被覆蓋,后置出現(xiàn)多少個,就會覆蓋多少層,上層目錄被覆蓋完后,會進行參數(shù)拼接 |
總結(jié)
閱讀了源碼之后,resolve 方法會對參數(shù)進行處理,考慮路徑的形式,在最后拋出絕對路徑。在使用的時候,如果是進行文件之類的操作,推薦使用 resolve 方法,相比來看, resolve 方法就算沒有參數(shù)也會返回一個路徑,供使用者操作,在執(zhí)行過程中會進行路徑的處理。而 join 方法只是對傳入的參數(shù)進行規(guī)范化拼接,對于生成一個新的路徑比較實用,可以按照使用者意愿創(chuàng)建。不過每個方法都有優(yōu)點,要根據(jù)自己的使用場景以及項目需求,去選擇合適的方法。
參考文獻
Node.js 模塊系統(tǒng)源碼探微 (https://juejin.cn/post/6844904016317513741)
webpack原理-如何實現(xiàn)代碼 (https://juejin.cn/post/7031342702906048543)
作者:方醒
歡迎關(guān)注微信公眾號 :政采云前端團隊