[Node.JS] 建立伺服器、掌握基本知識

前言

本系列文章記錄了如何用最基礎的方式建立 Node.js API 服務,目的在了解基本運作原理,實務上會運用其他框架、插件和中介軟體(Middleware)來快速完成環境建置。


💎 基本技能 - 模組引用

在 Node.js 中會使用 JavaScript 的模組方法來載入各種功能,這個段落會先介紹如何操作,如果已經熟悉可以跳過,進入下個段落。

範例情境:

  • 主要檔案:a.js
  • 模組-寫法1:b.js
  • 模組-寫法2:c.js

a.js

  • 使用 require 來引入其他資源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 自己的變數
let contentA = 1;
// 引用 b.js,如果被引用的檔案在相同目錄,路徑前方加上 ./
const contentB = require('./b');
// 引用 c.js
const contentC = require('./c');

console.log(contentA); // 1

// 變數存放了回傳的整個物件
console.log(contentB); // {...}
// 可以使用物件方法取得裡面的值
console.log(contentB.title); // B
console.log(contentB.getNum()); // 2

console.log(contentC); // {...}
console.log(contentC.title); // C
console.log(contentC.getNum()); // 3

b.js

  • 使用 module.exports 設定被引用時提供的資料
1
2
3
4
5
6
7
// 方法1- 直接設定要輸出的資料,多個資料時通常用物件格式來存放
module.exports = {
title: 'B',
getNum: function () {
return 2;
},
};

c.js

  • 使用 exports.屬性名稱 設定被引用時提供的資料
1
2
3
4
5
// 方法2- 新增屬性的方式逐一增加輸出的資料
exports.title = 'C';
exports.getNum = function () {
return 3;
};

💎 創建本地伺服器 - createServer

學會模組引用的操作後,就可以試著撰寫第一個 js 檔案,讓 Node.js 建立一個伺服器服務:
以下開發環境都使用 VS Code

  1. 建立伺服器 js 檔 (檔名自定)
    下面這段程式碼是 node.js 官方範例
