Node.js 介紹
什麼是 Node.js ?
Node.js 是能夠在伺服器端運行 JavaScript 的開放原始碼、跨平台執行環境。
Node.js 的出現,讓前端網站開發人員可以使用 JaveScript 來做後端或是系統層面的工作。讓前端開發網站開發人員使用已懂的 JavaScript 語言就可以自行設定網站伺服器。
Node.js 採用 Google 開發的 Chrome V8 JavaScript 引擎和 libuv 函式庫,可以用指令去執行 JavaScript,使用非阻塞輸入輸出、非同步、事件驅動等技術來提高效能,可最佳化應用程式的傳輸量和規模。這些技術通常用於資料密集的即時應用程式。

主要的 Node.js 組件 (An Intro to Node.js That You May Have Missed)
其他開源、低級組件,主要用 C/C++ 編寫:
- c-ares : 用於非同步 DNS 請求的 C 函式庫,用於 Node.js 中的一些 DNS 請求。
- http-parser:一個輕量級的 HTTP 請求/回應解析器。
- OpenSSL:一個著名的通用密碼庫。用於
tls和cryto模組。 - zlib:無損數據壓縮庫。
Node.js 能快速的原因是因為他對資源的調校不同,當程式收到一筆連線,
相較於 PHP 的每次連線都會新生成一個執行緒,當連線數量暴增時很快就會消耗掉系統的資源,並且容易產生阻塞 (block),
而 Node.js 則是會通知作業系統透過 epoll、kqueue、/dev/poll、select 等將連線保留,並放入 heap 中配置,先讓連線進入休眠 (Sleep) 狀態,等系統通知才觸發連線的 callback。
這種處理方式只會佔用記憶體,並不會使用到 CPU 資源。另外因為 JavaScript 語言的特性,每一個 request 都會有一個 callback,可以避免發生阻塞的狀況發生。
以下會先從非阻塞輸入輸出、非同步,再談到事件驅動。
非阻塞 (non-blocking)
輸入輸出(I/O) 是程式跟系統記憶體或網路的互動,例如發送 HTTP 請求、對資料庫CRUD 操作等等。
以網站開發者角度來看,大部分的網站程式都不需要太多的 CPU 計算,反而是在等待大量的 I/O 處理完畢 (HTTP 請求、資料庫的取得資料或是更新資料等),所以處理 I/O 的速度會是網頁程式效能的關鍵。
那要怎麼才能讓等待 I/O 的時間,不要卡住後續的程式碼呢?可以讓程式一邊等 I/O 處理,一邊繼續執行其他部分的程式碼,主要有兩種方法:
- 多執行緒 (multi-threaded):使用阻塞 (blocking) I/O 的設計。
- 單執行緒 (single-threaded):使用非阻塞 (non-blocking) I/O 的設計 + 非同步 (asynchronous) 處理。
阻塞就是 I/O 的處理阻擋了其他後續程式碼的執行。舉個例子:
| 阻塞 (blocking) | 非阻塞 (non-blocking) |
|---|---|
| 阻塞後續程式碼的執行,就好像是我們去附近買烤肉,交給老闆後,為了想吃到熱騰騰的食物,所以只能留在原地,不能先去其他地方 | 非阻塞不會阻擋後續程式碼的執行,就好像是我們去百貨公司美食街點餐,點完餐後會拿到一個呼叫器,就可以先離開,等到呼叫器響了再回去拿即可 |
像是 Python、Ruby 等語言是使用多執行緒 (multi-threaded),使用阻塞 I/O :
程式會等網路或是記憶體的作業結束後才會繼續往下,等待時間這個作業中的執行緒不會去做其他事情。
如果想要達到『等待 I/O 期間不要卡住其他程式碼』,的做法就是新開一個執行緒,直到任務完成,再告訴主執行緒說( 我完成囉!) 即可。
Node.js 使用單執行緒 (single-threaded) ,非阻塞 I/O + 非同步函式:
Node.js 使用非阻塞設計,那要怎麼去操作資料庫或是 HTTP 請求的輸入輸出呢?就要透過非同步 (asynchronous) 來處理囉!
非同步 (asynchronous)
非同步也可以稱為異步,它的作用就是讓程式不要被阻擋等 I/O 處理完,才可以跑下一行程式碼,是直到函式中的 callback 被呼叫的時候再執行後續要做的事情。
同步 vs. 非同步
- 非同步 (asynchronous)
可以想像一下你去咖啡廳買拿鐵跟黑咖啡,可能會發生的情況是:
- 你點了拿鐵跟黑咖啡
- 店員在收銀機上輸入點餐內容
- 店員請同事 A 準備拿鐵、請同事 B 準備黑咖啡,並告知做完後,要提醒店員
- 黑咖啡製作會比較快,B 同事會先完成,而剛好店員剛幫你結帳完沒事,所以把黑咖啡拿給你
- 拿鐵製作包含較多步驟,花費時間較久,等 A 同事完成後,店員剛好沒事,所以把拿鐵拿給你
可以看到,櫃檯店員一次只能做一件事情,但為了節省時間,店員將工作分配給其他同事,在下完指令後,店員會繼續幫你結帳,等同事們各自完成後會告知店員,店員在依序把飲料交給你 - 最終等待時間減少,也不會浪費閒置的資源,這就是現實生活中非同步的情況。
- 同步 (synchronous)
一樣我們點了拿鐵跟黑咖啡,換成同步的話:
- 店員在收銀機上輸入點餐內容
- 店員請同事 A 開始準備拿鐵
- A 同事準備完拿鐵,店員轉交給你
- 店員請同事 B 開始準備黑咖啡
- B 同事準備完黑咖啡,店員交給你
- 店員幫你刷載具、打統編、找錢等等
同樣的餐點內容,如果是同步處理,代表要等每一件事情做完,才可以做下一步。也就是說同事 A 完成拿鐵後,店員才請 B 同事準備黑咖啡。相對於非同步來說,會花費不少時間以及浪費不少閒置資源。
從咖啡店的例子中可以發現:
- 櫃檯店員手上一次能做的事情只有一件 (Single thread 單執行緒),只是在非同步的例子中,將製作咖啡的事情委派給其他同事處理,讓自己可以繼續幫你結帳,來提高效率 — JavaScript 是 Single thread,一次只能做一件事情。
- 在非同步的例子中,櫃檯店員將製作咖啡的事情委派出去,其實店員也不知道哪一個任務會先被完成,但店員還是可以繼續完成結帳的任務,不會因為同事 A、B 還在製作咖啡,就不能接下去動作 (non-blocking) — JavaScript 一次只能做一件事,但藉由 Node 提供的 API 協助,在背後處理這些事件 (同事在背後製作咖啡),可以等待製作同時,不會被阻塞 (blocking) 到下一件事情的執行。
- 非同步的例子中,店員委派事情的流程很簡單:就是請同事完成製作咖啡 (event) + 在收到同事通知完成後接手咖啡,並轉交給你 (callback function) — 若是採用非同步處理,會有 callback function 來指定事件完成後要接續做什麼:它不會立即被執行,而是等待委託的事情被完成後才觸發。
- 當同事 A、B 分別通知完成後,就會依序把咖啡放在店員的旁邊排成一排 (event queue)。想向店員有一個小助手 (event loop) ,他的工作內容是確認店員結帳完了沒有:如果結帳完了,就會把隊伍中第一杯咖啡叫給店員,讓店員交給你 (觸發 callback function) ; 如果店員還在結帳,就會讓隊伍中的咖啡擺在旁邊繼續等待。
JaveScript 實現非同步的方法不斷演進著:從 callback、promise 到最新的 async-await 函式。
Callback
什麼是 Callback
假設有 A、B、C 三件工作,其中 B 必須等待 C 做完才能執行。大部份的人幾乎都是做 A,再做 C,等待 C 做完以後最後做 B。但對於可多工的人來說,卻可能是同時做 A 與 C(多工),等待 C 完成後做 B。
Callback function 是一個被作為參數帶入另一個函式中的「函式」,這個被作為參數帶入的函式將在「未來某個時間點」被呼叫和執行 — 這是處理非同步事件的一種方式。
再次舉 A、B、C 三件工作的例子,其中 B 必須等待 C 做完才能執行,於是我們將 B 放到 C 的 callback 中,讓宿主環境在收到 C 完成的回應時後 B 放到佇列中準備執行。
doA();
doC(function() {
doB();
});常見的例子:
- 使用瀏覽器所提供的
setTimeout()或是setInterval()
setTimeout(() => {
console.log('這個訊息將在三秒後被印出來')
}, 3000)提供一個匿名函式作為參數帶入 setTimeout() 函式中,目的就是請 setTimeout() 在未來某個時間點(三秒後)呼叫和執行這個匿名函式。
- DOM 的事件監聽
const btn = document.querySelector('button')
btn.addEventListener('click', callbackFunctionName)callbackFunctionName 做為參數被帶入 addEventListener() 中,callbackFunctionName 不會立即被執行,而是未來按鈕被點擊時才會執行。
Callback 主要有一個缺點:回呼地獄
回呼地獄 (Callback Hell)
回呼地獄 (Callback Hell) 又稱為「毀滅金字塔」,指的是層次太深的巢狀 Callback,讓程式變的更複雜且難以預測或是追蹤。
向遠端伺服器發出請求並獲得資訊後,執行 Callback,再發出請求,獲得資訊後執行 Callback,再發出請求,獲得資訊後執行 Callback,就會不小心一層包一層,變成所謂的 Callback Hell。
doA(function() {
doB();
doC(function() {
doD();
});
doE();
});
doF();缺點:
- 可讀性低:如果程式碼出錯,要回頭慢慢找錯誤的地方
- 可維護性低:如果要修改其中一組函式,牽一髮而動全身
那在同步執行情況下,可以使用 try...catch 來捕捉錯誤訊息,但如果是在非同步情況下,要怎麼處理錯誤或是例外訊息呢!? 主要有兩種方式:
分別回呼 (Split Callback)
分別的回呼要設定兩個 Callback,一個用於成功通知,另一個則用於錯誤通知。如下,第一個參數是用於成功的 Callback,第二個參數是用於失敗的 Callback:
function success(data) {
console.log(data);
}
function failure(err) {
console.error(error);
}
ajax('http://sample.url', success, failure);那如果在 Callback 中發生錯誤,要怎麼辦呢!?
function success(data) {
console.log(x);
}
function failure(err) {
console.error(error);
}
ajax('http://sample.url', success, failure);
// Uncaught (in promise) ReferenceError: x is not defined
會直接報錯,並不會進入到 failure 這個 Callback 裡面,也就是說,如果是在 Callback 內發生錯誤,是不會被捕捉到的。
錯誤優先處理 (Error-First Style)
Node.js 的 API 常見這樣的設計方式,第一個參數是 error ,第二個參數是回應的資料 (data)。檢查 error 是否有值或是 true,否則就接續處理 data。
function response(err, data) {
if(err) {
console.error(err);
} else {
console.log(data);
}
}
ajax('http://sample.url', response);接下來我們來看 Promise,可以解決 callback 可讀性低的 Callback Hell 問題。
Promise
Promise:Callback 以外的另一種方式來處理非同步事件,且可讀性與可維護性比 Callback 好很多。 Promise 是一個物件,代表著一個尚未完成,但最終會完成的一個動作 - 在一個非同步處理流程中,它只是一個暫存的值。
我們一樣來說剛剛咖啡店的例子: 當我們點完拿鐵跟黑咖啡後,店員會給你一張印有號碼的收據,然後告訴你等等聽到號碼,就可以來領咖啡了,而這張收據就是 Promise,代表這個任務完成後,就可以接著執行接下來的動作了。
在等待過程中,其實無法百分百確定最後一定會拿到咖啡 (Promise) ; 店員可能順利做完咖啡交到你手上 (Resolved) ; 可能牛奶或是咖啡豆沒了,所以店員告訴你今天做不出來咖啡 (Rejected)。
Promise 就像上面的例子中,會處在三個任意階段中:
- Pedning:等待事情完成中,但不確定最終會順利完成或失敗
- Resolved(或稱 Fulfilled):代表順利完成了,並轉交結果
- Rejectesd:代表失敗了,並告知失敗原因
const getData = new Promise((resolve, reject) => {
// 製作咖啡.....
// 作業完成,並回傳錯誤訊息時
if (error) {
return reject('牛奶或是咖啡豆沒了')
}
// 作業成功完成
resolve({
data: '咖啡交到你手上',
})
})- resolve 函式:當非同步作業成功完成時,將結果做為參數帶入執行。
- reject 函式:當非同步作業失敗時,將錯誤訊息作為參數帶入執行。
創建出來的 Promise 物件在實例上有兩個重要的方法:
.then() 方法
當成功從 resolve() 獲得結果時 - 狀態會由 Pending 轉為 Resolved - then() 方法就會被調用。
getData
// 使用 then 方法,並將成功訊息印出來
.then(data => {console.log('成功資料', data)})以剛剛咖啡店的例子來看, then() 方法就像你會去聽咖啡人員是否叫號,並確認咖啡是否成功準備好了,就領取咖啡來喝。
.catch() 方法
當從 reject() 獲得錯誤訊息時 - 狀態由 Pending 轉為 Rejected - catch() 方法就會被調用來處理錯誤。
getData
// 使用 then 方法,並將成功訊息印出來
.then(data => {console.log('成功資料', data)})
// 使用 catch 方法,並將錯誤訊息印出來
.catch(error => console.log('錯誤訊息', error))一樣以剛剛咖啡店的例子來看, catch() 方法就像你會去聽咖啡店店員是否有告知咖啡做不出來的訊息,如果有,你就會去櫃檯退錢或是改其他產品代替。
串接 then() 方法
還記得上面我們示範的 callback hell ,因為我們依序要向不同的伺服器或資料庫取得資料,也要依序處理多個非同步的作業,所以會有一層一層的 callback 去達成。
從上面 then() 和 catch() 方法例子中可以發現,非同步作業執行完成後的處理步驟被獨立開來,這樣程式碼的可讀性就會高很多 - 在依序處理多個非同步作業時會更為明顯,我們可以看到下面我們用 then() 方法做串接:
getData
// 使用 then 方法,並將成功訊息印出來
.then(data => {
console.log(data.data1) // abc
return data.data1 + 'def'
})
// 獲得前一個 then() 回傳的結果
.then(data => {
console.log(data) // abcdef
return data + 'ghi'
})
// 獲得前一個 then() 回傳的結果
.then(data => {
console.log(data) // abcdefghi
})
// 使用 catch 方法,並將錯誤訊息印出來
.catch(error => console.log('錯誤訊息', error))Promise.all([….]) 方法
當在處理「多個非同步事件」時,Promise.all() 方法會等所有 Promise 都被順利完成 (Resolved) 後,才會執行接下去的動作 ; 一旦收到某一個 Promise 回傳錯誤 (Rejected),就會立即執行後續的錯誤處理流程。
我們以剛剛咖啡店的例子來說,我們點完咖啡後,又因為嘴饞,多買了一個麵包來吃,這時我們手上就有兩個領餐號碼,那 Promise.all([...]) 就是當你等到兩個號碼都被叫到後,才會去領餐。
那 Promise.all([....]) 的好處什麼呢!? 它可以縮短等待時間:
const oneSecond = new Promise((resolve, reject) => {
setTimeout(() => {
// 一秒後回傳資料
resolve('one second')
}, 1000);
})
const twoSecond = new Promise((resolve, reject) => {
setTimeout(() => {
// 兩秒後回傳資料
resolve('two second')
}, 2000);
})
const threeSecond = new Promise((resolve, reject) => {
setTimeout(() => {
// 三秒後回傳資料
resolve('three second')
}, 3000);
})
// 等到三個 Promise 都成功回傳後,才執行接下去的流程
Promise.all([oneSecond, twoSecond, threeSecond])
.then(([oneSecond, twoSecond, threeSecond]) => {
console.log(oneSecond, twoSecond, threeSecond)
})我們模擬一下我們要分別向三個資料庫請求資料 (我們以 setTimeout() 示意非同步處理事件),可以看到我們要先等第一個資料庫成功回傳資料後,才展開第二次請求,等到成功收到回傳後才開始第三次請求。而使用 Promise.all() 方法,我們能先依序向資料庫發出請求,並在成功獲得全部回傳資料後,才會做後續的動作。
Async/Await
那我們剛剛透過 Promise 包裝和使用,的確避免了 callback hell 讓整個流程變得很清楚,提升了程式碼的易讀性與可維護性。
Async/Await 是所謂的語法糖衣,是一種新的語法撰寫方式,來處理「非同步事件」,讓非同步的程式碼讀起來更像在寫「同步程式碼」。
Async/Await 是用來簡單化和清楚化 Promise 串連 then 這種相對複雜的結構,他回傳的一樣也是 Promise 物件,只是針對 promise-based 寫法進行包裝。
Async 關鍵字
async 關鍵字可以放在任意函式前面,它代表「我們正宣告一個非同步的函式,且這個函式會回傳一個 Promise 物件」:
// 一般函式
function getGroupInfo() {
return data
}
// 使用 async 函式
async function getGroupInfo() {
return data
}Await 關鍵字
在 async 函式中使用 await 關鍵字,代表「我們請 JavaScript 等待這個非同步的作業完成後,才展開後續的動作」,換句話說:await 讓 async 函式的執行動作暫停,等到它獲得回傳的 Promise 物件後 - 無論執行成功 (resolve) 或是失敗 (rejected) -才會恢復執行 async 函式。
- 優點:可以直接使用 await 後獲得的回傳值存於一個變數中做後續使用,而不是在呼叫
then()方法一個一個串,讓程式碼看起來像是在處裡一般的同步程式碼,提升了易讀性與可維護性。
Async/Await 範例
我們在後續的範例,一樣使用 setTimeout() 來模擬資料庫請求和等待資料的非同步:
function getFirstInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('first data')
}, 1000);
})
}
function getSecondInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('second data')
}, 2000);
})
}
async function getGroupInfo() {
// 代表等到第一筆資料回傳後,才印出結果和請求第二筆資料
const firstInfo = await getFirstInfo()
console.log(firstInfo)
// 代表等到第二筆資料回傳後,才印出結果
const secondInfo = await getSecondInfo()
console.log(secondInfo)
}
getGroupInfo()在 getGroupInfo() 函式前加上 async 關鍵字,來告知值這是一個非同步函示, getFirstInfo() 跟 getSecondInfo() 是兩個要處理非同步的地方,因此分別在兩個呼叫函式前加上 await 關鍵字。
await 搭配 Promise.all([…]) 方法
function getFirstInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 1秒後回傳結果
resolve('first data')
}, 1000);
})
}
function getSecondInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 2秒後回傳結果
resolve('second data')
}, 2000);
})
}
async function getGroupInfo() {
try {
const firstInfo = getFirstInfo()
const secondInfo = getSecondInfo()
// 同步發出非同步請求,並等到兩秒後(非三秒)成功獲得兩筆回傳後,才印出結果
const [firstData, secondData] = await Promise.all([firstInfo, secondInfo])
console.log(firstData, secondData)
} catch (error) {
// 處理錯誤回傳
console.log(error)
}
}
getGroupInfo()透過 await 關鍵字搭配 Promise.all() 方法,同步向兩個資料庫獲取資料,並等到兩筆資料都成功回傳後,才印出結果 - 省時效率提高!
我們可以看到下面這張圖,他是 Node.js 運行時的縮圖,一旦你的 Node.js 應用程序啟動,它會先開始一個初始化階段,運行啟動腳本,包括請求模組和註冊事件 callback。然後應用程序進入事件循環(也稱為主線程、事件線程等),從概念上講,它是為通過執行適當的 JS callback 來響應傳入的客戶端請求而構建的。

