跳至主要内容

83 篇文章 含有標籤「前端」

檢視所有標籤

EventSource API in JavaScript 入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

前言

在現代的網頁應用程式中,實時性資料更新是一個常見需求,例如即時通知、股價更新、聊天室訊息、伺服器狀態監控等。傳統上,開發者可能會透過輪詢(Polling)或 WebSocket 來實現。然而,若只是單向由伺服器推送訊息到瀏覽器端,其實有更簡單且高效的選擇:EventSource API

EventSource 基於 Server-Sent Events(SSE),由伺服器主動推送文字資料到客戶端,並且使用 HTTP 協議的持久連線,開發上比 WebSocket 更簡單,適合事件流的場景。本文將帶我們快速入門 EventSource API,理解它的特性與應用方式。


重點摘要

  1. EventSource 與 SSE 的核心概念

    • EventSource 是瀏覽器提供的 JavaScript API,用於接收伺服器推送的 SSE(Server-Sent Events)。
    • 採用 HTTP 長連線,不需要 WebSocket,也不需要額外協議。
    • 支援自動重連(瀏覽器會自動在連線中斷時重新連接伺服器)。
  2. EventSource 的適用場景

    • 即時通知(系統提醒、訊息推送)
    • 資料更新(股價、天氣、賽事比分)
    • 記錄串流(伺服器日誌、事件追蹤)
    • 聊天室訊息(單向推送)
  3. EventSource 的特性

    • 單向通訊:伺服器 → 客戶端
    • 自動重連機制(可透過伺服器端 retry: 指令調整重試時間)
    • 基於純文字的事件格式(MIME type 為 text/event-stream
    • 可透過自訂事件名稱分發不同事件
  4. 與其他技術比較

    • Polling:需要客戶端頻繁請求,耗費頻寬與伺服器資源。
    • WebSocket:雙向溝通更靈活,但需要額外處理協議與狀態管理。
    • EventSource(SSE):單向推送即可,實作簡單、輕量化,適合多數即時通知場景。

EventSource 使用範例

1. 瀏覽器端(JavaScript)

// 建立 EventSource 連線
const eventSource = new EventSource('/events');

// 接收預設訊息(message 事件)
eventSource.onmessage = function (event) {
console.log('收到訊息:', event.data);
};

// 接收自訂事件
eventSource.addEventListener('news', function (event) {
console.log('收到新聞事件:', event.data);
});

// 監控錯誤與連線狀態
eventSource.onerror = function (error) {
console.error('EventSource 發生錯誤:', error);
};

2. 伺服器端(Node.js Express 範例)

const express = require('express');
const app = express();

app.get('/events', (req, res) => {
// 設定 SSE 必要的 Header
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// 每隔 3 秒推送一個訊息
const intervalId = setInterval(() => {
const data = new Date().toLocaleTimeString();
res.write(`data: 現在時間 ${data}\n\n`);
}, 3000);

// 當客戶端中斷連線時清理資源
req.on('close', () => {
clearInterval(intervalId);
});
});

app.listen(3000, () => {
console.log('SSE 伺服器運行於 http://localhost:3000/events');
});

3. SSE 資料格式範例

伺服器回傳的資料需符合 text/event-stream 格式,每筆訊息以兩個換行結尾。

data: 這是一個預設訊息

event: news
data: 這是一個新聞更新

event: alert
data: 系統警告訊息
retry: 5000

說明:

  • data::主要訊息內容,可以有多行。
  • event::指定自訂事件名稱,客戶端可用 addEventListener 監聽。
  • retry::定義自動重連的延遲時間(毫秒)。

注意事項

  1. 瀏覽器支援性:大部分現代瀏覽器支援 EventSource(IE 除外)。
  2. 跨域問題:若伺服器與前端不同網域,需設定 CORS。
  3. 資料格式:僅支援 UTF-8 文字資料,若要傳送二進位資料需轉成 Base64 或 JSON。
  4. 連線數限制:部分瀏覽器對同一網域的 SSE 連線數有限制(通常 6 條)。
  5. 斷線重連:內建自動重連機制,但若伺服器返回錯誤狀態碼,可能需手動處理。

總結

EventSource API 提供了一個簡單又高效的方式,讓前端應用程式能夠輕鬆接收伺服器的即時推送。相較於 WebSocket,EventSource 不需要額外的協議處理,也避免了頻繁輪詢帶來的效能浪費。在僅需單向資料更新的場景下,它是一個理想解決方案。

當我們下次需要在前端實現即時通知、動態更新數據或流式資料顯示時,不妨先考慮 EventSource,它或許就是最輕量的選擇。

微前端(Micro-Frontend)介紹與入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

前言

隨著前端應用日益複雜、團隊規模擴大,「前端單體應用」(Monolithic Frontend)逐漸面臨維護困難、部署不靈活、開發效率低落等問題。微前端(Micro-Frontend)是一種將大型前端應用拆解為數個獨立子應用的架構設計理念,靈感來自後端的微服務(Microservices)架構。每個子應用可以由不同的團隊獨立開發、部署、維護,並共同組成一個整體的產品。

微前端不是某個框架,而是一種架構模式。它的目標是促進前端大型專案的模組化、團隊分工清晰、技術選型彈性,進而提升整體開發與交付效率。


重點摘要

微前端的常見做法

  1. Module Federation(Webpack 5 原生支援,超常見)
  • 各子應用直接共享模組,不用重複打包
  • Webpack 官方強項,Vite 沒有原生支援
  1. iframe / Web Component(框架無關,通用做法)
  • 每個子應用獨立部署,用 iframe 或 custom elements 包裝
  • Vite/React/Vue/Angular 都可以做
  1. 乾淨的 build output + 部署整合
  • 子應用都 build 出靜態資源,整合到主應用路由

  • 跟工具無關,Vite 也能勝任

  • 定義:微前端是一種將前端應用拆解為多個獨立子應用的架構設計模式。

  • 目的

    • 支援大型團隊並行開發
    • 提高部署彈性(單一子應用可獨立上線)
    • 增加技術選擇自由度(不同子應用可使用不同框架)
  • 核心概念

    • 子應用獨立開發、測試與部署
    • 主應用統一載入與整合子應用
    • 子應用可共享部分資源(如登入狀態、UI 元件)
  • 常見實作方式

    • iframe(早期簡單做法,但 UX 不佳)
    • Web Component(標準化但整合與溝通略複雜)
    • JavaScript 插入與渲染(如 single-spa、Module Federation)
  • 適用時機

    • 專案規模大、開發團隊超過 2 組以上
    • 需要支援異步部署與灰階上線
    • 跨框架共存需求(如同時有 React 與 Vue)

微前端實作方式簡介

1. iframe(不推薦)

將子應用放入 iframe 中載入,雖然簡單,但隔離性太強(無法共用狀態、樣式),SEO 和體驗差,不推薦用於現代 Web 專案。

2. Web Components(中立)

透過瀏覽器原生的 Custom Elements 技術(如 my-app-widget),讓子應用變成一個可重用的 HTML 元件,框架中立,但整合難度高。

3. JavaScript 插入與路由分流(主流)

由主應用動態載入子應用(HTML、JS、CSS),並透過路由或 DOM 控制子應用顯示。可使用像是:

其中 qiankun 是阿里開源的基於 single-spa 的微前端框架,中文文件齊全且上手容易。


實際範例:使用 qiankun 快速建立微前端架構

範例說明

目標:建立一個主應用(main-app),載入兩個子應用(react-app、vue-app)

1. 安裝 qiankun(主應用)

npm install qiankun

2. 主應用主體程式碼(main-app/src/main.ts)

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
{
name: 'react-app',
entry: '//localhost:3001',
container: '#subapp-container',
activeRule: '/react',
},
{
name: 'vue-app',
entry: '//localhost:3002',
container: '#subapp-container',
activeRule: '/vue',
},
]);

