Node.js 學習筆記

前言

這是一份未整理的 Node.js 學習筆記

正文

安裝

  • 在 CentOS 7 上安裝 Node.js 和 NPM
    NodeSource 是一家致力於提供企業級Node 支持的公司,他們為Linux 發行版維護一致更新的Node.js 軟件倉庫。
    要從CentOS 7 系統上的NodeSource 軟件倉庫安裝Node.js 和npm ,請按照下列步驟操作:
  1. 添加NodeSource yum 軟件倉庫
    Node.js 的當前LTS 版本是10.x 版。如果你想安裝的版本8 只吧下面的命令中setup_10.x 更改為setup_8.x 。
    運行以下curl命令將NodeSource yum軟件倉庫添加到您的系統:

    curl -sL https://rpm.nodesource.com/setup_10.x | bash -
  2. 安裝Node.js 和npm
    啟用NodeSource 軟件倉庫後,通過以下命令安裝Node.js 和npm :

    yum install nodejs
  3. 驗證Node.js 和npm 安裝

    node -v
    npm -v
  • 如何使用NVM 安裝Node.js 和npm
    NVM (Node 版本管理器)是一個bash 腳本,用於管理多個活動的Node.js 版本。NVM 允許我們安裝和卸載任何特定的Node.js 版本,這意味著我們可以擁有任何數量的Node.js 版本供我們使用或測試。
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash && export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

參考資料來源

要在CentOS 系統上使用NVM 安裝Node.js 和npm ,請按照下列步驟操作:

