---
# System prepended metadata

title: Node.js 學習筆記
tags: [Advantech]

---

# Node.js 學習筆記

## Intro

- 可以在瀏覽器外執行 javascript
- 同時處理多個 connection，適合高流量網站服務
- 提供網頁瀏覽器未提供的功能：API for 檔案操作、網路路由、作業系統 (原生 js 沒辦法)
- 應用：Web, RESTAPI, real-time app

## npm：Node.js 套件管理器

可以透過 npm，下載別人寫好的套件，然後[在程式碼中引用](#Modules)。
這邊是幾個 npm 最需要知道的命令：
- `npm init`：初始化 Node 專案並加入 `package.json`（無已安裝套件） 
- `npm install <套件名稱>`：將指定套件安裝到此專案
    - 會自動產生一個 `node_modules` 的資料夾，存放不同套件的檔案
    - 不會去修改 `node_modules`，也不會放上 github
    - `package-lock.json` 則鎖定版本套件號的檔案，確保不會有不相容的情況發生。
- `npm install`：直接去找 `package.json` 然後安裝對應套件
- `npm run <命令名稱>`：用 npm 去找 `package.json` 的 `scripts` 欄位，定義如下：
    - 在 `package.json` 裡面的 `script` 欄位
        ```javascript=
        // 裡面分別是 "命令名稱": "[commands]"
        "scripts": {
            "test": "echo \"Error: no test specified\" && exit 1",
            "start": "node server.js"
        },
        ```
    - 因此 `npm run start` 對應到的就是 `node server.js` 這個命令。

## 基本語法

### 匿名函式：用 `=>`

匿名函式通常用在監聽事件裡面或是 callback，因為不需要重複呼叫。會直接在參數後接上 `=>` 指向大括號，表示這是一個匿名函式，而前面就不需要寫函數名稱了。
```javascript=
// 具名函式
async function handleMessage(context, next) {
    const text = context.activity.text;
}
this.onMessage(handleMessage); // 傳函式參考
```
```javascript=
// 匿名函式：用在 onMessage 事件監聽器中
this.onMessage(async (context, next) => {
    const text = context.activity.text;
});
```

### 可選鏈式操作符（Optional Chaining）

可選鏈式操作符用一個簡單的 `?.` 來幫助你在物件鏈中輕鬆地檢查某個屬性是否存在。如果鏈中的某個屬性不存在，表達式會返回 `undefined`，而不會拋出錯誤。用來處理可能為空或未定義的數據時非常實用。一大特點是**安全地訪問嵌套的屬性**。
```javascript=
const user = {
  name: 'Alice',
  address: {
    city: 'Wonderland',
  }
};

// 傳統方式，需多次檢查屬性是否存在
const city = user && user.address && user.address.city;
console.log(city); // 'Wonderland'

// 使用可選鏈式操作符
const cityNew = user?.address?.city;
console.log(cityNew); // 'Wonderland'

// 如果 user.address 不存在，不會拋出錯誤，而是返回 undefined
const postalCode = user?.address?.postalCode;
console.log(postalCode); // undefined
```

### 樣板字面值（Template literals）

使用 `` 來包住字串，並用 `${}` 來包住想替換的值，功能上就類似於 Python 的 f-string。其中直接對字串換行，印出來也是有換行。
```javascript=
var a = 5;
var b = 10;
console.log(`Fifteen is ${a + b} and
not ${2 * a + b}.`);
// "Fifteen is 15 and
// not 20."
```

### JSDoc

為 JavaScript 所使用的 API 文件，在程式碼內透過**註解**的方式撰寫，運行後 JSDoc 會自動掃描註解內容，並生成一份網頁版的文件。就算不是用 Typescript 也能達成型別檢查的用途。
```javascript=
/**
 * Decide which agent to invoke based on agentName
 * @param {string} agentName - 後面可接此參數的註解
 * @param {string} userName
 * @param {object} userInfo
 * @param {string} message
 * @param {string} nextAction
 * @param {string} tool
 * @param {object} toolParams
 * @return {Promise<string>} agent response
 */
async getAgentResponse(
    agentName,
    userName,
    userInfo,
    message,
    nextAction,
    tool,
    toolParams,
) {
    ...
}
```
- `@function` 定義函式 
- `@description` 函式的描述內容 
- `@param` 函式的參數 
- `@returns` 函式返回值
- `@example` 寫一些函式 input/output 範例參考
- `@module` 將當前文件定義為模組，文件內所有參照都屬於這個 module
- `@global` 跨檔案使用@typedef，將會忽略程式碼本身作用域，定義為全域皆使用
- `@throws` 定義 function 會拋出的錯誤 
- `@see` 表示可以參考另一個文件內容或外部資源，外部資源用 `@link` 連結 
- `@class` /`@constructor` 將內容定義為 Class，通常會自動識別，不需要特別撰寫定義

## Objects in Node.js

- process

### Promise

Promise 物件代表的是一個異步操作的完成/失敗狀態以及其結果。以下是 Promise 的三種狀態，用來處理異步請求
- Pending: 初始狀態，動作尚未結束
- Fulfilled: 完成動作且結果成功
- Rejected: 動作失敗

創建與使用 Promise 的語法如下，其中有兩個內建方法，`resolve` 和 `reject`。`resolve` 表示操作成功，並傳遞結果；而 `reject` 表示操作失敗，並傳遞錯誤訊息，狀態變化如下列程式碼註解：

```javascript=
// [PENDING] Create a new Promise
const myPromise = new Promise((resolve, reject) => {
  // Simulate an async operation (e.g., API call, file read)
  setTimeout(() => {
    const success = Math.random() > 0.5;
    
    if (success) {
      // [FULFILLED] operation sucessful
      resolve('Operation completed successfully');
    } else {
      // [REJECTED] operation failed
      reject(new Error('Operation failed'));
    }
  }, 1000); // Simulate delay
});

// Using the Promise
myPromise
  .then(result => console.log('Success:', result))    // resolve 會跑來這
  .catch(error => console.error('Error:', error.message)); // reject 會跑來這
```

以傳統 Callback 處理方式與 Promise 處理方式比較，前者會有「回調地獄」，也就是程式碼太多階層，難以閱讀；後者則很乾淨使用 `then-catch` 處理錯誤，先試 `then` 裡面的方法，有錯誤則跑到 `catch` 那邊，如下，Promise 只需要用到一個 `catch` 就可以處理錯誤。（範例中，`user`, `order` 也是自定義變數）

```javascript=
getUser(id, (err, user) => {
  if (err) return handleError(err);         // 如果發生錯誤，處理錯誤
  getOrders(user.id, (err, orders) => {     // 用 user.id 去獲取訂單
    if (err) return handleError(err);       // 如果發生錯誤，處理錯誤
    // 處理訂單...
  });
});
```

```javascript=
getUser(id)
  .then(user => getOrders(user.id))  // 當 getUser 完成後，執行 getOrders
  .then(orders => processOrders(orders))  // 當 getOrders 完成後，處理訂單
  .catch(handleError);  // 如果有任何錯誤，處理錯誤
```



## Async/Await

Node.js 用 `async` 以及 `await` 來處理異步請求。前者定義在函式前面，說明這是一個**會回傳 Promise**的異步函式。後者定義在表達式的前面，用來**暫停目前動作，直到 Promise 被處理完成**才會繼續執行。

```javascript=
async function getData() {
  console.log('Starting...');
  const result = await someAsyncOperation();
  console.log(`Result: ${result}`);
  return result;
}

function someAsyncOperation() {
  // Promise 物件
  return new Promise(resolve => {
    setTimeout(() => resolve('Operation completed'), 1000);
  });
}

// Call the async function
getData().then(data => console.log('Final data:', data));
```

## Modules

Node.js 一般使用 `require` 來載入會用到的模組，相當於 Python 的 `import`，通常寫在程式的開頭。以下是 require 的範例。

```javascript=
require('dotenv').config();
const express = require('express');
const axios = require('axios');
const path = require('path');
```

常用模組 (modules) 包含以下幾種：

### `http`
- 建構 Web 服務器的模組
    ```javascript=
    const http = require("http");
    const server = http.createServer();

    // 主要修改代碼 回調函數中傳入兩參數
    // 一個是請求一個是響應
    server.on("request", (req, res) => {
        // req.url 可以獲得 :5000 後的 url 地址
        console.log(req.url);
        // res.write 方法可寫入文字 且會被瀏覽器編譯 所以可以寫入標籤
        res.setHeader("Content-Type", "text/plain; charset=utf-8");
        // 接著傳入中文測試，即可發現不再是亂碼
        res.write("<h1>TEST RES</h1>");
        // res.end 方法是結束響應 不然會一直轉圈圈
        res.end()    
    });

    server.listen(5000, () => {
        console.log("running...");
    });
    ```

### `dotenv`
- 找 `.env` 檔案並一行行解析，把每個變數加入至 `process.env`（Node.js 全域環境變量）
    ```javascript=
    require('dotenv').config();
    const API_KEY = process.env.API_KEY;
    console.log('Loaded API_KEY =', API_KEY);  // print to terminal
    ```
### `express`
- **是最常用的**，建立一個 **HTTP server**，定義 get, post 等伺服器端的路由與行為。而 `(req, res) => {...}` 為 `express` 的兩個物件。
- 簡單來說，就是製作 API 的模組
    ```javascript=
    const express = require('express');
    const app = express();
    
    // 解析 JSON body
    app.use(express.json());

    // 路由
    app.get('/', (req, res) => {
      res.send('Hello, Express!');
    });
    app.post('/echo', (req, res) => {
      res.json({ youSent: req.body });
    });
    app.listen(3000, () => {
      console.log('Express running on http://localhost:3000');
    });
    ```
    - `req`：包含「客戶端送來的請求」所有資訊。
        - `req.method`：HTTP 方法（GET、POST…）
        - `req.url`：請求的路徑（例如 `/api/llm`）
        - `req.headers`：所有標頭
        - `req.body`：若有用 `express.json()`，就是解析後的 JSON 物件
        - `req.params`：URL 路徑參數（像 `/user/:id` 的 `id`）
        - `req.query`：URL 查詢字串（例如 `?q=hello` 裡的 `q`）
    - `res`：負責「回應給客戶端」的物件。
        - `res.status(code)`：設定 HTTP 狀態碼（如 res.status(404)）
        - `res.json(obj)`：把物件序列化成 JSON, 設 `Content-Type: application/json`，並送出去
        - `res.send(text)`：直接送出字串或 Buffer
        - `res.sendFile(path)`：傳送檔案
        - `res.set(header, value)`：設定自訂標頭
        - `res.redirect(url)`：發一個 302 跳轉至指定 URL
### `axios`
- 建立基於 Promise 的 **HTTP client**，發起客戶端 get, post 等請求。
    ```javascript=
    const axios = require('axios');

    // GET
    axios.get('https://api.github.com/repos/nodejs/node')
      .then(resp => console.log(resp.data.full_name))
      .catch(err => console.error(err));

    // POST
    async function createPost() {
      const resp = await axios.post('https://jsonplaceholder.typicode.com/posts', {
        title: 'foo',
        body: 'bar',
        userId: 1
      });
      console.log(resp.data.id);
    }
    createPost();
    ```
### `path`
- 組合路徑，功能類似於 Python 的 `os.path`
    ```javascript=
    // 讀取 public 資料夾裡面的 html, css
    // __dirname：目前檔案所在資料夾絕對路徑變數
    // 再透過 path.join 串接路徑名
    app.use(express.static(path.join(__dirname, 'public')))
    ```

### `fs`
- 在瀏覽器中沒有操作文件的方法，但在 Node.js 通過引入核心模塊 fs 即可實現。
    ```javascript=
    // 引入文件核心模塊
    const fs = require("fs");

    // 使用 fs.readFile 方法讀取文件 
    // 參數1 是要讀取的文件路徑  
    // 參數2 是回調函數 回調中第一個參數是錯誤對象 第二個是文件內容
    fs.readFile("./README.md", (err, data) => {
      // 由於文件默認存儲二進制數據 
      // 然後 Node.js 本身又會把它轉成十六進制 所以輸出結果是十六進制
      // 解決看不懂十六進制的問題：使用 toString 方法把 data 轉回正常文字
      console.log(data.toString());
    });
    ```
    
### `jsonwebtoken` 

jwt 是一種無狀態式（stateless）的身份驗證憑證，可以在不登入的情況下就讓客戶端提供 token 給伺服端，並夾雜必要資訊讓伺服端可以驗證身份。

jwt 是一組字串，透過（`.`）切分成三個為 Base64 編碼的部分：
- Header：標記 token 的類型與雜湊函式名稱。
- Payload：要攜帶的資料，例如 user_id 與時間戳記(用戶資訊)，也可以指定 token 的過期時間。
- Signature：根據 Header 和 Payload，加上密鑰 (secret) 進行雜湊，產生一組不可反解的亂數，當成簽章，用來驗證 JWT 是否經過篡改。其中**加密者和解密者必須持有同一個密鑰**。

![image](https://hackmd.io/_uploads/rkG_y5g1be.png)

**Encoder**

```javascript
import jwt from 'jsonwebtoken';

async encodeJWT(payload) {
    // 取得 JWT secret
    if (!this.jwtSecret) {
      throw new Error('JWT secret is required.');
    }

    // 簽署 JWT token
    const token = jwt.sign(payload, this.jwtSecret, {algorithm: 'HS256'});
    return token;
}
```

**Decoder**

```typescript
import jwt from 'jsonwebtoken';

public async decodeJWT(req: Request): Promise<string[]> {
    // 先從請求中解析出 token
    const authHeader = req.headers['authorization'];
    // 形式為 "Bearer <token>"，所以取 index=1
    const token = authHeader.split(' ')[1];

    // 取得環境變數中的 JWT_SECRET 進行解碼（需與加密時一樣）
    const decodedPayload = jwt.verify(token, this.jwtSecret, {
        algorithms: ['HS256'],
    }) as jwt.JwtPayload;

    return decodedPayload;
}
```
