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