使用 fs module

  • fs.writeFileSync
    const fs = require('fs);
    在 Node.js 裏頭,如果要引用一個 module ,要用一個變數引用,然後之後就可以使用它
    例如
    fs.writeFileSync('hello.txt', 'Hello fromNode.js');
    上面的 function ,是將 Hello fromNode.js 寫進 hello.txt 這個檔案

    ### 建立一個最簡單的 server
    首先,我們先引用 `http` module,當我們要調用本地 module 時,我們可以指定路徑,像是 `./http` ,但我們要調用 global 的 module 時,我們不加任何路徑, 如下
    ```javascript
    const http = require('http');

接下來,我們利用剛剛引用的 http module 來建立一個 server ,如下:

const server = http.createServer((req, res) => {
console.log(req);
});

or

const server = http.createServer(function(req, res){
console.log(req);
});

or

function rqListener(req, res) {
console.log(req);
}
const server = http.createServer(rqListener);

最後,我們雖然已經建立了 server ,但是我們還沒有指定它的位址。 我們指定 3000 port 給這個 server ,如下:

server.listen(3000);

此時,我們可以從瀏覽器,輸入 localhost:3000 來拜訪這個 server

停止這個 loop

const http = require('http');
const server = http.createServer((req, res) => {
console.log(req);
process.exit();
});
server.listen(3000);

從 request 中取得我們想要的資訊

舉例來說,我們要取得 url , method , 以及 header 三項資訊,如下:

const http = require('http');
const server = http.createServer((req, res) => {
console.log(req.url, req.method, req.header);
// process.exit();
});
server.listen(3000);

下圖,我們可以看到我們特別指定的三項資訊:

設定 response

我們可以在 server 中,指定 response ,如下:

const http = require('http');
const server = http.createServer((req, res) => {
console.log(req.url, req.method, req.header);
// process.exit();
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title></head>');
res.write('<body><ht>Hello from my NOde.js Server</ht></body>');
res.write('</html>');
res.end();
});
server.listen(3000);

然後打開開發者工具,我們可以看到我們剛剛設定的 header

然後 response 的地方可以看到我們剛剛設定的 response

簡易的 request routing

我們可以指定觸發特定 response 的 url ,當 client 呼叫這個 url 時,就會觸發我們指定的 response ,反之,則觸發另外的 response

const http = require('http');
const server = http.createServer((req, res) => {
const url = req.url;
if (url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title></head>');
res.write('<body><form action="/message" method="post"><input type="text" name="message"><button type="submit">Send</button></form></body>');
res.write('</html>');
return res.end();
}
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title></head>');
res.write('<body><ht>Hello from my NOde.js Server</ht></body>');
res.write('</html>');
res.end();
});
server.listen(3000);

由上面的 code 可以看到,我們指定 req.url 必須要絕對等於 / 才會觸發條件內,我們指定的 response 如下:

當我們按下 send ,會執行 post method, action /message ,如下:

因為 action 的關係,會嘗試拜訪 message url ,而因為這個 url 並不符合我們設定的條件,所以會執行預設 response

簡單的 redirect request

現在,我們要簡易的 redirect 我們的 request ,如下:

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

const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;
if (url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title></head>');
res.write('<body><form action="/message" method="post"><input type="text" name="message"><button type="submit">Send</button></form></body>');
res.write('</html>');
return res.end();
}
if (url === '/message' && method === 'POST') {
fs.writeFileSync('message.txt', 'DUMMY');
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
}
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title></head>');
res.write('<body><ht>Hello from my NOde.js Server</ht></body>');
res.write('</html>');
res.end();
});
server.listen(3000);

從上面的 code 可以看到,我們新增了第二個 if statement。
如果 url 等於 //message 以及 method 等於 post ,雙重條件都符合之下,就會觸發我們設定的條件
我們使用了之前我們曾經使用的 fs module ,如果條件觸發,我們就會將 DUMMY 寫入一個叫做 message.txt 的檔案
接著回傳 status code 302
最後回導到 /
res.end() 之後,我們不可以在 define 新的 res ,否則就會出現錯誤,因為這邊我們要使用 return ,後續的代碼就不會再執行

Parsing request bodies

本章節,我們將解析 request 裡頭的 body 資料
並且初次接觸到了 stream 以及 buffer 的概念。
首先,我們先設定一個事件。 當接收到 data 時,觸發一個 function 並且帶入 chunk , chunk 是資料的最小單位。
接著我們使用了 console.log 來把 chunk 印出來!
同時,我們建立一個 body 常數 array ,並且將每一次觸發 data 事件時,我們都將 chunk 丟到這個 array 裏頭, 代碼如下:

const body = [];
req.on('data', (chunk) => {
console.log(chunk);
body.push(chunk);
});

接著,我們在建立一個事件,當 request 接收完成,我們在定義一個常數,叫做 parsedBody , 至於這個常數的內容,我們使用 buffer 物件,來將 body array 裡頭的 chunk 都串起來,然後轉換成 string 。
最後,我們使用 console.log 把常數 parsedBody 印出來,代碼如下:

req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
console.log(parsedBody);
});

結果如下:

接下來,我們在定義一個常數 message ,它的內容是用 ‘=’ 來將常數 parsedBody 分隔,變成一個 array ,然後我們取 [1] ,就是 array 中的第二項資料。
然後,我們將這個常數 message 一方面利用 console.log 印出來,一方面利用 fs module 來寫到一個叫做 message.txt 的檔案中。
代碼如下:

req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split('=')[1];
console.log(message);
fs.writeFileSync('message.txt', message);
});

至此, 此 episode 告一段落,最後全部的 code 如下:

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

const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;
if (url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title></head>');
res.write('<body><form action="/message" method="post"><input type="text" name="message"><button type="submit">Send</button></form></body>');
res.write('</html>');
return res.end();
}
if (url === '/message' && method === 'POST') {
const body = [];
req.on('data', (chunk) => {
console.log(chunk);
body.push(chunk);
});
req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split('=')[1];
console.log(message);
fs.writeFileSync('message.txt', message);
});
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
}
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title></head>');
res.write('<body><ht>Hello from my NOde.js Server</ht></body>');
res.write('</html>');
res.end();
});
server.listen(3000);

了解事件驅動代碼的執行

本章節介紹了事件驅動代碼的執行規則以及順序
舉例來說,如果我們對目前的代碼做了一些調整,如下:

req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split('=')[1];
fs.writeFileSync('message.txt', message);
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
});

首先,在一開始我們就引用了 http module 以及 fs module ,然後我們利用 http module 來建立一個 server ,並且讓這個 server 聽 3000 port 。
當有任何 requets 呼叫這個 server 時,都會觸發這個 server 。我們帶入 request 以及 response , 在 server 內可以用。
首先,我們定義發請求的 url 為常數 url , 再來,我們定義發請求的方法為常數 method
如果常數 url 等於 / 時,會觸發一系列的 response ,並且 return res.end(); 做結束。
如果常數 url 等於 /message 且常數 method 等於 POST 的話,定義常數 body 為 array。
接下來進入事件驅動, 當開始解析 request 時,我們帶入 chunk ,印出 chunk ,並且將 chunk 放入一個叫做 body 的 array 常數
另外一個事件,當 request 解析完成後, 定義一個常數叫做 parsedBody ,它是利用 buffer 物件來將在常數 body 內的所有 chunk 串連起來,然後變成 string
在定義一個常數叫做 message , 首先, 常數 parsedBody 是一個 string ,我們將這個 string 用 = 為分隔點,將這個 string 變成 array 之後,取 [1] ,就是這個 array 的第二項資料,這個就是常數 message 的值
接下來,我們利用一開始引用的 fs module , 將常數 message 的內容寫入一個叫做 message.txt 的檔案。
接下來,定義 response 的 status code 為 302
定義 response 跳轉的 location 為 /
最後, return res.end();

出了事件驅動之後,是定義 header ,然後定義另外一些 html 的 response , 最後是 res.end();

Server 內的執行部分到此做一個結尾。

由於 js 的事件驅動屬性,事件 end 並不會先被執行,反之,後面的代碼會先被執行。
所以這個更動會造成一個錯誤,那就是當 res.end(); 已經被執行了,才開始執行 end 事件內的 setHeader 以及 statusCode ,這樣就會造成如下的錯誤

如果,我們在 end 事件之下加了 return ,那錯誤就不會出現 , 修改代碼如下:

req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split('=')[1];
fs.writeFileSync('message.txt', message);
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
});
return;

因為 end 事件之下的 res.setHeader('Content-Type', 'text/html'); 就不會被執行了。
注意! 在 return 的當下,其實 end 的監聽事件是還沒有被執行的,但是當 server 裡頭的動作執行完畢之後, request 被解析完成,觸發了 end 的監聽事件,然後才開始執行這個事件裡頭的動作。

至此,此 Episode 告一段落,截至目前的完整程式碼如下:

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

const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;
if (url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title></head>');
res.write('<body><form action="/message" method="post"><input type="text" name="message"><button type="submit">Send</button></form></body>');
res.write('</html>');
return res.end();
}
if (url === '/message' && method === 'POST') {
const body = [];
req.on('data', (chunk) => {
console.log(chunk);
body.push(chunk);
});
req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split('=')[1];
fs.writeFileSync('message.txt', message);
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
});
return;
}
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title></head>');
res.write('<body><ht>Hello from my NOde.js Server</ht></body>');
res.write('</html>');
res.end();
});
server.listen(3000);

Blocking and Non-Blocking Code

所以,fs.writeFile 跟 fs.writeFileSync 差在哪?
fs.writeFileSync 會待這個檔案寫入的任務完成之後,才會繼續向後執行,而 fs.writeFIle 會異步執行,儘管檔案寫入的任務還沒完成,程式一樣會繼續向後執行,並且,我們可以在任務完成時執行一項 callback ,修改代碼如下:

req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split('=')[1];
fs.writeFile('message.txt', message, (err)=>{
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
});
});

從以上的代碼來看,當程式執行到寫入檔案那一行, fs.writeFile ,程式不會停下來等待 fs.writeFile 執行完畢,反之,程式會繼續往下跑 , 而當 fs.writeFile 執行完畢後,會觸發我們設定的 callback ,進而執行以下的代碼

res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();

簡述事件迴圈

本章節主要參閱官方文件 , 以及這位大大的文章
在本章節中,主要是搞懂 Node.js 中事件迴圈的概念。

  • Node.js 的架構圖

上圖可以看到,除了 V8 Engine , Node.js 使用了 libuv 來處理 I/O 的部分,提供了 asynchronous 以及 Non-Blocking API 以及事件迴圈 , 下面提到的事件迴圈,主要與 libuv 有關。

  • 什麼是事件迴圈?
    事件迴圈,藉由將工作量分擔給 Kernel 來處理,使 Node.js 得以做非阻塞 I/O 的操作,儘管 JsvaScript 是單線程的。

因為目前新型的 Kernel 都是多線程的,它們可以在背景運行多個程序。當其中一個程序完成了, Kernel 會通知 Node.js ,所以 Node.js 會調整將適合的 callback 加到 poll 階段的 queue 當中 ,這些 callback 最終將會被執行。

深談事件迴圈

以下是事件迴圈各個階段圖,以及運行順序

每個階段都有自己的 先進先出 的要被執行的 callback queue 。
每個階段都有自己特別的運行方式,一般來說,當事件迴圈跑到一個特定的階段,事件迴圈將會執行這個特定階段裡頭的操作,然後執行它的 callback ,這個執行的動作會重複,直到該階段內的 callback 都被執行完畢了,或者已經達到最大的執行數量。
當 queue 裡頭的工作都被處理完了,或者已達最大執行數量限制,事件迴圈會進入下一個階段,反覆循環。

因為上述提到的這些程序很有可能排定更多的程序,且由 poll 階段處理的事件將被 kernel 佇列著 , 所以 poll 事件可以在被佇列的同時也被執行。 造成的結果是,一個耗時較長的 callback , 會允許 poll 階段執行的久一點,甚至讓 timer 階段的工作等待。

各階段概述

  • timers: 這個階段主要處理 setTimeout() 以及 setInterval() 排程的 callback
  • I/O callbacks: 除了 timers, setImmediate(), close 之外的多數類型
  • idle, prepare: 只供內部使用
  • poll: 取回新的 I/O 事件; 某些情況, node 將會阻塞在這裡
  • check: setImmediate() callbacks 將會在這階段被觸發
  • close callbacks: socket, on …

libuv 各階段詳述

timers:

簡單來說, timers 階段將處理 setTimeout() 以及 setInterval() 的工作。 timers 並不保證可以準確地在給予的時間點執行 callback , 反之 ,給予的時間更像是一個最低的門檻,唯有過了這個給予的時間點, callback 才會被執行,這視乎當時的工作狀態。 系統的排程或者是其他 callback 的運行都可能會延遲 timers 執行的確切時間。總而言之,過了指定的時間點之後, timers 會盡可能地盡快執行排程的 callback
可以看看以下的範例:

var fs = require('fs');

function someAsyncOperation (callback) {
// Assume this takes 0 ms to complete
fs.readFile('/path/to/file', callback);
}
function anotherAsyncOperation (callback) {
// Assume this takes 0 ms to complete
fs.readFile('/path/to/file', callback);
}

var timeoutScheduled = Date.now();

setTimeout(function () {

var delay = Date.now() - timeoutScheduled;

console.log(delay + "ms have passed since I was scheduled");
}, 100);


// do someAsyncOperation which takes 200 ms to complete
someAsyncOperation(function () {

var startCallback = Date.now();

// do something that will take 10ms...
while (Date.now() - startCallback < 200) {
; // do nothing
}

});


// do anotherSyncOperation which takes 200 ms to complete
anotherAsyncOperation(function () {

var startCallback = Date.now();

// do something that will take 10ms...
while (Date.now() - startCallback < 200) {
; // do nothing
}

});

從上面的範例中可以看到, setTimeout 任務原定 100 ms 之後被執行,但是 someAsyncOperation 任務花了 0 + 200 ms ,當執行這個任務時,事件迴圈正處在 poll 階段,所以在一個循環中, 需等待 poll 階段中的任務完全處理完畢,或者達到最大處理數量限制。
所以在上面的範例中,需等待 poll 階段的任務 someSyncOperation 以及 anotherSyncOperation 被執行完畢,總共花費 400 ms 左右, 之後才會執行 setTimeout() 的任務。

I/O callbacks

這個階段主要執行系統端操作的 callbacks, 像是 TCP 錯誤。
舉例來說,當試圖連接時,如果一個 TCP socket 接收到 ECONNREFUSED, 某個 *nix 系統想要等待並回報錯誤,這些都會在 I/O callbacks 階段被佇列。

poll

poll 階段有兩種主要功能:

  1. 替時間點已經到的 timers 執行腳本
  2. 處理 poll queue 當中的事件

當事件進入 poll 階段,且沒有 timers 排程事件 , 下面兩件事中,其中一件會發生:

  • 如果 poll 階段不為空,事件迴圈將會執行佇列中的所有 callbacks ,又或者達到最大 callbacks 處理上限
  • 如果 poll 階段為空,以下兩件事中,其中一件會發生:
    • 如果腳本已經被 setImmediate() 排程,事件迴圈將會結束 poll 階段,並且繼續進入到 check 階段來處理該佇列中的排程
    • 如果腳本沒有 setImmediate() 的排程,那事件迴圈將會等待新的事件被加入到佇列,然後立即處理他們

一旦 poll 循環為空,事件迴圈將會檢查 timer 中有沒有可以執行的 callback。 如果有一個或多個可以執行了, 事件迴圈會回去執行 timer 階段的 callback

check

這個階段允許在 poll 階段完成後,立即執行 callback。
如果 poll 階段處於空轉,或者已經有 setImmediate() 的排程,事件迴圈將會繼續進入到 check 階段,而不會等待。

setImmediate() 事實上,是一個很特別的 timer 階段,它跟 timer 在事件迴圈內跑在不同的階段。 它使用 libuv API ,這個 API 排程 callback 使之在 poll 階段結束後被執行

通常,事件迴圈會停在 poll 階段等待新的 request 或 connection ,但是當 setImmediate() 有排程,且 poll 階段處於空轉, 那事件迴圈將會結束 poll 階段,並且進入 check 階段

close callbacks

如果一個 socket 或 handle 忽然被關閉, close 事件將會被置於這個階段,除非我們指定 process.nextTick 來執行它

setImmediate() vs setTimeout()

setImmediate()setTimeout() 很類似,但根據被呼叫的時機不一樣,行為也不同。

  • setImmediate() 被設計為,一旦 poll 階段結束時執行
  • setTimeout() 排程任務,在特定的時間之後執行

兩者之間執行的順序,根據被呼叫時的情況而有所不同。如果兩者都在主模組的時候被呼叫,那順序將由當時的程序的表現所決定,意思就是說,順序無法預測。
範例如下:

// timeout_vs_immediate.js
setTimeout(function timeout () {
console.log('timeout');
},0);

setImmediate(function immediate () {
console.log('immediate');
});

然而,如果兩者是在 I/O cycle 中被呼叫,那 sedImmediate() 將會優先於 setTimeout()

// timeout_vs_immediate.js
var fs = require('fs')

fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})

對比 setTimeout() , 使用 setImmediate() 的主要優勢為,如果在 I/O cycle 中, setImmediate() 將會被優先執行,不管 setTimeout() 有幾個

process.nextTick()

理解 process.nextTick()

你可能已經注意到, process.nextTick() 並沒有被顯示在圖表上,儘管它也是 asynchronous API 的一部分。 這是因為 process.nextTick() 技術上來說不算是事件迴圈的一部分。 nextTickQueue 將會在目前操作完成後,立即被執行,不管目前是在事件迴圈內的哪一個循環。

看看我們的圖表,不管在什麼時候,只要你在特定的階段呼叫 process.nextTick() , 所以經由 process.nextTick() 送出的 callbacks 將會在事件迴圈啟動下一個階段之前全部都處理完畢。 這樣的模式可能會造成一些不好的情況發生,因為如果你遞迴的使用 process.nextTick() callback ,就會造成所謂的 I/O 飢餓 ,事件迴圈將會無法進入 poll 階段

為什麼這樣的行為會被容許?

你可能會想,為什麼這樣的行為在 Node.js 終會被容許? Node.js 部分的設計哲學是, API 總是異步的,不管是否必要,可以參考以下範例:

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall (callback) { callback(); };

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {

// since someAsyncApiCall has completed, bar hasn't been assigned any value
console.log('bar', bar); // undefined

});

var bar = 1;

如果我們執行上面的代碼,會出現輸出如下:

因為 someAsyncApiCall 並沒有做任何異步的動作,照同步的流程跑到 console.log 時, bar 還沒有被定義

如果我們將代碼改成以下:

function someAsyncApiCall (callback) {
process.nextTick(callback);
};

someAsyncApiCall(() => {
console.log('bar', bar); // 1
});

var bar = 1;

可以得到以下的輸出:

如上所述, process.nextTick() 的執行時間,是在當前的階段內所有的工作都完成了,在進入下個階段之前,會將所有的 process.nextTick() 處理完畢。
在上面的例子中, process.nextTick() 會等到所有在此階段的代碼都被執行完畢,也就是待 var bar = 1 執行後,才去執行這個 callback ,所以不會出現 undefined 的情況。
請注意!這沒有最大處理數量限制,所以如果利用 process.nextTick() 指派遞迴任務,那就會造成 I/O 飢餓 情況, 事件迴圈將無法接收到新的 request

一個 tick 到底是多長?

一個 tick 的時間長度,是 Event Loop 繞完一圈,把所有 queues 中的 callbacks 依序且同步地執行完,所消耗的總時間。因此,一個 tick 的值是不固定的。可能很長,可能很短,但我們希望它能盡量地短。

process.nextTick() vs setImmediate()

千萬不要被這兩個階段的命名搞混了!

  • process.nextTick():
    在當前階段結束前執行完畢

  • setImmediate():
    在下一個階段,或者下一個事件迴圈的 tick 中執行

基本上,這兩個命名應該是要互換。 process.nextTick()setImmediate() 更快地被觸發。
這算是一個很難更動的部分,因為當初命名錯誤之後,隨時時間的推移,越來越多 npm 的 package 都是使用這樣的命名,所以一旦這命名變更了,影響會非常的大。

官方文件上建議開發者,在任何情況中,都使用 setImmediate() ,因為它可以更簡單的被邏輯思考,然後在不同的環境上,有著更廣的相容性。

Promise

從下面的原始碼可以看到 Promise , 或者又稱為 microtasks 的執行優先順序
依照原始碼的執行順序來看,在一個階段結束之前,process.nextTick() 會先被執行,緊接著, 執行 Promise

startup.processNextTick = function() {
var nextTickQueue = []; // Callbacks 會排進這個 queue!!
var pendingUnhandledRejections = [];
var microtasksScheduled = false;
var _runMicrotasks = {};
// ... 略
process.nextTick = nextTick; // nextTick 函式在下面
// ... 略
// process._setupNextTick 在 node.cc 中, 我認為意思到了, 就不用再挖下去了
const tickInfo = process._setupNextTick(_tickCallback, _runMicrotasks);
_runMicrotasks = _runMicrotasks.runMicrotasks;
// ... 略
function _tickCallback() {
var callback, args, tock;

do {
while (tickInfo[kIndex] < tickInfo[kLength]) {
// callbacks 從 queue 中一個一個被挖出來執行
tock = nextTickQueue[tickInfo[kIndex]++];
callback = tock.callback;
args = tock.args;

if (args === undefined) {
nextTickCallbackWith0Args(callback);
} else {
switch (args.length) {
case 1:
nextTickCallbackWith1Arg(callback, args[0]);
// ...
}
}
if (1e4 < tickInfo[kIndex])
tickDone();
}
tickDone();
// process.nextTick 的 callbacks 跑完, 接著跑 Promise 的 microtasks
_runMicrotasks();
emitPendingUnhandledRejections();
} while (tickInfo[kLength] !== 0);
}

// ...略
function nextTick(callback) {
var args;
if (arguments.length > 1) {
args = [];
for (var i = 1; i < arguments.length; i++)
args.push(arguments[i]);
}

// 將 callback 連它的 arguments 用一個物件存起來推進 queue
nextTickQueue.push(new TickObject(callback, args));
tickInfo[kLength]++;
}

// ...
};

事件迴圈總結

  • 順序:
    timers → I/O callbacks → idle, pare → poll → check → close callbacks → timers … 往復循環

  • 順序細節

    • timers 設定的時間過了之後,才會被’盡快’的執行。
      如果 poll 階段內還有工作還沒做完,會先做完,才會執行 timers 的工作,所以可能會延遲
    • 當處於 I/O 程序中,比如說, fs 模組中, setImmediate() 順序一定大於 setTimeout() ,因為 check 階段緊接在 poll 階段之後
    • 當處於主要模組中, setImmediate() 以及 setTimeout 的優先順序,取決於運行狀況,這個狀態下,次序無法確定
    • process.nextTick() 將在當前階段的工作結束前,在進入下一個階段之前執行, 所以他的優先性是第一名的
    • promise 的執行次序緊接在 process.nextTick() 之後,也是在當前階段結束前執行完畢

Express.js

建立一個 app server

  • 安裝 npm

    npm install --save
  • 安裝 express

    npm install --save express
  • 安裝 nodemon

    npm install --save-dev nodemon
  • 將 npm start script 設為 nodemon
    Set script as nodemon fileName.js

指定 status code

res.status (statusCode);

Promise

以下的範例中, function test 中,我們 return 了一個 Promise ,如果帶入 test function 中的 argument 是 1 ,那就走 resolve 路線 , 而除了 1 之外所有的 argument, 都走 reject 路線。
在 function main 中, 我們使用了 function test, 並帶入 argument 1, 個人覺得這有點像是 PHP 當中的 ternary 用法。
當 argument 等於我們在 promise 當中指定的 1 時,走 resolve 路線, 而 then 就是當 promise 為 resolve 路線時該做的事。
當 argument 等於是除了 1 之外的任何數,也就是會走 promise 當中的 reject 路線, 此時將會執行 catch 的動作。
我們在 promise 當中指定,當走 resolve 路線時,輸出為字串 Success, 所以在 then 的 closure 當中,被帶入的 argument 就是 Success
反之,當走 reject 路線時,輸出字串為 Error, 所以在 catch 的 closure 當中,被帶入的 argument 則為 Error

function test(number) {
return new Promise((resolve, reject) => {
if (number === 1) {
resolve("Success")
} else {
reject("Failed")
}
})
}
function main() {
test(1).then((result) => {
// result === "Success"
console.log(result)
}).catch((error) => {
// 不會被執行, 因為狀態是成功
})
test(2).then((result) => {
// 不會被執行, 因為狀態是成功
console.log(result)
}).catch((error) => {
// error === "Failed"
console.log(error)
})
}

建立 Datastore Model

// 從 google SDK 引用 Datastore function
const {Datastore} = require('@google-cloud/datastore');

// 輸入 project_id
const projectId = 'balmy-sanctuary-238903';

// 初始一個 Datastore instance
const datastore = new Datastore({
projectId: projectId,
});

// 匯出這個 module
module.exports = datastore;

建立一個 Controller


// 匯出這個 function
exports.test = function (req, res) {

async function quickStart() {
// The kind for the new entity
const kind = 'abc';
// The name/ID for the new entity
const name = 'sampletask1';
// The Cloud Datastore key for the new entity
const taskKey = datastore.key([kind, name]);

// Prepares the new entity

const task = {
key: taskKey,
data: {
description: 'Buy milk',
},
};

console.log(datastore.key(['name', 'kind']));
// Saves the entity
await datastore.save(task);
console.log(`Saved ${task.key.name}: ${task.data.description}`);
res.send(`Saved ${task.key.name}: ${task.data.description}`);
}

quickStart().catch(console.error);
};

Route

var express = require('express');
var router = express.Router();

// 導入 controller 模組, 並給予名稱
var datastore = require('../controllers/datastoreController');

/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
// 建一個 router, 並且導向 datastoreController 裡頭的 test function
router.get('/test', datastore.test);

module.exports = router;

Root address

// 找一個地方新增一個檔案,輸入以下的 code
// 之後我們就可以在任何一個檔案中,透過 require 這個檔案來 const rootDir
const path = require('path');
module.exports = path.dirname('/Users/ray/code/datastore/app.js');

若我們 console.log 上面 exports 的值,可以得到該專案下的 root 位址

Path

利用 path module 來指定路徑

// p 會等於專案根目錄下, data 資料夾之下的一個叫做 `products.json` 的檔案
// 專案根目錄請參考 `root address` 章節
const p = path.join(rootDir, 'data', 'products.json')

object.assign

  • 可用來複製或覆蓋目標物件
    let exampleObject = {a:1, b:2, c:3, c:4};
    let copy = object.assign({},
    exampleObject,
    {a:4, b:4, c:4, d:4});

test

  • 用來確認該 string 是否符合該 regex patten
    var str = "Hello world!";

    // look for "Hello"
    var patt = /Hello/g;
    var result = patt.test(str);
    // result = true

時間

var moment = require('moment-timezone');
var test = moment(createdDate).tz("Asia/Taipei").format('YYYY-MM-DD HH:MM:SS');
console.log(test);
// 2019-05-21 08:05:44

同時異步發多請求,並待全部有結果後繼續

// 需安裝兩個套件 `request-promise` 以及 `p-limit`
const request = require('request-promise');
const pLimit = require('p-limit');

class HealthCheckService {

static async getHealthCheckResults(sites) {

// 指定 limit 同時最多發十個 request
const limit = pLimit(10);


// 利用 map 從 sites 中拿到我們要發請求的 url
// 然後利用套件 `limit` 來限制同時發請求的數量,再來使用 `request-promise` 套件來對上面拿到的 url 發請求
let promises = sites.map((site) => {
let url = `https://yourAPI?host=${site.host}&cname=${site.cname}`;
return limit(() => request(url));
});