start();

3. 主應用 HTML 模板

<div id="subapp-container"></div>

4. 子應用需支援 qiankun 的生命週期函式(以 React 為例)

export async function bootstrap() {
console.log('React app bootstraped');
}
export async function mount(props) {
ReactDOM.render(<App />, document.getElementById('root'));
}
export async function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}

5. 子應用 Webpack 設定(publicPath)

output: {
publicPath: 'http://localhost:3001/',
},

微前端的挑戰與注意事項

  • 樣式隔離:CSS 必須避免衝突,可搭配 CSS Modules、Scoped CSS。
  • 狀態共享:登入資訊、使用者資料等需透過 global event 或共享 storage 處理。
  • 路由協調:子應用與主應用須協調 route 設計,避免相互干擾。
  • 部署整合:CI/CD pipeline 需考慮子應用與主應用的獨立部署與測試。

總結

微前端是一種極具彈性的架構設計理念,適合中大型團隊協作、複雜前端系統的模組化開發。不過它也帶來額外的技術成本與整合挑戰。在決定導入微前端前,應評估專案規模、開發團隊結構與維運資源是否適合。

實作上,建議可從單一框架開始(如 React + qiankun),逐步拆分模組與部署機制,再逐步進化為多框架混合的微前端架構,避免過早複雜化系統。

LangGraph 入門教學筆記:打造多步驟 AI 流程的圖形化解決方案 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

前言

隨著生成式 AI 的應用越來越廣泛,從客服機器人、智慧問答系統到複雜的自動化工作流程,開發者面臨的不再只是單次的文字生成,而是需要處理多步驟的對話邏輯與決策流程

傳統上,這類應用通常透過繁瑣的 if-else 邏輯、狀態機或多層函式巢狀處理,程式碼不易閱讀與維護。為此,LangGraph 應運而生。它是一個開源的 Python 函式庫,讓開發者可以用「流程圖」的方式清晰地定義每一步的處理邏輯,進而打造更穩定且模組化的 AI Workflow。


重點摘要

  • LangGraph 是什麼?

    • 由 LangChain 團隊開發的 AI Workflow 工具,透過流程圖(Graph)定義多步驟的推理過程。
    • 每一個節點(Node)代表一個處理步驟,從 LLM 回覆、條件判斷到工具呼叫等皆可定義成節點。
  • 核心特色

    • 使用有向圖(DAG)表示流程,每個節點都有明確的輸入與輸出狀態。
    • 支援條件分支、迴圈、自訂狀態、記憶上下文。
    • 可與 LangChain、OpenAI、Anthropic 等服務整合。
    • 適合用於構建 Agent、Chatbot、多階段處理流程。
  • 應用情境

    • 客製化對話代理人(如智能客服)
    • 多階段資訊處理(如:檢索、分類、摘要)
    • 工具選擇與執行流程(如:根據輸入選擇工具)
    • 擴展型 LLM 應用(如:RAG、Tool Use)
  • 重要元件說明

    • StateGraph:定義整體流程圖。
    • Node:每個節點代表一個具邏輯意義的步驟。
    • State:儲存目前上下文狀態,可自訂欄位。
    • Conditional Edge:根據邏輯結果決定下一個節點。

實際範例:打造一個 FAQ 對話機器人

本範例將建立一個簡單的對話流程:

  1. 使用者輸入問題。
  2. 呼叫 OpenAI GPT 模型回覆。
  3. 若輸入為 "bye",流程結束;否則持續對話。

1. 安裝套件

pip install langgraph langchain openai

2. 定義狀態與回應節點

from langgraph.graph import StateGraph, END
from langchain.chat_models import ChatOpenAI
from typing import TypedDict

# 自訂狀態格式
class ConversationState(TypedDict):
messages: list[str]
last_user_input: str

# 初始化 LLM
llm = ChatOpenAI(model="gpt-4", temperature=0)

# 處理回應的節點
def generate_response(state: ConversationState):
user_input = state["last_user_input"]
state["messages"].append(f"User: {user_input}")
response = llm.predict(f"請回答以下問題:{user_input}")
state["messages"].append(f"AI: {response}")
return state