Node.js 運行時的圖示 (An Intro to Node.js That You May Have Missed)
libuv 是非同步處理的函式庫(以 C 語言為主),在面對非同步作業時,會開啟執行緒池 (thread pool / worker pool) ,預設共有四個執行緒 (也就是 worker threads),運算這些 I/O 用的方式是事件迴圈 (event loop)。
所以就算 V8 處理程式碼是單執行緒,一但進入到 libuv 手上,它還是會幫你把會阻塞的 I/O 分工到執行緒池中交給不同的執行緒去處理,直到 callback 發生才會丟回應讓程式知道。
那我們來看看 libuv 如何透過事件迴圈有效的處理非同步 I/O。
事件驅動 (event-driven)
在講事件驅動前,我們先來了解一下什麼是事件:
事件 (event)
事件是指用戶或是系統作出的動作,例如使用者點選按鈕,或是檔案讀取完成、某種錯誤產生等等,都叫做事件。
事件驅動 (event-driven)
事件驅動是一種程式執行模型,表示程式的進行是依據事件的發生而定,監聽到事件就處理、處理完就執行 callback ,透過不斷的監聽跟回應事件執行程式。
而事件驅動在不同的地方有不同的實現。瀏覽器 (前端) 和 Node.js (後端) 基於不同的技術實現了各自的事件迴圈。就 Ndoe.js 來說,事件就是交給 libuv 去處理 ; 至於瀏覽器的事件迴圈在 HTML 5 的規範中有定義。
事件迴圈 (event loop)
因為 Node.js 只有一個執行緒,所以當 libuv 把非同步事件處理完後,callback 要被丟回應用程式中排隊,等待主執行緒的 stack 為空的時候,才會開始執行。這個排隊的地方就是事件佇列 (event queue)。
libuv 會不斷檢查有沒有 callback 需要被執行,有的話分配到主執行緒結束手邊的程式後處理,因此這個過程稱為 『事件迴圈』。

