1. 程式人生 > >Node.js躬行記(2)——檔案系統和網路

Node.js躬行記(2)——檔案系統和網路

一、檔案系統

  fs模組可與檔案系統進行互動,封裝了常規的POSIX函式。POSIX(Portable Operating System Interface,可移植作業系統介面)是UNIX系統的一個設計標準。fs模組提供了多個操作目錄和檔案的方法,大多會有同步和非同步兩種版本,同步的方法名稱會以Sync為字尾。

1)目錄處理

  fs模組的readdir()方法可非同步的讀取目錄內容,回撥函式包含兩個引數,第一個是錯誤物件,第二個是一個包含檔名稱的陣列,對應的同步方法是readdirSync()。在下面的示例中,當前目錄包含兩個檔案,上一級目錄包含一個目錄名稱。

const fs = require('fs');
fs.readdir('./', (err, files) => {
  console.log(files);        //[ 'demo.txt', 'index.js' ]
});
fs.readdir('../', (err, files) => {
  console.log(files);        //[ '1' ]
});

  其它處理目錄的方法還有opendir()、mkdir()等。

2)讀寫檔案

  在fs模組中,可使用批量方法readFile()將檔案內容一次性的載入到記憶體中,如下所示。

const fs = require('fs');
fs.readFile('./origin.txt', (err, buf) => {
  console.log(buf.toString());        //"hello Node.js"
});

  對應的寫入方法是writeFile(),如下所示,檔案路徑、寫入內容和回撥函式是必傳的引數。如果檔案不存在,那麼會自動建立。

fs.writeFile('./target.txt', 'hello Node.js', (err) => {
  if (err) throw err;
  console.log('檔案已被儲存');
});

  當檔案很大時,像上面這樣直接讀取會有問題,可以改用流式方法createReadStream(),分批次的讀取檔案,如下所示,每次只讀7個位元組的內容。

const readable = fs.createReadStream('./origin.txt', {highWaterMark: 7});
readable.on("data", (chunk) => {
  /*************/
  /* "hello N"
  /* "ode.js"
  /************/
  console.log(chunk.toString());
});

  通過管道方法pipe()將origin.txt中的內容寫入到target-stream.txt中,如下所示,對於不存在的檔案,也會自動建立。

const writable = fs.createWriteStream('./target-stream.txt');
readable.pipe(writable);

3)檔案描述

  fs模組的stat()方法可讀取檔案的描述資訊,如下所示。

fs.stat('./demo.txt', (err, stats) => {
  console.log(stats);
});

  回撥函式中的stats引數是一個fs.Stats物件,其屬性如下所示。它還包含一些判斷方法,例如isDirectory()、isFile()等。

Stats {
  dev: 195650,
  mode: 33206,
  nlink: 1,
  uid: 0,
  gid: 0,
  rdev: 0,
  blksize: undefined,
  ino: 36873221949682120,
  size: 13,
  blocks: undefined,
  atimeMs: 1586227933993.0217,
  mtimeMs: 1585882949091.0166,
  ctimeMs: 1586227933995.0222,
  birthtimeMs: 1586227933993.0217,
  atime: 2020-04-07T02:52:13.993Z,
  mtime: 2020-04-03T03:02:29.091Z,
  ctime: 2020-04-07T02:52:13.995Z,
  birthtime: 2020-04-07T02:52:13.993Z
}

  fs模組還提供了fstat()方法,在功能上與stat()等價,只是fstat()方法的第一個引數是檔案描述符。在POSIX系統中,檔案描述符是一個正整數,它實際上是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。當開啟或建立一個檔案時,就會被分配一個檔案描述符。

  在下面的示例中,open()方法的回撥函式中包含一個fd引數(即檔案描述符),搭配fstat()方法就能讀取檔案資訊。

fs.open('./demo.txt', 'r', (err, fd) => {
  fs.fstat(fd, (err, stats) => {
    console.log(stats);
  });
});

4)監控檔案

  fs模組提供了兩種方法來監控檔案:watch()和watchFile(),前者能監控檔案或目錄的更改,後者只能監控檔案的更改。

  watch()方法的監聽器回撥包含兩個引數,第一個是事件型別(包括rename和change),第二個是觸發事件的檔名稱,如下所示。

fs.watch('./demo.txt', (eventType, filename) => {
  console.log(eventType, filename);
});

  執行node命令後,每次更改demo.txt檔案,就會在控制檯打印出下面這條語句。

$ node index.js
change demo.txt

  雖然watch()方法的效能優於watchFile()方法,但是watch()不是一個跨平臺的方法,其表現在各個平臺中並非百分百一致(例如filename引數不能保證提供),而watchFile()是跨平臺的。

5)非同步檔案

  fs.promises是Node提供的一組備用的非同步檔案系統方法,它們會返回Promise物件而不是通過回撥來處理結果。例如以Promise的方式使用stat()方法,如下所示。

const fsPromises = require('fs').promises;
fsPromises.stat('./demo.txt').then(stats => {
  console.log(stats);
});

二、網路

  Node可以處理的網路協議包括HTTP、TCP、HTTPS等,本節將會重點分析HTTP協議。Node中的http模組基於net、stream、events等模組,提供了一系列的網路傳輸的底層API,可建立HTTP伺服器和客戶端,包含重定向、代理、上傳資料等功能。