// 上面的每一個 promises, 都是一個請求。 現在我們利用 `Promise.all`, 待所有的結果都回來之後,在 return
return await Promise.all(promises);
}
}

dotenv

安裝

npm

npm install dotenv

yarn

yarn add dotenv

建立 .env 檔

.env

touch .env
  • require 語法, 會自動去讀 .env
    require('dotenv').config();

customName.env

touch custom.env
  • require 語法, 需要額外指定
    require('dotenv').config({ path: 'custom.env' });

使用方法

直接使用 process.env

let DB_AUTH = process.env.DB_AUTH;



使用 multer 上傳檔案

安裝

npm install multer --save

定義 storage

在 /Storages/local.js 輸入以下 code

const multer = require('multer');
const moment = require('moment');
module.exports = multer.diskStorage({
destination: function (req, file, cb) {
// 目的地資料夾位址
cb(null, 'uploads')
},
filename: function (req, file, cb) {
// 上傳檔案命名規則
cb(null, file.fieldname + '-' + moment(new Date()).format("YYYYMMDD-hhmmss"))
}
});

建立 middleware

在 server.js require 上面定義的 storage 以及 multer

const multer = require('multer');
const storage = require('./Storages/local');
const upload = multer({storage: storage,});

將 upload 加到 route

App.post('/logParsing/upload', [upload.single('log'), LogParsingController.validate('upload')], LogParsingController.upload);