事件迴圈(event loop) (Node.js 101 — 單執行緒、非同步、非阻塞 I/O 與事件迴圈)
libuv 事件迴圈有哪些階段呢?

libuv 事件迴圈 (nexocode)
libuv 的事件迴圈共有六個階段,每個階段的作用如下:
Timers:等計時器 (setTimeout、setInterval) 的時間一到,會把他們的 callback 放到這裡等待執行。
Pending callbacks:會把作業系統層級的錯誤給callback (TCP errors、sockets 連線被拒絕)。
Idle, prepare:內部使用。
Poll:最重要的一個階段。
- 如果 Queue 不為空,依次取出 callback 函數執行,直到 Queue 為空或是抵達系統最大限制。
- 如果 Queue 為空但有設置 「setImmediate」,就進入 check 階段。
- 如果 Queue 為空但沒有設置 「setImmediate」,就會在 Poll 階段等到直到 Queue 有東西或是 Timers 時間抵達。
Check:處理 setImmediate 的 callback。
Close callbacks:執行 close 事件的 callback,利如 socket.destroy()。
事件迴圈就是不斷重複以上階段。每個階段都有自己的 callback 佇列,在進入某個階段時,都會從所屬的佇列中取出 callback 來執行,當佇列為空或者被執行 callback 的數量達到系統的最大數量時,就會進入下一階段。
根據以上提到事件驅動、單執行緒和非同步、非阻塞的 I/O 處理特性,Node.js 很適合拿來開發 I/O 密集型應用程式,如影音串流、即時互動、在線聊天、遊戲、協作工具、股票行情等軟體。
NPM
NPM 是跟 Node.js 一起安裝的線上套件庫,可以下載各式各樣的 JavaScript 套件來使用,能解決 Node.js 代碼部署上的很多問題。
安裝好後,可以使用 npm -v 來檢查版本:
$ npm -v
8.5.0跟 NPM 息息相關的是 package.json 這個檔案,他是掌管專案資訊的重要檔案,我們可以使用 init 指令來設定 package.json:
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
.... 省略 ....
Press ^C at any time to quit.
package name: (message) demo
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author: ian <[email protected]>
license: (ISC)- name: 就是該專案的名字,它預設就是該目錄名。
- description: 專案描述。
- entry point: 專案切入點,這有點複雜,之後再說。
- test command: 專案測試指令,之後說。
- git repository: 專案原始碼的版本控管位置。
- keywoard: 專案關鍵字
- author: 專案作者,以 author-name [email protected] 寫之。
- license: 專案版權。
設定好後,專案資料夾就會多一個 package.json 的檔案,打開後可以看到,是我們剛剛所設定好的資訊:
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "ian <[email protected]>",
"license": "ISC"
}接下來,要如何下載網路上的模組,要使用 install 指令來下載,我們下載 Node.js 最小又靈活的 Web 應用程式框架 express 來做示範:
$ npm install express
added 50 packages, and audited 51 packages in 1s
2 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities下載好後,可以看到剛剛 package.json 檔案多了 dependencies 欄位,它裡面會紀錄我們安裝了哪些套件,所以未來我們想知道專案使用了哪些套件,我們可以從 dependencies 這個欄位知道。
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "ian <[email protected]>",
"license": "ISC",
"dependencies": {
"express": "^4.17.3"
}
}除此之外,還會多一個 node_modules 資料夾,這個資料夾就會存放我們所下載的套件
$ ls node_modules/
accepts cookie-signature etag inherits mime-types qs setprototypeof
array-flatten debug express ipaddr.js ms range-parser statuses
body-parser depd finalhandler media-typer negotiator raw-body toidentifier
bytes destroy forwarded merge-descriptors on-finished safe-buffer type-is
content-disposition ee-first fresh methods parseurl safer-buffer unpipe
content-type encodeurl http-errors mime path-to-regexp send utils-merge
cookie escape-html iconv-lite mime-db proxy-addr serve-static vary當我們都安裝好後,express 已經包在 node_modules 目錄內,在專案裡面,就可以使用 require(‘套件名稱‘) 來使用套件囉!
var express = require('express');因為這些套件都可以直接在網路上下載到,所以在推送 git 專案時,可以使用 .gitignore 來隱藏不想被 push 的檔案,當我們想要下載套件回來時,只要使用:
$ npm install就可以依照 package.json 裡面的 dependencies 來下載套件!
REPL
Node.js REPL (交互式解釋器):表示一個電腦環境,類似 Windoes 系統的終端或是 Unix/Linux 的 Shell,我們可以在終端機上輸入命令,並接收系統的響應。
Node 自帶了交互式解釋器,可以執行以下任務:
- 讀取:讀取用戶輸入,解析輸入的 JavaScript 數據結構並儲存在內存中。
- 執行:執行輸入的數據結構。
- 顯示:輸出結果。
- 循環:循環以上任務直到用戶按下兩次的 ctrl+c 按鈕退出。
我們可以輸入以下命令來啟動 Node 的終端:
$ node
Welcome to Node.js v16.14.2.
Type ".help" for more information.
>可以執行簡單的數學運算:
$ node
Welcome to Node.js v16.14.2.
Type ".help" for more information.
> 1+4
5
> 5/2
2.5
> 3*7
21
> 4-3
1
> 1 + (2*4) -5
4
>也可以將數據存在變數中,在需要時使用它。變數宣告需要使用 var 關鍵字,如果沒有使用關鍵字,會直接顯示出來。也可以使用 console.log() 來輸出變數
$ node
Welcome to Node.js v16.14.2.
Type ".help" for more information.
> x = 19
19
> var y = 10
undefined
> x + y
29
> console.log("Hello")
Hello
undefined
>Node REPL 也支持輸入多行程式,我們試著寫一個 do-while 迴圈:
Welcome to Node.js v16.14.2.
Type ".help" for more information.
> var x = 0
undefined
> do {
... x++;
... console.log("x:" + x);
... } while (x<5);
x:1
x:2
x:3
x:4
x:5
undefined
>也可以使用下底線(_)來獲得上一個程式的運算結果:
$ node
Welcome to Node.js v16.14.2.
Type ".help" for more information.
> var x = 10
undefined
> var y = 20
undefined
> x + y
30
> var sum = _
undefined
> console.log(sum)
30
undefined
>Express 框架
Node.js 在實作上不會單獨使用,通常會搭配框架去使用,像是 Express JS 後端框架,可以讓開發人員在寫同一個功能時,少寫很多程式的工具。
Express 是 Node.js 環境下提供的輕量後端架構,自由度極高,透過豐富的 HTTP 工具,能快速發開後端應用程式,它提供:
- 替不同 HTTP Method、不同 URL 路徑的 requests 編寫不同的處理方法。
- 透過整合「畫面」的渲染引擎來達到插入資料到樣板產生 response。
- 設定常見的 web 應用程式,例如:連線用的 Port 和產生 response 樣板的位置。
- 在 request 的處理流程中增加而外的中間層 (Middleware) 進行處理。
第一個 Express Hello world 程式:
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})$ node app.js
Example app listening on port 3000使用 node 來啟動伺服器,並使用 Port 3000 來連線。應用程式指向 URL (/) 的路由,以 “Hello World!” 回應如果是其他路徑,res 就會回應 404 找不到。
畫面 (view)
剛剛有提到說它可以使用整合「畫面」的渲染引擎來顯示到樣板,我們可以透過 --view 指令來產生樣板以及應用程式的目錄:
$ express --view=pub
create : public/
create : public/javascripts/
create : public/images/
create : public/stylesheets/
create : public/stylesheets/style.css
create : routes/
create : routes/index.js
create : routes/users.js
create : views/
create : app.js
create : package.json
create : bin/
create : bin/www路由 (router)
路由是判斷應用程式如何回應用戶端對特定端點的要求,而特定端點是一個 URL 或是路徑,與一個特定的 HTTP 要求方法 (GET、POST) 等,路由定義的結構如下:
app.METHOD(PATH, HANDLER)其中
- app 是 express 的實例。
- METHOD 是 HTTP 要求的方法。
- PATH 是伺服器上的路徑。
- HANDLER 是當路由相符時要執行的函數。
以下範例簡單說明不同 HTTP 要求的方法:
首頁中以 Hello World! 回應。
app.get('/', function (req, res) {
res.send('Hello World!');
});對根路由 (/)(應用程式的首頁)發出 POST 要求時的回應:
app.post('/', function (req, res) {
res.send('Got a POST request');
});對 /user 路由發出 PUT 要求時的回應:
app.put('/user', function (req, res) {
res.send('Got a PUT request at /user');
});對 /user 路由發出 DELETE 要求時的回應:
app.delete('/user', function (req, res) {
res.send('Got a DELETE request at /user');
});參考資料
Node.js 官網:https://nodejs.dev/learn/introduction-to-nodejs
Node.js 101 — 單執行緒、非同步、非阻塞 I/O 與事件迴圈:https://medium.com/wenchin-rolls-around/node-js-101-%E5%96%AE%E5%9F%B7%E8%A1%8C%E7%B7%92-%E9%9D%9E%E5%90%8C%E6%AD%A5-%E9%9D%9E%E9%98%BB%E5%A1%9E-i-o-%E8%88%87%E4%BA%8B%E4%BB%B6%E8%BF%B4%E5%9C%88-ef94f8359eee
An Intro to Node.js That You May Have Missed:https://itnext.io/an-intro-to-node-js-that-you-may-have-missed-b175ef4277f7
Sequelize:https://sequelize.org/v6/index.html
透過 sequelize 來達成 DB Schema Migration:https://hackmd.io/@TSMI_E7ORNeP8YBbWm-lFA/ryCtaVW_M?print-pdf#%E4%BD%BF%E7%94%A8sequelize%E5%BB%BA%E7%AB%8B%E4%B8%80%E5%BC%B5user-table
認識同步與非同步 — Callback + Promise + Async/Await:https://medium.com/%E9%BA%A5%E5%85%8B%E7%9A%84%E5%8D%8A%E8%B7%AF%E5%87%BA%E5%AE%B6%E7%AD%86%E8%A8%98/%E5%BF%83%E5%BE%97-%E8%AA%8D%E8%AD%98%E5%90%8C%E6%AD%A5%E8%88%87%E9%9D%9E%E5%90%8C%E6%AD%A5-callback-promise-async-await-640ea491ea64
你懂 JavaScript 嗎?#23 Callback:https://ithelp.ithome.com.tw/articles/10206555
How to create JOIN queries with Sequelize:https://sebhastian.com/sequelize-join/