我在 github上有個維護時間比較長的 repository,開始時只有幾個檔案,後來檔案數目逐漸增多,期間整理了好幾次,現在已經整理成了好幾個資料夾了,有時候想找某個檔案的時候,但是不確定到底在哪個資料夾裡面,於是就憑感覺一個一個資料夾試過去,層級少點還好,但是層級一多,就算是明確知道在哪個資料夾裡,一層層點進去也要點好幾次
於是心中一動,就想著把當前倉庫的目錄結構列出來,直接寫在 README.md
檔案上,想看哪個檔案直接點,一次點選即可,手寫目錄肯定是不太友好的,因為我可能頻繁增刪檔案,甚至是再次整理檔案結構,而且也不具備通用性,萬一哪天又想把另外一個倉庫也列出目錄結構,那麼又要手寫一遍,所以最好寫個程式碼程式來幫我完成這種工作
先看效果圖:
遞迴獲取所有檔案路徑
目標是輸出目錄的層級結構,那麼首先要把當前倉庫根目錄下所有檔案的路徑獲取到,思路很清晰的,先用 fs.readdirSync
讀取目錄,並且遞迴迴圈子目錄,直到最後一層
function getDirStruct(basePath = __dirname) {
const files = fs.readdirSync(basePath)
files.forEach(file => {
// 處理先不要顯示的檔案
if (excludeFile.indexOf(file) !== -1 || excludePrefix.some(pre => file.indexOf(pre) === 0)) return
const fullPath = path.resolve(basePath, file)
const fileStats = fs.statSync(fullPath)
// 如果是資料夾,則繼續遍歷其子檔案
return fileStats.isDirectory(file) ? getDirStruct(fullPath) : absolutePath.push(fullPath)
})
}
複製程式碼
這裡獲取到的是所有檔案在本地目錄的絕對全路徑,但是後面是需要把這個東西上傳到 github
的,所以需要把這個絕對路徑改為相對路徑,用於拼接檔案的 url
地址
// 絕對路徑轉相對路徑
const rPath = path.relative(__dirname, apath)
複製程式碼
這裡有幾個小點需要注意下
- 排除無用的干擾檔案
程式不可能直接執行在 github
頁面上的,所以你需要把倉庫下載下來,再本地目錄中執行程式,那麼因為使用了 git
的緣故,所以根目錄中肯定存在一個 .git
資料夾,這個資料夾裡的東西很多,而且你也不太可能希望展示這個東西,所以最好排除掉
類似的還有一些 img
資料夾,裡面存了很多圖片,你可能也不想展示出來,因為太佔篇幅了而且也沒什麼用,所以也要排除掉
- 不同平臺的路徑分隔符
不同平臺上的檔案路徑分隔符是不一樣的,在 windows
上 路徑分隔符是 \
,而在 POSIX
(即類 UNIX
系統,包括 Mac
、Linux
)上則是 /
,所以需要區別處理
nodejs
中可通過 path.sep
來獲取當前作業系統的檔案路徑分隔符
- 遞迴
檔案的逐級讀取涉及到遞迴操作,如果目錄層級不是深到令人智熄的地步,那麼除了程式執行時間比較長以外,不會有什麼問題,但是如果是讀取類似於 node_modules
這種資料夾,而且巢狀很深,那麼就可能導致 棧溢位,程式直接 boom
那麼這裡就不得不提到 尾遞迴
了,尾遞迴
就能很好地避免 棧溢位
問題,關於 尾遞迴
,參見 知乎:什麼是尾遞迴?
目錄樹(層級結構)
獲取到了所有檔案路徑之後,需要對這些檔案路徑按照進行整理,得到一棵 Dir Tree
,就是一個用於描述這些檔案路徑的層級結構的資料
這裡構建的 Dir Tree
類似下述結構:
{
_children: ['README.md', 'LICENSE'],
'CSS': {
_children: ['CSS-Note-1.md', 'CSS-Note-2.md', '效能優化.md'],
},
'Vue': {
_children: ['效能優化.md', '新特性.md'],
'無渲染Vue元件': {
...
}
}
}
複製程式碼
每個目錄層級下,對於單檔案,直接存入這個層級下的一個名為 _children
的陣列屬性中,對於資料夾,則將資料夾的名字作為這個層級下一個屬性名,然後這個屬性的值,再按照上述規則進行遞迴,直到最後一層
當然,這個 Dir Tree
的結構仁者見仁智者見智,只要你覺得順眼怎麼樣都可以,這裡只是舉了一個栗子◎
資料結構確定了,下面就需要將檔案路徑陣列整理成上面的結構,例如,對於 /project/demo/src/index.js
這個路徑,需要整理成:
const tree = {
project: {
demo: {
src: {
_children: ['index.js']
}
}
}
}
複製程式碼
那麼這裡就有個問題了,在建立這個 tree
物件之前,tree
這個資料可能是沒有 project
這個屬性的,又或者有 project
屬性,但是這個屬性下沒有 demo
這個屬性
解決的方法,很明顯的一個就是逐級判斷,沒有這個屬性的,就加上去,然後當構造出 tree.project.demo.src
結構的時候,再在這個結構上,加上 ._children = ['index.js']
結構,這個過程其實可以簡化一下,比如藉助 超程式設計
例如,你要是覺得不要每次都要判斷到底有沒有這個屬性,那麼就可以使用 es6 Proxy
,實現自動新增屬性的能力:
function autoAddProperty() {
return new Proxy({}, {
get(target, key, receiver) {
if (!(key in target)) {
target[key] = autoAddProperty()
}
return Reflect.get(target, key, receiver)
}
})
}
// 用法
const obj = autoAddProperty()
obj.a.b.c.d = 1
console.log(obj.a.b.c.d) // => 1
複製程式碼
當然,除了 proxy
之外,也可以藉助 eval
,本示例使用的就是 eval
,因為程式碼更簡潔,eval
這個方法可能很多 JavaScript
書籍上都會提到不要隨便用,對於新手來說,這個特性坑比較多,所以在不明確其副作用的情況下,還是最好不要用,但並不是說不能用,如果用這個特性多寫一行程式碼就能另外少寫十行,那為什麼不用一用?
更多關於超程式設計的內容,可見 知乎: 怎麼理解超程式設計?、 【資源集合】 ES6 超程式設計(Proxy & Reflect & Symbol)
輸出結構
資料結構搞定了,那麼最後的輸出就很簡單了,就是按照層級進行解構,同樣是需要用到尾遞迴,值得稍微提一下的就是,想讓輸出的目錄結構呈現出一種次序關係,那麼就需要在遞迴中記住層級關係,可通過指定一個引數 level
來實現,根據這個引數的值來決定製表符 \t
的數量,從而控制縮排來表現層級
function formatLink(obj = structs, basePath = '', level = 1) {
// ...
}
複製程式碼
另外,給大家說個小 tip
,github
頁面上是可以使用快捷鍵的,例如在一個 github
倉庫頁面上按下 t
鍵,就會啟用查詢檔案模式,不同的頁面,例如賬戶個人主頁和某個倉庫頁面可用的快捷鍵可能所有差別,而且這些快捷鍵很多,想要憑記憶記下來可能有些困難,可以按下 shift + /
鍵,即可在當前 github
頁面彈出一個 modal
彈窗,上面就顯示當前頁面所有可用的快捷鍵
總結
實際上我做完這件事情後,最大的收穫並不是寫出了一個小程式,解決了我想使用程式碼解放雙手自動生成檔案目錄樹的問題,而是在解決這個問題的過程中,延伸開來學到的其他的東西,例如尾遞迴、超程式設計乃至是 github
的快捷鍵,這才是價值更大的收貨。
這些東西都是我以前不知道的東西,並且因為這些知識可能對於小白不太友好所以很難在其他的技術文章中看到,所以如果我不主動探尋,可能要再過很長一段時間甚至是永遠都不知道。
不懂不可怕,因為只要你想懂你總會懂的,但不知道就很可怕了,不知道就是不知道,哪怕那東西再簡單,但就因為你不知道所以你就是不知道,這就無解了
本文示例程式碼已經放到 github上了,嗯,這個 github倉庫下 README.md上展示的目錄層級結構,就是根據這份檔案生成的