在開發過程中,會常用到Node.js ,它利用V8 提供的能力,拓展了JS 的能力。而在Node.js 中,我們可以使用JS 中原本不存在的path 模組,為了我們更熟悉的運用,讓我們一起來了解一下吧~
本文Node.js 版本為16.14.0,本文的源碼來自於此版本。希望大家閱讀這篇文章後,會對大家閱讀原始碼有所幫助。
Path 來處理檔案和目錄的路徑,這個模組中提供了一些便於開發者開發的工具函數,來協助我們進行複雜的路徑判斷,提高開發效率。例如:
在專案中設定別名,別名的設定方便我們對檔案更簡單的引用,避免深層逐級向上尋找。
reslove: { alias: { // __dirname 目前檔案所在的目錄路徑'src': path.resolve(__dirname, './src'), // process.cwd 目前工作目錄'@': 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 { // 繼續偵測資料夾deleDir(childpath) } }) // 刪除空資料夾fs.rmdirSync(src) } deleDir("../floor")
簡單的了解了一下path 的使用場景,接下來我們根據使用來研究一下它的執行機制,以及是怎麼實現的。
引入path 模組,呼叫path 的工具函數的時候,會進入原生模組的處理邏輯。
使用_load
函數根據你引入的模組名稱作為ID,判斷要載入的模組是原生JS 模組後,會透過loadNativeModule
函數,利用id 從_source
(保存原生JS模組的源碼字串轉成的ASCII 碼)中找到對應的資料載入原生JS 模組。
執行lib/path.js 文件,利用process 判斷作業系統,根據作業系統的不同,在其檔案處理上可能會存在操作字元的差異化處理,但方法大致一樣,處理完後傳回給呼叫方。
resolve 傳回目前路徑的絕對路徑
resolve 將多個參數,依序進行拼接,產生新的絕對路徑。
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 路徑( servernamesharename,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
方法只是將傳入的參數進行規範化拼接,對於產生一個新的路徑比較實用,可以依照使用者意願建立。不過每個方法都有優點,要依照自己的使用場景以及專案需求,去選擇合適的方法。