3. 定義流程結束條件與流程圖

# 判斷是否要結束對話
def should_continue(state: ConversationState):
if state["last_user_input"].lower().strip() == "bye":
return END
return "generate"

# 建立流程圖
builder = StateGraph(ConversationState)
builder.add_node("generate", generate_response)
builder.set_entry_point("generate")
builder.add_conditional_edges("generate", should_continue)

graph = builder.compile()

4. 執行對話流程

# 初始狀態
state = {
"messages": [],
"last_user_input": "你好,這是什麼系統?"
}

# 執行第一輪
state = graph.invoke(state)

# 模擬第二輪
state["last_user_input"] = "LangGraph 是什麼?"
state = graph.invoke(state)

# 模擬結束對話
state["last_user_input"] = "bye"
state = graph.invoke(state)

# 印出對話記錄
for msg in state["messages"]:
print(msg)

範例輸出結果

User: 你好,這是什麼系統?
AI: 這是一個由 LangGraph 架構的對話系統。
User: LangGraph 是什麼?
AI: LangGraph 是一個讓開發者用流程圖方式設計 AI 應用的工具。
User: bye
AI: 感謝使用,祝您有美好的一天。

總結與延伸

LangGraph 為 LLM 應用程式帶來一個明確的結構化框架,讓我們能夠模組化管理多步驟流程、狀態記憶與條件判斷。相較於傳統方式,它更適合用來構建複雜、可維護的對話式 AI 應用。

延伸應用可以包括:

  • 整合 LangChain 工具(如:向量資料庫、搜尋引擎)
  • 建構具有分支與回饋機制的智能 Agent
  • 開發能根據上下文自我修正的 RAG 系統
  • 將整個 LangGraph 部署為 Web API 或背景工作流程

參考文件

  1. LangGraph: LangChain Agent 的殺手鐧 (入門)

useSWR 入門教學筆記:打造高效、簡潔的資料請求方式 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在現代前端開發中,資料的取得與管理是不可或缺的一環。傳統上,我們可能使用 useEffect 搭配 fetchaxios 來處理資料請求,但這樣的方式不僅冗長,還需要手動管理 loading、error 狀態與快取邏輯。為了解決這些問題,Vercel 推出的 SWR(stale-while-revalidate)提供了一種簡潔、聲明式且高效的資料取得方式,特別適合搭配 React 應用開發。

本文將介紹 SWR 的核心觀念、使用方式與基本範例,幫助我們快速上手並應用於實務開發中。


重點摘要

  • SWR 是什麼?

    • SWR 是由 Vercel 開發的 React Hooks 函式庫,提供資料快取與同步機制。
    • 名稱來自 HTTP 快取策略 “stale-while-revalidate”,意指:先顯示舊資料,再重新驗證更新資料
  • 為什麼要使用 SWR?

    • 自動處理資料快取與重新驗證。
    • 簡化資料請求邏輯,減少樣板程式碼。
    • 支援多種進階功能(錯誤重試、revalidate on focus、polling 等)。
  • 基本用法

    • 使用 useSWR(key, fetcher) 進行資料請求。
    • key:唯一識別資料來源的 key,通常為 API 路徑。
    • fetcher:資料請求函式,可使用 fetchaxios 實作。
  • 常見功能

    • isLoadingerror 狀態管理。
    • 自動重試與重新整理資料。
    • 快取與全域共用資料(Shared cache)。
    • 手動重新驗證資料(revalidate)。
    • 支援 SSR、Pagination、Mutation 等進階功能。

實際範例:取得 GitHub 使用者資料

1. 安裝 SWR

npm install swr
# 或使用 yarn
yarn add swr

2. 撰寫 fetcher 函式

// libs/fetcher.ts
export const fetcher = (url: string) => fetch(url).then((res) => res.json());

3. 在元件中使用 useSWR

// pages/UserProfile.tsx
import useSWR from 'swr';
import { fetcher } from '../libs/fetcher';

const UserProfile = () => {
const { data, error, isLoading } = useSWR('https://api.github.com/users/octocat', fetcher);

if (isLoading) return <div>載入中...</div>;
if (error) return <div>載入失敗:{error.message}</div>;

return (
<div>
<h1>{data.name}</h1>
<p>GitHub:{data.login}</p>
<p>Followers:{data.followers}</p>
<img src={data.avatar_url} width={100} />
</div>
);
};

export default UserProfile;

4. 手動重新驗證資料

const { data, mutate } = useSWR('/api/data', fetcher);

// 手動刷新資料
const handleRefresh = async () => {
await mutate();
};

5. 搭配條件式載入

const shouldFetch = userId !== null;

const { data } = useSWR(shouldFetch ? `/api/users/${userId}` : null, fetcher);

6. 自訂快取與設定

import useSWR from 'swr';

const { data, error } = useSWR('/api/data', fetcher, {
refreshInterval: 10000, // 每 10 秒重新抓資料
revalidateOnFocus: true, // 回到畫面時自動刷新
dedupingInterval: 5000, // 阻止過於頻繁的 API 請求
});

總結

SWR 提供了一種優雅、聲明式的方式來管理 React 應用中的資料請求與快取,不僅能有效簡化程式碼,還能提高使用者體驗與應用效能。其彈性與擴充性也適合應用於中大型專案中。

當我們熟悉了 SWR 的基本用法後,接下來也可以進一步探索以下功能:

  • Mutation API:用於資料寫入後手動更新快取。
  • 依賴 key 的動態載入:搭配 router 參數動態請求資料。
  • 全域快取策略自訂(SWRConfig):統一設定所有請求的行為。

透過 SWR,我們不再需要手動處理快取與副作用邏輯,只需專注於資料的呈現與邏輯本身,是開發現代 React 應用的絕佳利器。

參考文件

  1. React Hooks for Data Fetching

JavaScript 可選鏈接運算符(Optional Chaining)介紹與入門教學 | 學習筆記

· 閱讀時間約 5 分鐘
kdchang