1)HTTP伺服器

  通過http.createServer()方法就能建立HTTP伺服器,其內部會呼叫net.Server()方法,返回一個http.Server例項。當服務被建立後,就能通過server.listen()方法監聽一個埠。

  在下面的示例中,監聽的埠是8000,createServer()的回撥函式包含兩個引數:req和res,分別表示HTTP的請求和響應,前者是IncomingMessage類(一個可讀流)的例項,後者是ServerResponse類(一個可寫流)的例項。__dirname是一個全域性變數,儲存著當前檔案所處的路徑。

const http = require('http'); 
const fs = require('fs'); 
const server = http.createServer(function (req, res) {
  fs.readFile(__dirname + "/index.html", function (err, data) {
    if (err) {
      res.statusCode = 500;
      res.end(String(err));
    } else {
      res.end(data);
    }
  });
}).listen(8000);

  在回撥函式中,可修改或解析響應報文,例如像上面當發生錯誤時,將狀態碼改成500。在瀏覽器中訪問http://localhost:8000,就能通過end()方法在頁面中輸出index.html的檔案內容。

  注意,必須呼叫響應物件的end()方法結束此次響應,如果省略,那麼頁面將會一直處於載入中,阻塞內容渲染。

  http.Server包含一組事件,例如connection和request,如下所示。前者會在建立新的TCP流時觸發,後者會在每次請求伺服器時觸發。

server.on("connection", function (socket) {
  console.log(connection");
});
server.on("request", function (req, res) {
  console.log('request');
});

2)重定向

  3XX格式的狀態碼用於重定向,例如在Node中實現302跳轉,如下所示。

http.createServer(function (req, res) {
  if(req.url == '/strick') {
    res.writeHead(302, {'Location': 'http://www.pwstrick.com'});
  }
  res.end();
}).listen(8000);

  當瀏覽器訪問http://localhost:8000/strick時,才會執行重定向。writeHead()方法可一次性設定響應的狀態碼和所有的首部。如果只想設定單個響應首部,可以使用setHeader()方法。

3)上傳資料

  在實際的業務開發中,免不了上傳資料的需求,例如表單提交、影象上傳等。下面是一張form表單,包含一個文字框和一個提交按鈕。

<form action="http://localhost:8000" method="post">
  <input type="text" name="name" />
  <button type="submit">提交</button>
</form>

  點選提交按鈕,會將整張表單提交到HTTP伺服器(程式碼如下所示),假設文字框中輸入的內容為“咖啡機strick”。

const http = require('http');
const querystring = require('querystring');

const server = http.createServer(function (req, res) {
  //宣告Content-Type響應首部,以免中文亂碼
  res.setHeader("Content-Type", "application/json; charset=utf-8");
  const arr = [];
  req.on("data", chunk => {
    arr.push(chunk);
  });
  req.on("end", () => {
    const buf = Buffer.concat(arr);        //拼接Buffer資料
    const params = querystring.decode(buf.toString());
    console.log(params.name);             //"咖啡機strick"
    res.write(JSON.stringify(params));    //響應資料
    res.end();
  });
}).listen(8000);

  在data事件中接收資料並新增到arr陣列中,然後在end事件中由Buffer.concat()方法拼接Buffer資料。querystring模組的decode()方法可將查詢字串解析成一個物件。write()方法會將響應資料傳送給客戶端。

4)客戶端服務

  http模組提供了request()方法,可讓客戶端向伺服器發起請求。在下面的示例中,postData變數是要傳送的資料,options變數是各種配置引數,包括請求首部的資訊。

const http = require('http'); 
const querystring = require('querystring');

const postData = querystring.stringify({
  'name': '咖啡機strick'
});
const options = {
  hostname: 'localhost',
  port: 8000,
  path: '/',
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': Buffer.byteLength(postData)
  }
};
const req = http.request(options, (res) => {
  const code = res.statusCode;                    //狀態碼
  const headers = JSON.stringify(res.headers);       //響應首部
  res.setEncoding('utf8');         //為可讀流設定字元編碼
  res.on('data', (chunk) => {
    console.log(chunk);            //響應資料
  });
  res.on('end', () => {
    console.log('響應中已無資料');
  });
});
req.write(postData);
req.end();

  注意,在請求最後需要呼叫end()方法,結束本次請求。Node還提供了一個基於request()的便捷方法:get(),它會自動呼叫req.end()方法,並且請求方法是GET。

5)代理

  代理是一種特殊的網路服務,允許一個客戶端通過這個服務與目標伺服器進行間接的連線,也就是在它們之間建立了一層中轉,可用來過濾廣告、控制內部資源的訪問許可權、保障終端的隱私或安全等。

  http模組可搭建出一個簡易的HTTP代理伺服器,下面示例引用自《Node.js硬實戰》技巧52一節。

const http = require('http');
const url = require('url');
http.createServer(function (req, res) {
  const options = url.parse(req.url);  //一個URL物件
  options.headers = req.headers;
  
  req.on('data', chunk => {            //將請求的原始資料傳送給代理
    proxyRequest.write(chunk);
  });
  req.on('end', chunk => {             //完成原始請求
    proxyRequest.end();
  });
  
  //複製原始請求
  const proxyRequest = http.request(options, proxyResponse => {
    proxyResponse.on('data', chunk => {    //將響應的原始資料傳送給客戶端
      res.write(chunk);
    });
    proxyResponse.on('end', chunk => {     //完成代理請求
      res.end();
    });
    //將響應報文傳送給客戶端
    res.writeHead(proxyResponse.statusCode, proxyResponse.headers);
  });
}).listen(8000);

  url模組的parse()方法能將字串轉換成一個URL物件。在繫結請求物件的data事件後,就能將客戶端的請求資料傳送給代理。通過http.request()方法發起一次代理請求,並將伺服器的響應資料傳送給客戶端。

&n