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