前言

JavaScript 中,處理深層嵌套結構時,我們經常會遇到 nullundefined 的問題。例如,當我們需要訪問一個對象的屬性,而該屬性本身可能不存在時,傳統的做法會導致錯誤,這樣的情況會非常繁瑣。為了解決這個問題,JavaScript 引入了 可選鏈接運算符(Optional Chaining),簡化了屬性訪問過程,並防止了因為屬性為 nullundefined 造成的錯誤。

本文將詳細介紹可選鏈接運算符的概念、用法以及常見的實際應用場景。

1. 可選鏈接運算符的基本語法

可選鏈接運算符(?. 是 JavaScript 中一種新的語法,通過它我們可以安全地訪問對象的屬性,並且在中途如果遇到 nullundefined,就會停止執行並返回 undefined,而不是拋出錯誤。

基本語法結構如下:

object?.property
object?.[key]
object?.method()
  • object?.property:如果 objectnullundefined,則返回 undefined,否則返回對象的 property 屬性。
  • object?.[key]:這是動態屬性名的情況,與 object?.property 類似,當 key 是變數或表達式時,這種語法很有用。
  • object?.method():如果 objectmethodnullundefined,則返回 undefined,不會調用該方法。

2. 為什麼需要可選鏈接運算符?

在傳統 JavaScript 中,當我們處理嵌套對象的屬性時,若某個屬性不存在或是 nullundefined,我們會遇到錯誤。例如:

const user = {
name: 'Alice',
address: {
street: '123 Main St',
},
};

console.log(user.address.street); // "123 Main St"
console.log(user.phone.number); // TypeError: Cannot read property 'number' of undefined

在這個例子中,當我們嘗試訪問 user.phone.number 時,由於 phone 屬性不存在,會拋出錯誤。為了解決這個問題,通常我們需要進行多層檢查:

console.log(user && user.phone && user.phone.number); // undefined

這樣的寫法看起來雜亂,並且很難處理更深層次的嵌套。可選鏈接運算符解決了這個問題,使得代碼更加簡潔和安全。

3. 可選鏈接運算符的應用場景

3.1 訪問對象屬性

當我們需要訪問對象的某一層屬性時,如果中間層級的某個屬性為 nullundefined,那麼使用可選鏈接運算符就能防止錯誤的拋出。

const user = {
name: 'Alice',
address: {
street: '123 Main St',
},
};

console.log(user?.address?.street); // "123 Main St"
console.log(user?.phone?.number); // undefined

在這個例子中,user?.address?.street 會安全地返回 street 屬性,而 user?.phone?.number 會返回 undefined,因為 phone 屬性並不存在。

3.2 訪問數組元素

在操作數組時,如果我們想訪問某個索引的元素,也可以使用可選鏈接運算符來避免錯誤。

const array = [1, 2, 3];

console.log(array?.[1]); // 2
console.log(array?.[10]); // undefined

這裡,array?.[1] 會返回 2,而 array?.[10] 會返回 undefined,即使索引超出了數組的範圍。

3.3 調用對象方法

如果對象的方法不存在,使用可選鏈接運算符可以避免拋出錯誤,並且返回 undefined

const user = {
name: 'Alice',
greet() {
console.log('Hello!');
},
};

user?.greet(); // "Hello!"
user?.sayGoodbye(); // undefined

在這個例子中,user?.greet() 會調用 greet 方法並顯示 "Hello!",而 user?.sayGoodbye() 則返回 undefined,因為 sayGoodbye 方法不存在。

3.4 動態屬性名

可選鏈接運算符也支持用動態屬性名來訪問對象屬性,這在處理具有不確定屬性的對象時非常有用。

const user = {
name: 'Alice',
preferences: {
theme: 'dark',
},
};

const key = 'theme';
console.log(user?.preferences?.[key]); // "dark"

在這個例子中,key 是一個變量,表示要訪問的屬性名,user?.preferences?.[key] 可以安全地獲取 preferences 中的 theme 屬性。

4. 與傳統方法的比較

使用可選鏈接運算符,我們的代碼變得更加簡潔,減少了不必要的檢查。傳統的方式可能需要多次檢查對象的存在,才能安全地訪問某個屬性,而可選鏈接運算符讓這一過程變得直觀且易於維護。

傳統方法:

if (user && user.address && user.address.street) {
console.log(user.address.street);
}

使用可選鏈接運算符:

console.log(user?.address?.street);

5. 可選鏈接運算符與 null 合併運算符(??

可選鏈接運算符經常與 null 合併運算符(??)一起使用。?? 用來返回當前值是否為 nullundefined,如果是則返回其右側的值,否則返回當前值。

const user = null;
const name = user?.name ?? 'Default Name';
console.log(name); // "Default Name"

在這裡,user?.name 會返回 undefined,因為 usernull,而 ?? 會將其替換為 'Default Name'

6. 總結

可選鏈接運算符(?.)是 JavaScript 中非常實用的一個特性,它簡化了嵌套對象屬性訪問的邏輯,避免了 nullundefined 帶來的錯誤,使代碼更加簡潔且容易理解。無論是在處理複雜的 API 返回數據還是操作動態結構的對象時,可選鏈接運算符都能發揮重要作用。在日常開發中,我們可以利用它來編寫更健壯、可讀性更強的代碼。

React Context API 入門教學 | 學習筆記

· 閱讀時間約 5 分鐘
kdchang

React Context API 是 React 提供的一種方式,讓我們能夠在組件樹中傳遞資料,而不需要一層層地使用 props。Context API 可以解決多層嵌套組件的傳遞問題,讓我們在深層組件中輕鬆訪問到全局狀態。本文將介紹如何使用 React Context API,並提供一個簡單的範例來展示其實際應用。

什麼是 React Context API

React Context API 是 React 的一個內建功能,它可以讓我們在組件樹中共享資料,避免多層嵌套的 props 傳遞。Context 主要由三個部分組成:

  1. React.createContext():創建一個 Context 物件。
  2. Provider:這是 Context API 中的一個組件,它用來包裹整個應用,並提供一個全局的資料源。
  3. Consumer:這是用來訪問 Context 資料的組件,它能夠獲取 Provider 中傳遞的資料。

使用 Context 的目的,是為了避免將相同的資料層層傳遞到每個組件,這樣可以讓應用的資料流變得更加簡潔。

使用 Context API 的步驟

步驟 1: 創建 Context

首先,我們需要使用 React.createContext() 來創建一個 Context 物件。這個物件會返回一個 ProviderConsumer 組件,讓我們在應用中使用。

import React from 'react';

// 創建 Context
const MyContext = React.createContext();

步驟 2: 使用 Provider 來傳遞資料

Context 的 Provider 是用來包裹應用的,它會接收一個 value 屬性,這個屬性就是要共享給整個組件樹的資料。

const App = () => {
const [user, setUser] = React.useState({ name: 'John', age: 30 });

return (
<MyContext.Provider value={user}>
<UserProfile />
</MyContext.Provider>
);
};

在這個範例中,我們將一個 user 物件傳遞給 MyContext.Providervalue 屬性,這樣整個組件樹中的所有子組件都能夠訪問到這個 user 資料。

步驟 3: 使用 Consumer 來接收資料

在需要使用資料的地方,我們可以使用 MyContext.Consumer 來獲取資料。Consumerchildren 是一個函數,它會接收一個 value 參數,這個參數就是在 Provider 中傳遞的資料。

const UserProfile = () => {
return (
<MyContext.Consumer>
{(user) => (
<div>
<h1>{user.name}</h1>
<p>Age: {user.age}</p>
</div>
)}
</MyContext.Consumer>
);
};

在這個範例中,UserProfile 組件通過 Consumer 來訪問 MyContext 中的 user 資料,並渲染顯示用戶的名字和年齡。

步驟 4: 使用 useContext Hook (React 16.8 及以上)

React 16.8 引入了 useContext Hook,這樣我們可以更方便地在函數組件中使用 Context,而不需要使用 Consumer。這樣的寫法更加簡潔,並且避免了過多的嵌套。

import React, { useContext } from 'react';

const UserProfile = () => {
const user = useContext(MyContext);

return (
<div>
<h1>{user.name}</h1>
<p>Age: {user.age}</p>
</div>
);
};

使用 useContext 可以直接從 Context 中獲取資料,而不需要使用 Consumer。這使得代碼更簡潔,並提高了可讀性。

實際範例

下面是一個完整的範例,展示了如何使用 React Context API 來管理應用中的全局狀態。這個範例將包括一個用戶資料的管理,並能夠在多個組件中共享這些資料。

import React, { useState, useContext } from 'react';

// 創建 Context
const MyContext = React.createContext();

const App = () => {
const [user, setUser] = useState({ name: 'John', age: 30 });

return (
<MyContext.Provider value={user}>
<div>
<UserProfile />
<AgeUpdater />
</div>
</MyContext.Provider>
);
};

const UserProfile = () => {
const user = useContext(MyContext);

return (
<div>
<h1>{user.name}</h1>
<p>Age: {user.age}</p>
</div>
);
};

const AgeUpdater = () => {
const user = useContext(MyContext);
const setUser = useState()[1];

const updateAge = () => {
setUser({ ...user, age: user.age + 1 });
};

return (
<div>
<button onClick={updateAge}>Increase Age</button>
</div>
);
};

export default App;

範例解析

  1. App 組件:在 App 組件中,我們使用 useState 定義了一個 user 資料,並通過 MyContext.Provider 將資料提供給下層組件。
  2. UserProfile 組件UserProfile 使用 useContext 來讀取 MyContext 中的資料,並顯示用戶的名字和年齡。
  3. AgeUpdater 組件:這個組件同樣使用 useContext 來讀取和更新 user 資料。我們在這裡定義了一個按鈕,當按下時,會更新 user 的年齡。

Context API 的優缺點

優點:

  1. 簡化資料傳遞:當我們需要在多層嵌套的組件中共享資料時,使用 Context 可以避免繁瑣的 props 傳遞。
  2. 可擴展性:Context 非常適合用於應用中的全局狀態管理,像是用戶認證、語言設置、主題樣式等。

缺點:

  1. 重新渲染問題:當 Provider 中的資料變更時,所有使用該 Context 的組件都會重新渲染。對於大型應用來說,這可能會影響性能。
  2. 狀態過度共享:Context 主要用於共享全局資料,如果將太多不相關的資料放入同一個 Context,可能會使代碼變得難以維護。

總結

React Context API 是一個強大的工具,可以幫助我們管理應用中的全局狀態。在適當的情況下使用 Context 可以大大簡化代碼,避免深層嵌套的 props 傳遞。但也需要謹慎使用,避免過多不必要的資料共享,從而影響性能和可維護性。在開發中,我們可以根據具體需求來選擇是否使用 Context API,並搭配其他狀態管理工具(如 Redux 或 Zustand)來管理更複雜的應用狀態。

參考文件

  1. 用 React Context API 實作跨組件傳值的功能

React useMemo 與 useCallback 差異介紹與入門教學 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

在使用 React hooks 進行開發時,useMemouseCallback 是兩個常被提及的性能優化工具。它們都屬於 記憶化(memoization) 技術,用來避免不必要的重算與重渲染。然而,很多初學者在理解這兩者的用途與差異時常感到困惑。這篇文章將從概念出發,並搭配實際範例,幫助你掌握 useMemouseCallback 的核心用途與實作方式。


一、共通點:記憶化

React 在每次組件渲染時,預設會重新執行所有函式與表達式。當某些值(如計算結果、函式)在依賴未改變的情況下不需要重新產生,我們可以利用記憶化來優化效能。

這就是 useMemouseCallback 的主要功能:根據依賴陣列(dependency array)決定是否重建值或函式


二、差異概念總覽

Hook主要用途回傳內容使用時機
useMemo記憶「計算結果」任意值計算過程昂貴,避免重複運算
useCallback記憶「函式定義」函式傳遞函式給子元件,避免不必要的 re-render

換句話說:

  • useMemo(fn, deps)const value = memoized(fn)
  • useCallback(fn, deps)const callback = memoized(() => fn)

三、useMemo 範例

假設我們有一個需要進行繁重計算的函式,例如統計某個資料集合中的數值:

import React, { useState, useMemo } from 'react';

function slowFunction(num) {
console.log('Running slow function');
let result = 0;
for (let i = 0; i < 1e7; i++) {
result += num;
}
return result;
}

function ExpensiveComponent() {
const [count, setCount] = useState(0);
const [input, setInput] = useState(1);

const computedValue = useMemo(() => {
return slowFunction(input);
}, [input]);

return (
<div>
<h2>Expensive Calculation</h2>
<p>計算結果:{computedValue}</p>
<input
type="number"
value={input}
onChange={e => setInput(Number(e.target.value))}
/>
<button onClick={() => setCount(c => c + 1)}>重新渲染 ({count})</button>
</div>
);
}

在這個例子中,若不使用 useMemo,只要任何 state 改變(例如點擊按鈕改變 count),整個組件都會重新執行 slowFunction,導致效能問題。透過 useMemo,只有 input 改變時才會重新計算,其他情況會重複使用上次的計算結果。


四、useCallback 範例

有時候我們會將函式作為 props 傳遞給子元件。如果每次重新 render 都產生新的函式實例,會導致子元件誤以為 props 改變,而重新渲染。這時就可以用 useCallback

import React, { useState, useCallback, memo } from 'react';

const ChildButton = memo(({ onClick }) => {
console.log('ChildButton rendered');
return <button onClick={onClick}>點我</button>;
});

function ParentComponent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);

const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);

