我在 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
來實現,根據這個引數的值來決定製表符
的數量,從而控制縮排來表現層級
function formatLink(obj = structs, basePath = ``, level = 1) {
// ...
}
複製程式碼
另外,給大家說個小 tip
,github
頁面上是可以使用快捷鍵的,例如在一個 github
倉庫頁面上按下 t
鍵,就會啟用查詢檔案模式,不同的頁面,例如賬戶個人主頁和某個倉庫頁面可用的快捷鍵可能所有差別,而且這些快捷鍵很多,想要憑記憶記下來可能有些困難,可以按下 shift + /
鍵,即可在當前 github
頁面彈出一個 modal
彈窗,上面就顯示當前頁面所有可用的快捷鍵
總結
本文示例程式碼已經放到 github上了,嗯,這個 github倉庫下 README.md上展示的目錄層級結構,就是根據這份檔案生成的