- 關于flag
- 基本分類
- w和a的區別
- 修飾操作
- 關于mode
- 關于encoding
- 關于fd
- 拷貝文件
- 原生拷貝API的缺陷
- buffer級的copy實現
- 關于writeFile和write的區別
- write簡寫
- 創建和刪除文件
- 創建和刪除文件夾
- 級聯創建文件夾
- 遞歸刪除文件夾
- 先序深度異步刪除
- 先序廣度異步刪除
- 關于fs.constants
- stats中的三個time
[TOC]
## 關于flag
通過設置讀寫文件API的`flag`屬性,我們能控制我們操作文件的方式以及一些操作細節。
### 基本分類
首先我們要知道一個文件的操作方式主要分為哪幾種:
- `r` : 讀文件
- `w` : 寫文件
- `a` : 追加
當我們調用`read`系的API時,默認的flag即為`r`,同樣,當我們調用`write`系的API時,默認的flag即為`w`。其中`w`和`a`的區別在于。
#### w和a的區別
w每次會清空文件,a則是向里添加。
文件不存在時w會創建文件,而a則會報錯。
### 修飾操作
So,這些`flag`最基礎的作用是標識我們這個操作是讀文件還是寫文件。但`flag`的作用不僅于此,我們還能在此基礎之上添加一些輔助標識來定制一些我們讀寫文件時的細節操作。
其中若添上`+`表示對基本的操作方式進行取反加強(如果是寫就還能讀,如果是讀就還能寫),如果是`x`則就是排他。
- r+ 讀取并寫入 文件不存在報錯
- rs 同步讀取文件并忽略緩存
- wx 排他寫入文件
- w+ 讀取并寫入文件,不存在則創建,存在則清空 和r+區別在于不會報錯會創建不創建的文件
- wx+ 和w+類似 其他方式打開
- ax 排他
- a+ 讀取并追加寫入,不存在則創建 和w+相比是不會覆蓋
- ax+ 作用于a+類似,但是以排他方式打開文件
容易記混淆的是創不創建報不報錯,我們要注意即使`r` 用了`+` 也并不能在文件不創在時創建一個文件,相比之下`a`則是可以的,但`a+`相較于`w+`它永遠保持它追加的本質,不會像w清空再寫入。
## 關于mode
寫入相關的API我們能設置`mode`,嗯。。。`w`可以創建文件,文件創建時需要設置一個權限。(So,如果是`r`,則不存在mode的配置需要)
權限是用八進制表示的,`0o`開頭后面再跟三個數(代表三種用戶的權限,我,所屬用戶組,游客),`0777`表示最大權限。
數字7代表一個用戶組被賦予了最大權限,可讀可寫可執行,讀寫執行的權重分別為421,加起來為7,故一個用戶組最大權限為7。
記憶口訣: 兒(2)媳(寫)一直(執)死(4)讀書
>[info] windows中文件不能直接執行,故我們常常用`0o666`而不是`0o777`
## 關于encoding
讀寫文件時都有encoding的配置項,唯一的區別是他們的默認配置。
寫文件時默認的編碼是`utf8`,**讀文件時默認沒有編碼,它讀取的是buffer**。
## 關于fd
fd是被打開文件的文件標識,我們稱之為它為`句柄`,句柄句柄,就是一個把手,我們只要握住它就能操控整個被標識的文件。
它是一個從3開始的數字,為什么是從3開始的呢?它本身是該從1開始的,但是1、2都被占用了,1代表標準輸出,2代表錯誤輸出。至于什么是標準輸出錯誤輸出,你可以想一下`console.log/info`和`console.error/warn`,他們是等價的。
另外還有一點灰常重要的點需要注意,linux中fd是有上限的,且一旦打開一個文件fd就會++,故我們需要記得手動關閉這些文件防止溢出。
## 拷貝文件
拷貝文件可能是最常用的文件操作方式了,它將數據從一個地點流向了另外一個地點。
### 原生拷貝API的缺陷
`Node`8.5以上的版本為我們提供了拷貝的API——copyFile,但Ta是基于`readFile`和`writeFile`的,這樣有個缺點,那就是readFile是將文件一次性讀到內存中后倉讓writeFIle直接寫入到文件中,嗯,假若文件過大,大過內存,那。。。就gg了。
### buffer級的copy實現
```
let fs = require('fs');
let path = require('path');
function copy(source,target,speed,cb){
if(typeof(speed)==='function'){
cb = speed;
speed = 10;
}
fs.open(path.join(__dirname,source),'r+',0o666,function(err,rfd){
if(err)console.error('打開文件失敗!');
fs.open(path.join(__dirname,target),'w+',0o666,function(err,wfd){
if(err)console.error('打開文件失敗!');
let buf = Buffer.alloc(speed)
,length = buf.length;
function next(){
fs.read(rfd,buf,0,length,null,(err,bytesRead)=>{
if(err)console.error('讀取文件失敗!');
if(!bytesRead){
fs.close(rfd,function(err){});
fs.fsync(wfd,function(err){
fs.close(wfd,function(err){});
});
return cb&&cb();
}
fs.write(wfd,buf,0,bytesRead,null,(err,bytesWritten)=>{
if(err)console.error('寫入文件失敗!');
next();
});
});
}
next();
});
});
}
copy('test.js','test2.js',()=>{console.log('拷貝完畢')});
```
### 關于writeFile和write的區別
除了他們一個是將整個文件一次性寫入,一個可以控制寫入的顆粒大小以外,
writeFile是直接寫入文件不需要經過緩存,而write是會先寫入緩存的,
So如果我們是使用write寫入文件,我們常常在關閉一個寫入的文件之前利用`fsync`API強制將緩存中的內容寫入到本地文件后再`close`關閉文件。
## write簡寫
write有一種不常見的簡寫方式
```
fs.open(path.join(__dirname,'1.txt'),'w+',function(err,fd){
// let buf = Buffer.from('你好呀');
// fs.write(fd,buf,0,buf.length,null,function(err,bytesWritten){
// if(err)return console.log('fail');
// console.log('ok');
// })
//以上可以這么簡寫
fs.write(fd,Buffer.from('你好呀'),function(err,bytesWritten){
if(err)return console.log('fail');
console.log('ok');
})
})
```
## 創建和刪除文件
node中沒有類似于`mkfile`、`touch filename`這樣的創建文件的API,
取而代之的是`write`系列API,上文中我們說過`write`系列的API的默認`flag`為`w`,這意味著如果一個文件不存在的話我們會先創建這個文件再寫入。
同樣的我們也能使用`open`來創建文件,只需要將`flag`設置為`w`即可
```
fs.open(path.join(__dirname,'1.txt'),'w+',function(err,fd){
console.log('創建成功');
})
```
---
刪除文件在node中很簡單
```
fs.unlink('xxx',function(err){})
```
## 創建和刪除文件夾
刪除文件夾
```
fs.rmdir('xxx',function(err){})
```
創建文件夾
```
fs.mkdir('xxx',function(err){})
```
API很簡單,但不論是刪除還是創建文件夾都有些需要注意的地方。
### 級聯創建文件夾
node中創建文件夾也逃不過不能級聯創建目錄的命運。
如果我們想創建一個目錄`a/b/c`,那么要先有文件夾`a`,在文件a下還得有文件夾b,才能在a中的b下創建文件夾c。
So,我們需要自己動手Lu一個
```
function mkdirP(dir,cb){
let paths = dir.split('/');
// [a,b,c,d]
//a a/b a/b/c
function next(index){
if(index>paths.length){
return cb&&cb(err);
}
let newPath = paths.slice(0,index).join('/');
fs.access(newPath,function(err){
if(err){ // 如果文件不存在就創建這個文件
fs.mkdir(newPath,function(err){
next(++index);
})
}else{
next(++index); //說明已有文件夾,跳過繼續創建下一個文件夾
}
});
}
next(1)
}
```
### 遞歸刪除文件夾
刪除文件夾需要注意一點,如果要刪除的文件夾里有東西,我們需要把里面的東西都刪完了才能刪掉這個文件夾,否則就會報錯。
So,我們需要對文件夾進行遞歸遍歷操作,
我們選擇采用先序深度和先序廣度兩種方式去實現遞歸刪除文件夾
#### 先序深度異步刪除
```
function rmdir(dir,cb){
fs.readdir(dir,function(err,files){
// 讀取到文件
function next(index){
if(index===files.length)return fs.rmdir(dir,cb); //=== 表示能遍歷的都遍歷完了,刪除該層目錄
let newPath = path.join(dir,files[index]);
fs.stat(newPath,function(err,stats){
if(stats.isDirectory()){ // 如果是文件夾
// 要讀的是b里的第一個 而不是去讀c
// 如果b里的內容沒有了 應該去遍歷c
rmdir(newPath,()=>next(index++));
}else{
//刪除文件后繼續遍歷即可
fs.unlink(newPath,next(index++));
}
});
}
next(0);
});
}
```
#### 先序廣度異步刪除
```
function rmdirp(dir,cb){
let dirs = [dir]
,index = 0;
function rmdir(){
let current = dirs[--index];
if(current){
fs.stat(current,(err,stat)=>{
if(stat.isDirectory()){
fs.rmdir(current,rmdir);
}else{
fs.unlink(current,rmdir);
}
});
}
}
!function next(){
if(index===dir.length)return rmdir(); //說明index停止增長,所有文件已遍歷完畢
let current = dirs[index++];
fs.stat(current,function(err,stat){
if(err)return cb(err);
if(stat.isDirectory()){
fs.readdir(current,function(err,files){
dirs = [...dirs,...files.map(item=>path.join(current,item))];
});
}else{
next(); //說明是文件,那在上一輪next中已經被添加進數組中,直接跳過
}
});
}();
}
```
## 關于fs.constants
- fs.constants.F_OK :path is visible to the calling process. This is useful for determining if a file exists, but says nothing about rwx permissions. Default if no mode is specified.
- fs.constants.R_OK :path can be read by the calling process.
- fs.constants.W_OK - path can be written by the calling process.
- fs.constants.X_OK - path can be executed by the calling process. This has no effect on Windows (will behave like fs.constants.F_OK).
主要是配合`fs.access`,作為其第二個參數,默認是`F_OK`。
## stats中的三個time
- atime 文件被訪問時(`read`)會觸發修改
- mtime 文件被更新時(`write`)會觸發修改
- ctime 相當于atime+mtime+chmod+rename...
## 大多數人都死在了規范上
不得不抱怨一句,大多數猿們都累死在了適應各種規范、API、框架上。