return (
<div>
<h2>useCallback Demo</h2>
<p>Count: {count}</p>
<ChildButton onClick={handleClick} />
<button onClick={() => setOther(o => o + 1)}>改變其他狀態</button>
</div>
);
}

在這個範例中,ChildButton 是經過 memo 包裹的元件,只有在 props.onClick 改變時才會重新渲染。使用 useCallback 確保 handleClick 函式在 [](無依賴)下只會創建一次,即使 other 改變,ChildButton 也不會重新渲染。


五、常見錯誤與注意事項

  1. 過度使用會反效果
    useMemouseCallback 本身也有記憶成本,不建議過度使用。只有在你確定函式或運算昂貴,或造成子元件重 render 才需要用。

  2. 依賴陣列要正確
    記得將函式中引用的變數正確加入依賴陣列中,否則會造成記憶結果與預期不符。

  3. 搭配 React.memo 效果更明顯
    useCallback 通常與 memoPureComponent 搭配,否則即使函式地址一樣,也無法避免重 render。


六、總結

當你在開發中遇到效能瓶頸或元件不必要地重複渲染時,才是使用 useMemouseCallback 的好時機。舉例來說:

  • 在表格過濾、排序等涉及大量資料處理的畫面中,可以用 useMemo 優化計算。
  • 在表單中將函式傳遞給 input 元件時,使用 useCallback 可避免整個表單重 render。

