# 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 是否經過篡改。其中**加密者和解密者必須持有同一個密鑰**。

**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;
}
```