1
2
3
4
5
6
7
8
9
10
// 引用 http 模組
const http = require('http');
// 建立一個本地伺服器,設定接收到請求後的回傳資料
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('Hello');
res.end();
});
// 使用 8000 port
server.listen(8000);
  1. 啟動伺服器
    開啟終端機(熱鍵為 Ctrl + `),使用 Node.js 執行撰寫好的檔案,如果沒有跳出錯誤訊息就表示本地伺服器成功運行了。
1
2
** 請先將路徑切換到 js 存放的位置,檔案名稱修改為自己設定的名稱 **
node 檔案名稱.js
  1. 關閉伺服器
    伺服器執行的狀態下,在終端機視窗內按下 Ctrl+C 就可以終止。

  2. 連線測試
    伺服器正常運作後可以開啟瀏覽器,輸入網址 http://127.0.0.1:8000,沒有問題的話就會顯示 Hello,這段網址中的 127.0.0.1 表示這部電腦的本地位置,冒號加上數字 8000 就可以指定使用前面步驟設定的 port 來連線。

  3. 修改測試
    試著修改 Content-Type 或 res.write 的內容,讓網頁顯示不一樣的資訊。
    每次修改儲存後都需要重起 node,可以透過 npm 安裝 nodemon 這個套件,就能在編輯檔案後自動重新載入伺服器。

1
2
3
4
5
** 安裝指令(建議全域安裝)**
npm i -g nodemon

** 使用 nodemon 來執行 js 檔案 **
nodemon app.js

npm - nodemon

💎 程式碼說明

http

程式一開始引用了 node.js 中的 http 模組,並使用 createServer 方法來建立伺服器,模組的其他功能可以參考官方文件。

官方說明文件 - http

監聽連線請求

createServer 方法裡面傳入的函式像是 addEventListener 事件偵測,但監聽的行為是有沒有連線請求傳送到這台伺服器主機,這個監聽函式也可以使用變數獨立出來,增加一點可讀性,改寫後如下方程式碼:

1
2
3
4
5
6
7
8
9
10
const http = require('http');
// 監聽連線請求的函式
const requestListener = (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('Hello');
res.end();
};
// 建立伺服器的參數帶入儲存函式的變數
const server = http.createServer(requestListener);
server.listen(8000);

request

監聽函式帶有兩個參數,req 是 request(請求)的簡寫,當我們在瀏覽器網址列輸入網址送出連線,就會對該網址對應的伺服器主機發出請求,請求資訊會包含瀏覽器版本、需要的資源、請求的方式…等資訊,在 node.js 中可以透過 req 變數來查看詳細資訊。

  • 觀察 req 的內容:
    修改監聽函式 > 儲存檔案 > 重啟伺服器 > 瀏覽本地網址 > 在終端機查看 req 資訊
1
2
3
4
5
6
7
8
const requestListener = (req, res) => {
// 新增下面這段程式碼,來顯示請求的內容
console.log(req);

res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('Hello');
res.end();
};
  • req 的常用屬性:
    • req.url:取得請求的資源路徑。一個網站會有好幾個檔案,透過這個屬性就可以取得請求的路徑,如果請求的網址後面沒有任何路徑,就會得到根目錄(/),通常會設置回應首頁頁面。
    • req.method:取得請求的方法。一般連線請求會使用 GET 方法,但是若有夾帶其他資訊時就會使用其他方法,各種方法可以參考 MDN 的說明。

      MDN - HTTP 請求方法

    • req.on("data", ...):取得請求夾帶的資料。當連線使用 POST、PATCH…等方法時通常回夾帶資料給伺服器處理,就會需要使用這個方法來接收。
    • req.on("end", ...):資料接收完成後執行動作。網路封包傳遞會把資料切成片段,不會每次都一次傳完,所以在 on(data) 的階段無法確認收到的資料已經完成,要用這個方法才能確保資料接收完,再執行對應的程式碼。

response

  • res 是 response(回應)的簡寫,伺服器收到請求後,就會把設定好的資訊傳回用戶端,在簡易的範例中只會回應 Hello 的文字,可以撰寫程式碼結合 req.url 和 req.method 判斷來回應不同的內容。
  • res.writeHead(status, header):回傳的表頭資訊。就像 req 一樣,res 不會只傳送單純的資料,需要包含各種資訊,status code 是狀態碼,200 就是成功,而 header 會告訴對方提供了哪些連線方法,這也是開發實務上容易採坑的地方,詳細的資訊都可以在 MDN 的 HTTP 主題中查看。

    MDN - HTTP

  • Content-Type:檔案格式。瀏覽器會依據這個屬性來決定怎麼讀取接收到的資料,常見的有 JSON(application/json)、HTML(text/html)、純文字(text/plain)…等。
  • res.end():程式運作不像人類會自主開始或停止,所以需要執行一段程式碼明確的告知『我傳完了』,否則用瀏覽器連網頁後頁籤上方會一直出現轉圈圈的圖示(表示持續等待回應中)。

listen(port)

  • listen(8000):使用的通訊埠。HTTP 協定預設使用 80 port,1-1023 是系統保留,所以在自訂 port 的時候通常都是 X000 起跳。

    Wiki - Port

  • 較安全的寫法:寫好的網頁服務通常會放在各種雲端代管主機上,這些服務會由系統自動分配 port,如果程式碼寫死特定的 port,就無法跟雲端環境匹配,理所當然的就連不到,這時候可以利用環境變數 process.env.PORT 來取得執行環境的 port 號,就能動態調整讓服務正常運行,改寫如下:
1
2
3
4
5
const PORT = process.env.PORT || XXXX;
// ES6 寫法
const { PORT = XXXX } = process.env;

server.listen(PORT);

💎 探索 Req 與 Res - Postman

Postman 是一套網頁請求模擬工具,相較於用瀏覽器的 Dev 工具,Postman 專注於檢查 Request & Response,也能模擬各種請求行為(GET、POST、PATCH…等),有助於 API 的測試和 debug。

安裝 Postman

前往 Postman 官方下載,完成後執行安裝會跳轉至註冊會員網站,完成註冊後就可以使用了。

使用 Postman

進入操作介面後,一些設定或導覽可以都先略過,接著照以下步驟操作:

  1. 先點擊上方選單 HOME
  2. 點擊左側選單 Workspaces
  3. 選擇 My Workspace
  4. 點擊內容區塊上方分頁的 +
  5. 在網址列裡面輸入任一網址或是本地的網址(例如本篇文章練習的 http://127.0.0.1:8000
  6. 送出後就能查看詳細資訊了

詳細教學也可以參考:
六角學院 - Postman 教學文章


💎 補充知識

Global 全域物件

每個執行中的軟體或是瀏覽器頁面就像是一個星球,每個星球就像是一個全域物件,囊括了星球上的所有東西,在網頁環境中的全域物件是 window,而 Node.js 中是 global

雖然都是全域,除了名稱外也有些差異,假如在全域中引入兩個檔案(a.js 和 b.js),裡面都撰寫了 var a = 1,這時網頁的 window.a 會得到 1 的結果,Node.js 的 global.a 則是 undefined

Node.js 中的每個模組都會是獨立的,不會互相干擾也不會變成全域變數(除非直接操作 global 物件),網頁中的 window 則可能被 var 變數影響(ES6 後建議全部改用 let 才不會污染全域)。

檔案路徑

一個專案中會有許多資料夾和檔案,要存取這些資源就需要知道檔案的路徑位置,才能順利找到檔案,Node.js 提供 path 模組,可以快速的分解、組合路徑。

  • 全域變數:__dirname__filename 可以取得當前的目錄和檔案名稱。
1
2
3
4
5
// 目錄名稱
console.log(__dirname);

// 檔案名稱
console.log(__filename);
  • Path:Node.js 的 path 模組提供許多方法來處理路徑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 範例路徑
const url = '/aa/bb/cc.js';

// 引用 path 模組
const path = require('path');

// 取得目錄
path.dirname(url); // /aa/bb

// 取得檔名
path.basename(url); // cc.js

// 取得副檔名
path.extname(url); // js

// 拆解路徑(回傳物件格式)
path.parse(url); // { root: '/', dir: '/aa/bb', base: 'cc.js', ext: '.js', name: 'cc' }

// 組合路徑
path.join('/ii', 'jj', 'kk.js'); // /ii/jj/kk.js

Node.js - path


💎 結語

本篇的筆記記錄了如何使用 Node.js 本地建立伺服器、透過 Postman 檢查 Request & Response,接下來會記錄如何製作一個簡單的 todolist api。