記住一個原則:不要為了使用 hook 而使用,而是根據實際效能需求進行優化。如果你的應用很小或尚未遇到效能問題,先專注於撰寫可讀性高、邏輯清楚的程式碼,這才是最重要的。

參考文件

  1. 如何優化 React 元件的渲染效能,並避免渲染陷阱

JavaScript 模組系統:CommonJS 與 AMD 入門教學 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

前言

在 JavaScript 早期,所有程式碼通常寫在單一文件中,這樣的方式在小型專案中或許可行,但當應用程式變得更大、更複雜時,這種結構會導致管理困難。因此,模組化的概念被引入,允許開發者將程式碼拆分成可重複使用的獨立部分,提高可維護性與擴展性。

在 ES6 標準推出之前,JavaScript 主要依賴 CommonJS(CJS)Asynchronous Module Definition(AMD) 來實現模組化。這兩種模組系統有不同的設計理念與應用場景,以下將詳細介紹其特性與實作方式。


1. CommonJS(CJS)—— Node.js 的標準模組系統

概述

CommonJSNode.js 所採用,主要用於伺服器端開發。它的核心概念是 同步載入(Synchronous Loading),這意味著模組在執行時會逐步載入,而不是並行載入。

CommonJS 主要透過 require() 來載入模組,並使用 module.exportsexports 來匯出模組內容。

CommonJS 語法

(1) 定義模組(匯出)

// math.js
const pi = 3.14159;

function add(a, b) {
return a + b;
}

function subtract(a, b) {
return a - b;
}

// 匯出模組
module.exports = {
pi,
add,
subtract
};

(2) 使用模組(載入)

// main.js
const math = require('./math');

console.log(math.pi); // 3.14159
console.log(math.add(10, 5)); // 15
console.log(math.subtract(20, 8)); // 12

CommonJS 特點

優點: 適用於伺服器端(Node.js),可輕鬆管理模組與依賴。
簡單直覺的 require()module.exports 語法
支援循環依賴(Circular Dependencies),當兩個模組互相依賴時,仍能正確解析。

缺點: 同步載入(Synchronous Loading),不適合瀏覽器端,因為會阻塞執行緒,影響頁面效能。
不支援瀏覽器環境,需使用 Webpack 或 Browserify 來轉換為瀏覽器可用的程式碼。


2. AMD(Asynchronous Module Definition)—— 適用於瀏覽器的模組系統

概述

AMD 是專為 瀏覽器環境 設計的模組系統,解決了 CommonJS 無法在前端環境直接運作的問題。AMD 的關鍵特點是 非同步載入(Asynchronous Loading),允許模組在需要時才載入,避免影響頁面效能。

AMD 主要使用 define() 來定義模組,require() 來載入模組。

AMD 語法

(1) 定義模組(匯出)