Error Handing

在 Controller 中自定義 error handing, 也可建立一個 middleware 來驗。 注意: err, req, res, next 的順序至關重要!!

upload(err, req, res, next) {
if (err instanceof multer.MulterError) {
return res.status(400).send(Output(false, [], 'Wrong field'))
}
}




validator

安裝

Document

npm install express-validator --save

引用模組

在 controller 引用模組

const { body } = require('express-validator');
const { validationResult } = require('express-validator');

設立規則

在 controller 定規則, 新增一個 method

validate(method){
switch (method) {
case 'getCountHost': {
return [
body('measurement').exists(),
]
}

case 'getSumData': {
return [
body('measurement').exists(),
body('host').exists(),
]
}

case 'getData': {
return [
body('measurement').exists(),
body('host').exists(),
]
}
}
}

驗證並回報錯誤

在 controller 中定義錯誤並設定回報格式

const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).send(Output(false, [], errors.errors[0].msg))
}

使用 validator

以 middleware 的方式使用 validator

App.post('/netdatadb/sum', NetdataController.validate('getSumData'), NetdataController.getSumData);

結果

若有錯誤, 結果如下:

{
"succeeded": false,
"data": [],
"message": [
{
"msg": "Invalid value",
"param": "measurement",
"location": "body"
}
]
}



