Node.js<四>——常見的內建模組解析
內建模組path
路徑的演練
- path模組用於對路徑和檔案進行處理,提供了很多好用的方法
-
並且我們知道在
Mac OS
、Linux
和windows
上的路徑分隔符是不一樣的- 在Mac OS、Linux的Unix作業系統上使用
/
來作為檔案路徑的分隔符 - windows上會使用
`或者
`來作為檔案路徑的分隔符,當然目前也支援/
- 在Mac OS、Linux的Unix作業系統上使用
-
那麼如果我們在windows上使用
\
來作為分隔符開發了一個應用程式,要部署到Linux上面應該怎麼辦呢?- 顯示路徑會出現一些問題
- 所以為了遮蔽它們之間的差異,在開發中對於路徑的操作我們可以使用path模組
如果我們盲目的將兩個路徑進行拼接,比如path1 + '/' + path2
,可能在當前所在的作業系統中是可以跑起來的,但如果我們換了一個作業系統執行程式碼,可能就識別不到該路徑了,因為不同的作業系統要求的路徑分割符是不一樣的
js
const path = require('path')
// 我們path1和path2兩部分路徑故意寫錯誤的分隔符出來
const path1 = 'User\ASUS\DCC'
const path2 = 'MyWork\5月4日'
// path物件中的resolve方法可以實現兩個路徑之間根據當前作業系統選取正確的路徑分隔符進行拼接
const fileName = path.resolve(path1, path2)
// 可以看到我們原字串中原本是用\拼接的,通過path.resolve方法之後都換成了使用\進行拼接
console.log(fileName); // C:\Users\ASUS\Desktop\前端學習\演算法\User\ASUS\DCC\MyWork\5月4日
path模組的其他方法
1. 獲取路徑的資訊
path.dirname()
方法返回一個 path 的目錄名path.basename()
方法返回一個 path 的最後一部分,一般來說是檔名path.extname()
方法返回 path 的副檔名
js
const path = require('path')
const file = '/foo/bar/baz/asdf/quux.html'
const path1 = path.dirname(file)
const path2 = path.basename(file)
const path3 = path.extname(file)
console.log(path1); // /foo/bar/baz/asdf
console.log(path2); // quux.html
console.log(path3); // .html
2. join路徑拼接
path.join()
方法使用平臺特定的分隔符把全部給定的 path 片段連線到一起,並規範化生成的路徑。簡單來說就是根據當前的作業系統選取合適的路徑分隔符將多個路徑拼接在一起,當然其也會糾正原先路徑中不正確的分隔符
js
const path = require('path')
const path1 = '/USER/ASUS'
const path2 = 'My/DCC'
const fileName = path.join(path1, path2)
console.log(fileName); // \USER\ASUS\My\DCC
3. resolve方法拼接(用的最多)
path.resolve()
方法會把一個路徑或路徑片段的序列解析為一個絕對路徑
js
const path = require('path')
const path1 = 'asd/sad'
const path2 = 'My/DCC'
const fileName1 = path.join(path1, path2)
const fileName2 = path.resolve(path1, path2)
console.log(fileName1); // asd\sad\My\DCC
console.log(fileName2); // C:\Users\ASUS\Desktop\前端學習\演算法\asd\sad\My\DCC
4. resolve和join方法的區別
- 如果處理完全部給定的 path 片段後還未生成一個絕對路徑,則當前工作目錄會被用上,且其能識別
../
、./
和/
js
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// 如果當前工作目錄為 /home/myself/node,
// 則返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'
- 給定的路徑的序列是從右往左被處理的,後面每個 path 被依次解析,直到構造完成一個絕對路徑
js
const path = require('path')
const path1 = 'asd/sad'
const path2 = '/My/DCC'
const fileName1 = path.join(path1, path2)
const fileName2 = path.resolve(path1, path2)
console.log(fileName1); // asd\sad\My\DCC
console.log(fileName2); // \My\DCC
- 生成的路徑是規範化後的,且末尾的斜槓會被刪除,除非路徑被解析為根目錄
js
const path = require('path')
const path1 = '/USER/ASUS'
const path2 = 'My/DCC/'
path.join(path1, path2) // \USER\ASUS\My\DCC\
path.resolve(path1, path2) // \USER\ASUS\My\DCC
path.resolve方法在webpack中也有大量使用
比如我們在react
專案使用craco
來配置路徑別名的時候
使用ES Module也是可以引入node中的核心模組的
因為上面也有說過,ES Module
匯入CommonJS
的模組是被允許的,所以自然也可以匯入嵌入在node中的核心模組
js
// test.mjs
import path from 'path'
console.log(path); // 是正常的path物件
內建模組fs
- fs是
File System
的縮寫,表示檔案系統 -
對於任何一個為伺服器端服務的語言或者框架通常都會有自己的檔案系統
- 因為伺服器需要將各種資料、檔案等放置到不同的地方
- 比如使用者資料可能大多數是放到資料庫中的
- 比如某些配置檔案或者使用者資源(圖片、音影片)都是以檔案的形式存在於作業系統上的
-
Node也有自己的檔案系統操作模組,就是
fs
- 藉助於Node幫助我們封裝的檔案系統,我們可以在任何的作業系統(
windows、Mac OS、Linux
)上面直接去操作檔案 - 這也是Node可以開發伺服器的一大原因,也是它可以成為前端自動化指令碼等熱門工具的原因
- 藉助於Node幫助我們封裝的檔案系統,我們可以在任何的作業系統(
fs的API介紹
大多數API都提供了三種操作方式:
- 方法一:同步操作檔案——程式碼會被阻塞,不會繼續執行
js
const fs = require('fs')
const filename = './test.html'
const file = fs.statSync(filename)
console.log(file);
statSync
方法可以同步讀取我們的檔案資訊, 返回一個 fs.Stats 例項
- 方法二:非同步回撥函式操作檔案——程式碼不會被阻塞,需要傳入回撥函式,當獲取到結果時,回撥函式被執行
fs.stat
使用方法:fs.stat(path, callback)
,回撥有兩個引數 (err
, stats
) ,其是非同步讀取檔案的,在發生錯誤或者讀取完成之後都會去執行我們的回撥函式
js
const fs = require('fs')
const filename = './test.html'
// 回撥函式的兩個引數,一個是錯誤資訊,另一個是檔案資訊
const file = fs.stat(filename, (err, info) => {
console.log(info); // Stats類
})
console.log(file); // undefined,因為是非同步的,所以並不會阻塞程式碼執行,列印的時候還沒有獲取到檔案資訊
-
方法三:非同步
Promise
操作檔案——程式碼不會被阻塞,通過fs.promises
呼叫方法操作,會返回一個Promise,可以通過then
、catch
進行處理- 很多的api都提供了promise方式,但並不是所有的,所以大家在使用某些東西的時候可以先去查閱文件
js
const fs = require('fs')
const filename = './test.html'
// fs.promises是fs模組中的一個物件,它裡面的很多方法都是基於promise的
const file = fs.promises.stat(filename).then(res => {
console.log(res); // Status類
}).catch(err => {
console.log(err);
})
console.log(file); // Promise { <pending> }
檔案描述符
檔案描述符(File desciptors
)是什麼呢?
- 在
POSIX
系統上,對於每個程序,核心都維護著一張當前開啟著的檔案和資源的表格 - 每個開啟的檔案都分配了一個稱為檔案描述符的簡單的數字識別符號
- 在系統層,所有檔案系統操作都是用這些文字描述符來標識和跟蹤每個特定的檔案
Windows
系統使用了一個雖然不同但概念上類似的機制來跟蹤資源
為了簡化使用者的工作,Node.js抽象出操作系統之間的特定差異,併為所有開啟的檔案分配一個數字型的檔案描述符。也就是說node中的api很多把文字描述符的東西遮蔽掉了,相當於內部幫你做了這些操作
fs.open()
方法用於分配新的檔案描述符
- 一旦被分配,則檔案描述符可用於從檔案讀取資料、向檔案寫入資料、或請求關於檔案的資訊
```js const fs = require('fs') const filename = './test.html' fs.open(filename, (err, fd) => { console.log(fd); // 4
// 通過描述符去讀取對應的檔案資訊 fs.fstat(fd, (err, info) => { console.log(info); // Stats物件 }) }) ```
檔案的讀寫
如果我們下網對檔案的內容進行操作,這個時候可以使用檔案的讀寫
fs.readFile(path, options, callback)
:讀取檔案的內容fs.wraiteFile(file, data, options, callback)
:在檔案中寫入內容
1. 檔案寫入
js
const fs = require('fs')
fs.writeFile('./a.txt', '你好啊!', err => {
console.log(err);
})
我們原本是沒有這個檔案的,但由於options
引數中的flag
屬性預設是w
,所以會幫我們自動建立一個檔案並將對應的值寫入
在上面的程式碼中,你會發現有一個大括號沒有填寫任何的內容,這個就是寫入時填寫的options引數
-
flag
:寫入的方式,預設是 'w'- 'r' - 以讀取模式開啟檔案。如果檔案不存在則發生異常
- 'r+' - 以讀寫模式開啟檔案。如果檔案不存在則發生異常
- 'rs+' - 以同步讀寫模式開啟檔案。命令作業系統繞過本地檔案系統快取
- 'w' - 以寫入模式開啟檔案。檔案會被建立(如果檔案不存在)或截斷(如果檔案存在)
- 'wx' - 類似 'w',但如果 path 存在,則失敗
- 'w+' - 以讀寫模式開啟檔案。檔案會被建立(如果檔案不存在)或截斷(如果檔案存在)。
- 'wx+' - 類似 'w+',但如果 path 存在,則失敗
- 'a' - 以追加模式開啟檔案。如果檔案不存在,則會被建立
- 'ax' - 類似於 'a',但如果 path 存在,則失敗
- 'a+' - 以讀取和追加模式開啟檔案。如果檔案不存在,則會被建立
- 'ax+' - 類似於 'a+',但如果 path 存在,則失敗
js
const fs = require('fs')
fs.writeFile('./a.txt', '你好啊!', { flag: 'a+' }, err => {
console.log(err);
})
我們將flag改為a+之後,做的就是檔案的追加操作了,發現我們要寫入的文字出現在了目標檔案的末尾
encoding
:字元的編碼,預設是'utf8'
2. 檔案讀取
在檔案讀取時,如果不填寫encoding
,則返回的結果是Buffer
,類似是一串二進位制編碼
因為在fs.readFile
方法中,encoding
屬性的預設值為null
,也就是說他是沒有預設值的,需要我們手動指定才行,其flag的預設值是'r'
```js
const fs = require('fs')
// 沒有指定encoding,他就不知道以哪種字元編碼格式去讀取檔案
fs.readFile('./a.txt', (err, data) => {
console.log(data); //
fs.readFile('./a.txt', {encoding: 'utf-8'}, (err, data) => { console.log(data); // 你好啊!你好啊!你好啊! }) ```
資料夾操作
1. 新建一個資料夾 — fs.mkdir(path[, mode], callback)
```js const fs = require('fs') const path = require('path') // 絕對路徑 const targetDir = path.resolve(__dirname, 'dcc') try { fs.mkdirSync(targetDir) } catch (err) { console.log(err); }
// 相對路徑 const dirname = './dcc' // fs模組有個existsSync方法可以判斷當前引數所對應的路徑存不存在 if (!fs.existsSync(dirname)) { fs.mkdir(dirname, err => { console.log(err); }) } ```
發現對應的資料夾已經建立好了,如果我們再執行一遍這個程式發現會報錯,說明了相同的資料夾是不可以重複建立的,同時也說明了當我們使用fs.mkdir
方法建立檔案時,可以傳入絕對路徑,也能傳入相對路徑
2. 獲取資料夾的所有檔案 — fs.readdir(path[, options], callback)
const fs = require('fs')
fs.readdir('./dcc', (err, files) => {
// 其讀取到的是一個檔案陣列,包括裡面的目錄也能讀取到
console.log(files); // [ 'a.html', 'b.txt', 'c.md', dir ]
})
思考:如果我們現在想把該資料夾裡面的所有檔案讀取出來,比如說資料夾中其它資料夾的檔案,應該要怎麼實現呢?
其實我們可以通過傳入引數的形式讓readdir
方法讀取目錄下面檔案的時候,把它對應的檔案型別也傳遞出來,也就是將options
所對應的withFileTypes
屬性更改為true
那麼每一個檔案資訊都對應一個Dirent
物件,且每個物件中都有一個isDirectory
方法(在原型上)用來判斷當前檔案是不是資料夾,如下圖所示:
既然資料夾裡面還有可能會套資料夾,所以想要讀取出所有檔案的路徑就必須要用遞迴的方法來實現了
js
const fs = require('fs')
const path = require('path')
const getFileName = (dirname) => {
// 根據目錄路徑讀取該路徑下的所有檔名稱,
// withFileTypes屬性改為true是為了在讀取檔名稱的時候順便將其檔案型別暴露出來
// 檔案型別並不是Dirent物件下的屬性,而是要我們通過isDirectory方法去獲得
fs.readdir(dirname, { withFileTypes: true }, (err, files) => {
files.forEach(file => {
// 每個Dirent物件上都有一個isDirectory方法用於判斷該檔案是否為資料夾
if (file.isDirectory()) {
// 如果當前檔案仍然是個資料夾,則需要通過resolve方法找到它的路徑,然後遞迴呼叫函式
const filePath = path.resolve(dirname, file.name)
getFileName(filePath)
// 不是資料夾就直接將名稱打印出來即可
} else {
console.log(file.name);
}
})
})
}
getFileName('./dcc')
從列印結果可以得知,確實已經遞迴實現了列印一個目錄下面的所有檔名稱
3. 資料夾重新命名
重新命名可能操作可能需要以管理員身份執行編輯器才被允許
js
const fs = require('fs')
fs.rename('./dcc', './kobe', err => {
console.log(err);
})
資料夾的複製案例
場景:一個資料夾中有很多個資料夾,每一個資料夾中又有很多的檔案,現要求將這些檔案按照原先所在的檔案目錄格式選取出指定字尾名的檔案拷貝到另一個資料夾中
js
const fs = require('fs')
const path = require('path')
// 獲取起始檔案路徑和目標檔案路徑
const startPath = process.argv[2]
const endPath = process.argv[3]
const ext = process.argv[4] || '.js'
// 得到起始路徑下的檔案
const allDirs = fs.readdirSync(startPath)
// 遍歷其實資料夾並取出他的子資料夾
for (const name of allDirs) {
// 原來資料夾的路徑
const originDir = path.resolve(startPath, name)
// 獲得到拷貝過去後的資料夾路徑
const targetDirname = path.resolve(endPath, name)
// 通過判斷這個路徑存不存在,來決定要不要新的建立資料夾,如果存在則說明該檔案已經被建立過了,直接跳過去建立下一個資料夾即可
if (fs.existsSync(targetDirname)) continue;
// 在目標路徑下建立一個新的資料夾
fs.mkdirSync(targetDirname)
// 讀取與之對應的資料夾內的檔案
const currDirFiles = fs.readdirSync(originDir)
// 遍歷該資料夾,得到所有檔案
for (const name of currDirFiles) {
// 判斷當前檔案的字尾名是不是要拷貝過去的檔案
if (path.extname(name) === ext) {
// 拼接得到原先檔案的路徑和拷貝過去的檔案路徑
const originCopyName = path.resolve(originDir, name)
const targetCopyName = path.resolve(targetDirname, name)
// 利用copyFileSync方法進行檔案拷貝,其接收兩個引數,一個是要被拷貝的原始檔名稱,另一個是拷貝操作的目標檔名
fs.copyFileSync(originCopyName, targetCopyName)
console.log('拷貝成功!');
}
}
}
當我們執行 node test.js ./dir1 ./dir2 .txt
命令之後,發現以txt
為字尾名的檔案都被拷貝過去了,說明我們的程式沒有問題
Events模組
events基礎方法
Node中的核心API都是基於非同步事件驅動的
- 在這個體系中,你某些物件(發射器(
Emitters
))發出某一個事件 - 我們可以監聽這個事件(監聽器(
Listeners
)),並且傳入的回撥函式會在事件被觸發時呼叫
發出事件和監聽事件都是通過EventEmitter類來完成的,他們都屬於events物件
emitter.on(eventName, listener)
:監聽事件,也可以使用addListener
emitter.off(eventName, listener)
:移除監聽事件,也可以使用removeListener
emitter.emit(eventName[, ...args])
:發出事件,可以攜帶一些引數
我們從events模組中匯入的內容和其它模組有所不同,因為其是一個類。我們可以根據這個類創建出一個“發射器”
js
const EmitterEmitter = require('events')
console.log(EmitterEmitter);
const emitter = new EmitterEmitter()
通過發射器,我們可以監聽、取消、發射相應的事件
- 可以同時監聽多個相同的事件,繫結的函式都會被執行
- 發射事件的時候可以攜帶多個引數,在監聽的回撥函式中,我們可以用...剩餘運算子來將他們集中到一個變數中
- 如果繫結的都是相同的事件,那麼觸發的時候按照監聽的順序來執行,下面程式碼就是先執行“我被點選了1”,然後再執行“我被點選了2”
```js // addEventListener 是 on 的簡寫 emitter.on('click', (...args) => { console.log('我被點選了1', args); // 2s之後輸出:我被點選了1 [ 'kobe', 'james' ] })
// 如果繫結的都是相同的事件,那麼觸發的時候按照監聽的順序來執行 emitter.on('click', (...args) => { console.log('我被點選了2', args); // // 2s之後輸出:我被點選了2 [ 'kobe', 'james' ] })
setTimeout(() => { emitter.emit('click', 'kobe', 'james') // 發射事件,可以攜帶引數 }, 2000) ```
如果我們把setTimeout
裡面的函式改寫一下,然後再將第二個註冊事件的回撥函式抽離出去,列印結果會發生什麼變化呢?
```js // addEventListener 是 on的縮寫 emitter.on('click', (...args) => { console.log('我被點選了1', args); })
const clickFn = (...args) => { console.log('我被點選了2', args); }
emitter.on('click', clickFn)
setTimeout(() => { emitter.emit('click', 'kobe', 'james') emitter.off('click', clickFn) emitter.emit('click', 'kobe', 'james') }, 2000) ```
第一次發射事件的時候,兩個註冊的事件都會被觸發,但是當我們使用emitter.off
取消了第二個註冊事件的後,下次發射相同事件時,第二個事件就不會再被觸發了
events獲取資訊
emitter.eventNames(eventName)
返回一個列出觸發器已註冊監聽器的事件的陣列emitter.listenerCount(eventName)
返回正在監聽名為eventName
的事件的監聽器的數量emitter.listeners(eventName)
返回名為eventName
的事件的監聽器陣列的副本
``` const EmitterEmitter = require('events') const emitter = new EmitterEmitter() emitter.on('click', (...args) => { console.log('我被點選了1', args); }) const clickFn = (...args) => { console.log('我被點選了2', args); } emitter.on('click', clickFn) emitter.on('tab', clickFn)
console.log(emitter.eventNames()); // [ 'click', 'tab' ] console.log(emitter.listenerCount('click')); // 2 console.log(emitter.listeners('click')); // [ [Function (anonymous)], [Function: clickFn] ] ```
events中不常用方法
emitter.once
繫結的事件只監聽一次。新增一個單次listener
函式到名為eventName
的事件。 下次觸發 eventName 事件時,監聽器會被移除,然後再呼叫
js
const EmitterEmitter = require('events')
const emitter = new EmitterEmitter()
emitter.once('click', (...args) => {
console.log('我被點選了1', args);
})
setTimeout(() => {
// 這一次事件的發射會執行emitter.once繫結的函式
emitter.emit('click', 'kobe', 'james')
// 第二次事件的發射不會執行emitter.once繫結的函式
emitter.emit('click', 'kobe', 'james')
}, 500)
emitter.prependListener
將監聽事件新增到最前面,但是新增listener
函式到名為eventName
的事件的監聽器陣列的開頭。 不會檢查listener
是否已被新增。多次呼叫並傳入相同的eventName
和listener
會導致listener
被新增與呼叫多次
js
const EmitterEmitter = require('events')
const emitter = new EmitterEmitter()
emitter.on('click', (...args) => {
console.log('我被點選了1', args);
})
// prependListener方法繫結的函式執行會比用on繫結的回撥函式早
emitter.prependListener('click', (...args) => {
console.log('我被點選了2', args);
})
setTimeout(() => {
emitter.emit('click', 'kobe', 'james')
}, 500)
mitter.prependOnceListener
:將監聽事件新增到最前面,但是隻監聽一次emitter.removeAllListeners([eventName])
移除全部或指定eventName
的監聽器;注意,在程式碼中移除其他地方新增的監聽器是一個不好的做法,尤其是當EventEmitter
例項是其他元件或模組(如socket
或檔案流)建立的。