// math.js
define([], function () {
const pi = 3.14159;

function add(a, b) {
return a + b;
}

function subtract(a, b) {
return a - b;
}

return {
pi,
add,
subtract
};
});

(2) 使用模組(載入)

// main.js
require(['math'], function (math) {
console.log(math.pi); // 3.14159
console.log(math.add(5, 6)); // 11
});

AMD 特點

優點: 適用於瀏覽器環境,支援非同步載入,提高效能。
使用 define()require() 來管理模組,能夠載入多個依賴。
非同步執行,適合大型應用程式,減少載入時間。

缺點: 語法較繁瑣,比 CommonJS 需要更多設定。
需要 RequireJS 來執行,瀏覽器無法直接支援 AMD。


3. CommonJS vs AMD vs ES Modules(ESM)

特性CommonJS(CJS)AMDES Modules(ESM)
適用環境Node.js瀏覽器瀏覽器 & Node.js
載入方式require()require()import/export
同步/非同步同步(Synchronous)非同步(Asynchronous)靜態解析(Static)
優勢簡單易用,適合伺服器端適用瀏覽器,非同步載入現代標準,支援 Tree Shaking
限制不適合瀏覽器需要 RequireJS需要 ES6 瀏覽器或 Node.js 12+

4. CommonJS 與 AMD 的使用時機

  • 當開發伺服器端應用程式時,建議使用 CommonJS,因為它與 Node.js 相容性最佳。
  • 當開發前端應用時,AMD 是一種選擇,但目前更推薦使用 ES Modules(ESM)
  • 現代 JavaScript 建議使用 ES Modules(import/export),因為它已經成為標準,並且同時支援瀏覽器與 Node.js 環境。

5. 結論

在 JavaScript 模組化的歷史發展中,CommonJS 被廣泛用於 伺服器端,而 AMD 則主要針對 瀏覽器環境 設計。隨著 ES6 的 ES Modules(ESM)標準化,許多開發者已經轉向 ESM,因為它在語法上更直覺,並且可以同時適用於前端與後端。

雖然 CommonJS 和 AMD 仍然在某些專案中使用,但未來趨勢將逐漸轉向 ES Modules。因此,對於新專案,建議 優先使用 ES Modules(import/export),而對於舊專案或特定環境(如 Node.js 早期版本),仍可能需要使用 CommonJS 或 AMD。

JavaScript 模組(Module)入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

1. 什麼是 JavaScript 模組?

JavaScript 模組(Module)是一種將程式碼拆分成多個獨立文件,並在不同文件間共享和管理程式碼的方式。透過模組化的設計,可以讓程式碼更具結構性、可讀性與可維護性。

在 ES6(ECMAScript 2015)之前,JavaScript 主要透過 IIFE(立即執行函式)、CommonJS 或 AMD 來模組化程式碼。而 ES6 之後,JavaScript 原生支援 ES Modules(ESM),提供 importexport 來管理模組。


2. 為什麼需要模組?

  1. 避免全域變數污染
    • 模組能夠封裝變數,避免不同程式碼區塊互相影響。
  2. 提高可維護性
    • 讓程式碼結構更清晰,拆分不同的功能至獨立文件中。
  3. 支援程式碼重用
    • 可在多個專案中共享相同的模組,避免重複開發。
  4. 支援延遲載入(Lazy Loading)
    • 透過動態 import(),按需載入模組,提高效能。

3. ES6 模組語法

在 ES6 中,我們主要使用 exportimport 來定義和載入模組。

(1) export 的使用

命名匯出(Named Export)

// math.js
export const pi = 3.14159;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}

預設匯出(Default Export)

// greeting.js
export default function sayHello(name) {
return `Hello, ${name}!`;
}

(2) import 的使用

匯入命名匯出

// main.js
import { pi, add, subtract } from "./math.js";

console.log(pi); // 3.14159
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6

匯入預設匯出

// main.js
import sayHello from "./greeting.js";

console.log(sayHello("Alice")); // "Hello, Alice!"

匯入所有模組內容

// main.js
import * as math from "./math.js";

console.log(math.pi); // 3.14159
console.log(math.add(2, 3)); // 5

使用 as 重新命名

import { add as sum, subtract as minus } from "./math.js";

console.log(sum(10, 5)); // 15
console.log(minus(10, 5)); // 5

4. ES 模組的特性

  1. 靜態解析(Static Analysis)

    • importexport 必須在頂層作用域,不能在條件語句或函式內部。
    • 在編譯時(compile time)解析模組,而不是執行時(runtime)。
  2. 模組作用域

    • 每個模組都有自己的作用域,變數不會污染全域作用域。
  3. 自動使用嚴格模式(Strict Mode)

    • ES6 模組內部自動啟用 "use strict",無需手動指定。

5. 動態載入模組

有時候我們希望在特定條件下載入模組,而不是在程式開始時就載入所有模組。這時可以使用 import() 來動態載入。

if (true) {
import("./math.js").then((math) => {
console.log(math.add(5, 10)); // 15
});
}
  • import() 回傳一個 Promise,當模組載入完成後執行回調函式。
  • 這種方式適合懶加載(Lazy Loading)與條件性載入。

6. varlet 在模組中的行為

在模組內,變數 var 仍然會被提升(Hoisting),但 letconst 具有區塊作用域。

// module.js
var globalVar = "I am global";
let localVar = "I am local";
// main.js
import "./module.js";

console.log(globalVar); // "I am global" (因為 var 會提升到全域)
console.log(localVar); // ReferenceError: localVar is not defined

7. 在瀏覽器與 Node.js 環境使用 ES 模組

(1) 瀏覽器

在 HTML 文件中,使用 <script type="module"> 來載入 ES6 模組。

<script type="module">
import { add } from "./math.js";
console.log(add(10, 5));
</script>

(2) Node.js

Node.js 14+ 版本支援 ES 模組,但需要:

  • 檔案副檔名改為 .mjs
  • package.json 設定 "type": "module"
{
"type": "module"
}
// math.mjs
export function multiply(a, b) {
return a * b;
}
// main.mjs
import { multiply } from "./math.mjs";
console.log(multiply(4, 5)); // 20