serve static file in Express

app.use('/static', express.static(path.join(__dirname, 'public')))

path.join(__dirname, ‘public’): 使用絕對路徑讀取檔案

以下為請求的路徑範例:

http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css
http://localhost:3000/static/js/app.js
http://localhost:3000/static/images/bg.png
http://localhost:3000/static/hello.html

Error Handing in Express

參考文件

  • 同步的錯誤需要被 catch 嗎? 不需要

  • 對 controller 的 error handler 的 error catch?

    exports.catchErrors = (fn) => {
    return function(req, res, next) {
    return fn(req, res, next).catch(next);
    };
    };
  • next()next(err) 的分別? next() 是將當前的 middleware 終止, 並導向下一個 middleware, 而 next(err) 則是會直接導向 err handler

  • 異步錯誤需要特別被 catch 嗎? 需要哦!

  • 下面的代碼中, next 的位置是怎麼樣的一種寫法? 將 next 置於 callback 的位置, 當沒有錯時, 跳往下一個 handler, 而當有錯誤時, 將錯誤導向 error handler

    app.get('/', [
    function (req, res, next) {
    fs.writeFile('/inaccessible-path', 'data', next)
    },
    function (req, res) {
    res.send('OK')
    }
    ])
  • 在 asynchronous 中, 如何處理 error? 若出錯, 要將 err 放到 next() 中帶往 error handler

  • 下面的代碼中, 處理 error 的邏輯是什麼? 當 fs.readFile 沒錯時, 跳往下一個 handler, 如果有錯, 跳往 error handler

    app.get('/', [
    function (req, res, next) {
    fs.readFile('/maybe-valid-file', 'utf-8', function (err, data) {
    res.locals.data = data
    next(err)
    })
    },
    function (req, res) {
    res.locals.data = res.locals.data.split(',')[1]
    res.send(res.locals.data)
    }
    ])
  • 在 production 環境中, Express 會將 stack trace 送往客戶端嗎? 不會

  • 如果已經開始回 response 了才遇到錯誤, 比如說正在串流到客戶端, Express 預設的 error handler 會結束掉連線嗎? 會哦

  • 在使用自定義 error handler 時, 若要避免正在回 response 時遇到錯誤, 要注意什麼? 要檢查 header 是否已經傳送了, 若是, 則要將錯誤導向 Express 預設 error handler 來中止連線

  • 以下代碼是什麼樣的錯誤處理邏輯? 如果 header 已經傳送了, 將錯誤傳給 Express 預設 error handler 來中止連線

    function errorHandler (err, req, res, next) {
    if (res.headersSent) {
    return next(err)
    }
    res.status(500)
    res.render('error', { error: err })
    }
  • 什麼情況之下, 即使已經有建立自定義 error handler 了, Express 還是會將 error 送到預設的 error handler? 當 next(err) 被呼叫了一次以上

  • 定義一個 error-handling middleware, 需要幾個參數? 四個

  • 如何在自訂的 error handler 中, 在回應 client 之前, log stack trace?

    console.error(err.stack)
  • 怎麼寫一個最簡單的 log error 的 middleware?

    function logErrors (err, req, res, next) {
    console.error(err.stack)
    next(err)
    }
  • 請敘述下面的 error handler 邏輯 若請求有使用 xhr 的話, 直接回應指定錯誤訊息, 若無, 導向預設 error handler (因為已經呼叫兩次)

    function clientErrorHandler (err, req, res, next) {
    if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' })
    } else {
    next(err)
    }
    }
  • 如果我有兩組以上的 app.get(‘/‘, [fn1, fn2, fn3]), 現在我已經執行了 fn1, 我想要跳掉 fn2, fn3 到下一個 app.get(‘/‘, [fn4, fn5, fn6]), 該怎麼做?

    fn1(req, res, next){
    //do something
    next('route')
    }
利用 Gitlab CI/CD 部署專案到 GCP virtual machine Docker 技術筆記

留言

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×