8. 模組引入方式整理

環境引入方式
瀏覽器(ESM)<script type="module">
Node.js(ESM)import { foo } from './module.mjs'
Node.js(CommonJS)const foo = require('./module.js')
動態載入(Lazy Load)import('./module.js').then(...)
重新命名import { foo as newFoo } from './module.js'
匯入所有內容import * as mod from './module.js'

9. 結論

  1. ES 模組是 JavaScript 原生模組系統,使用 importexport 來管理程式碼。
  2. 模組有助於提升可讀性與可維護性,避免全域變數污染。
  3. 動態載入(import())可以優化效能,適合延遲載入模組。
  4. 瀏覽器與 Node.js 都支援 ES6 模組,但 Node.js 需要 .mjspackage.json 設定 "type": "module"
  5. 模組可以透過不同方式引入,根據環境選擇適合的方法。

掌握 JavaScript 模組的概念,能夠讓你更有效地開發與維護大型專案。

MongoDB 入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

前言

在現代應用程式開發中,資料儲存已不再侷限於傳統的關聯式資料庫(如 MySQL、PostgreSQL)。特別是在處理非結構化資料、需要高延展性或頻繁 schema 變動的應用場景中,NoSQL 資料庫逐漸成為主流選擇。

其中,MongoDB 是最受歡迎的 NoSQL 資料庫之一。它採用文件型(Document-Oriented)結構,使用 JSON 類型格式(實際為 BSON)儲存資料,讓開發者能更靈活地設計資料模型與操作資料。MongoDB 強調可擴展性、彈性資料結構與高效查詢能力,廣泛應用於 Web 開發、物聯網、大數據處理等領域。


重點摘要

  • MongoDB 是什麼?

    • 開源的 NoSQL 文件資料庫,使用 BSON 格式儲存資料。
    • 資料以「資料庫 → 集合(Collection)→ 文件(Document)」的層級組織。
    • 每個文件(Document)類似於 JSON 結構,支援巢狀資料與陣列。
  • 主要特性

    • 文件型資料儲存(更彈性且接近開發者熟悉的物件結構)
    • 無需預先定義 Schema,可動態變更欄位
    • 垂直與水平延展能力佳
    • 提供複寫與分片支援(Replica Set、Sharding)
    • 強大的查詢語言,支援索引、聚合、全文搜尋
  • 應用場景

    • RESTful API 後端儲存(如 Node.js + Express 專案)
    • 快速原型設計與資料模型測試
    • 高並發讀寫需求(例如留言板、商品評論系統)
    • 資料格式變動頻繁的場景(如 IoT 裝置紀錄)

安裝與啟動

1. 安裝 MongoDB(本機)

Mac 使用者(使用 Homebrew):

brew tap mongodb/brew
brew install mongodb-community@7.0
brew services start mongodb/brew/mongodb-community

Windows / Linux: 可前往 https://www.mongodb.com/try/download/community 下載對應版本。

2. 啟動 MongoDB

mongod

啟動成功後,預設會在 mongodb://localhost:27017 提供本地服務。

3. 開啟 Mongo Shell(或使用 MongoDB Compass GUI)

mongosh

進入後會看到互動式 shell 環境,開始操作你的資料庫。


MongoDB 基本操作(Shell 範例)

1. 建立 / 切換資料庫

use blog

2. 建立集合(Collection)與新增文件(Document)

db.posts.insertOne({
title: 'MongoDB 入門教學',
author: 'KD',
tags: ['database', 'nosql', 'mongodb'],
published: true,
created_at: new Date(),
});

插入文件時自動建立集合與資料庫。

3. 查詢文件

db.posts.find();
db.posts.find({ author: 'KD' });
db.posts.findOne({ published: true });

支援條件、邏輯查詢、排序、分頁等功能:

db.posts.find({ published: true }).sort({ created_at: -1 }).limit(5);

4. 更新文件

db.posts.updateOne({ title: 'MongoDB 入門教學' }, { $set: { published: false } });

支援 $set, $inc, $push, $unset 等更新操作符。

5. 刪除文件

db.posts.deleteOne({ title: 'MongoDB 入門教學' });

使用 Mongoose 操作(Node.js 範例)

在 Node.js 專案中,常使用 mongoose 封裝操作 MongoDB。

1. 安裝套件

npm install mongoose

2. 建立連線與定義模型

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/blog');

const postSchema = new mongoose.Schema({
title: String,
author: String,
tags: [String],
published: Boolean,
created_at: { type: Date, default: Date.now },
});

const Post = mongoose.model('Post', postSchema);

3. 實際使用範例

新增資料:

const newPost = new Post({
title: '用 Node.js 操作 MongoDB',
author: 'KD',
tags: ['nodejs', 'mongodb'],
published: true,
});

await newPost.save();

查詢資料:

const posts = await Post.find({ published: true }).limit(5);

更新資料:

await Post.updateOne({ title: '用 Node.js 操作 MongoDB' }, { published: false });

刪除資料:

await Post.deleteOne({ title: '用 Node.js 操作 MongoDB' });

聚合(Aggregation)入門

MongoDB 提供強大的 Aggregation Pipeline 功能,可進行統計、分組、轉換。

範例:統計作者貼文數量

db.posts.aggregate([{ $group: { _id: '$author', count: { $sum: 1 } } }, { $sort: { count: -1 } }]);

總結

MongoDB 以其彈性、易用與高延展性,成為許多現代應用的首選資料庫,特別是在快速開發、微服務架構或大數據處理場景中表現優異。透過簡單的 JSON 結構與強大的查詢能力,即使不熟 SQL 的開發者也能快速上手,打造穩定且具擴展性的資料儲存系統。

初學者可先從基本的增刪查改練習起,逐步熟悉資料結構與聚合操作,再延伸到使用 Mongoose 開發 REST API,或搭配 GraphQL、Next.js 等前後端整合工具,深入打造現代 Web 應用。