跳至主要内容

JWT 的實作安全策略與 Refresh Token 安全做法入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

一、JWT 基本安全策略

1. 使用強密鑰(Secret)或非對稱加密

  • 對稱加密:使用 HS256 時,secret 應具備高強度(建議 256-bit 以上)。
  • 非對稱加密:使用 RS256 時,私鑰與公鑰分離,便於服務間驗證。

2. 設定有效期限(exp)

  • Access Token 建議設為 短效(10~30 分鐘)
  • 避免永久有效的 Token,防止被盜用後長期濫用。

3. 限制 Token 權限與資訊

  • Token 中只放必要資訊(例如:userIdrole)。
  • 不要放敏感資料(如密碼、信用卡、住址等)。

4. 透過 HTTPS 傳遞 JWT

  • 絕對 禁止在 HTTP 傳輸 JWT,避免中間人攻擊(MITM)。
  • 所有 API 傳遞 token(包括 Refresh Token)都必須走 HTTPS。

5. 防範 XSS

  • 若將 accessToken 存放於 localStorage,需格外注意防止前端被注入腳本。
  • 建議使用 HttpOnly cookie 儲存 Refresh Token(無法被 JavaScript 存取)。

二、為什麼要使用 Refresh Token?

Access Token 有效期短,提高安全性,但使用者不希望頻繁重新登入,這時就用 Refresh Token 來「悄悄地」幫用戶更新 Access Token。


三、JWT + Refresh Token 實作架構

          +-------------+                      +----------------+
| 前端客戶端 | | 後端伺服器 |
+-------------+ +----------------+
| |
|--------> /login (帳密登入) --------->|
| |
|<----- accessToken + refreshToken -----|
| |
| 使用 accessToken 存取受保護資源 |
|--------> /api/profile ------------->|
| |
accessToken 過期 ↓ |
|--------> /refresh (更新 token) ------>|
|<------- 新的 accessToken -------------|

四、最佳實作策略詳解

1. accessToken 存放位置

  • 可存在記憶體(例如 Redux、React Context)或 localStorage
  • 優點:操作方便
  • 缺點:暴露於 XSS 攻擊風險

2. refreshToken 存放位置

  • 建議儲存在 HttpOnly、Secure 的 Cookie 中:
Set-Cookie: refreshToken=xxxxx; HttpOnly; Secure; SameSite=Strict; Path=/refresh
  • 前端 JavaScript 無法讀取,降低風險
  • 伺服器可以從 cookie 中自動取出 refreshToken

3. 建立 /refresh API(專責刷新 token)

// POST /refresh
import jwt from 'jsonwebtoken';

router.post('/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;

if (!refreshToken) return res.status(401).json({ error: '無 refresh token' });

try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);

const newAccessToken = jwt.sign(
{ userId: decoded.userId, username: decoded.username },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);

res.json({ accessToken: newAccessToken });
} catch (err) {
return res.status(403).json({ error: 'Refresh token 無效或過期' });
}
});

4. 可選:Refresh Token 儲存機制(實現強制登出與黑名單)

  • 資料庫方案(建議):

    • 每次登入時產生唯一的 refreshToken 並儲存於 DB
    • 使用時驗證是否仍存在
    • 登出或異常行為時移除對應的 refreshToken
  • Redis 快取黑名單方案(進階):

    • 遇到帳號盜用或封鎖時,將 token 加入黑名單清單,後續驗證可即時攔截

5. 登出處理(安全地移除 token)

router.post('/logout', (req, res) => {
res.clearCookie('refreshToken', { path: '/refresh' });
res.json({ message: '已成功登出' });
});

五、常見攻擊防護建議

威脅類型防護方法
XSS不使用 localStorage 儲存 refresh token,輸出內容時做 XSS 過濾
CSRF使用 SameSite=Strict 的 cookie,或搭配 CSRF Token
Token 竄改使用強加密簽章(如 HS256/RS256)並驗證
重放攻擊設定合理 exp 過期時間並結合裝置驗證(如 IP / UA)
Token 外洩refreshToken 實作多設備登入管理與黑名單機制

六、小結

元素實作策略優點
Access Token存放在記憶體或 localStorage快速存取,搭配短效時安全性高
Refresh Token存放於 HttpOnly Cookie避免 JS 存取,提高防護能力
/refresh API驗證 refreshToken 並產生新 accessToken提升使用者體驗與系統可用性
黑名單策略儲存 refreshToken 資訊於資料庫支援強制登出與封鎖帳號

七、額外建議

  • 單一使用者只能同時登入 N 台裝置,可依照 refreshToken 綁定設備資訊。
  • 將 JWT 實作與中介層(middleware)封裝好,避免開發重複邏輯。
  • 若你使用的是框架如 Next.js、NestJS、Spring Boot,也有內建 JWT 與 Refresh 模組可以善用。

參考文件

  1. 官方文件

Prisma ORM 入門教學筆記 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在現代 Web 應用中,資料庫與後端邏輯的整合至關重要,而 ORM(Object Relational Mapping)工具正好扮演了簡化資料操作、提升開發效率的角色。Prisma 是一個現代化的 TypeScript ORM,具有直觀的語法、自動型別生成、遷移管理與強大的查詢功能,深受開發者喜愛。

本教學將帶你從安裝 Prisma、設定 schema、執行 migration、撰寫查詢等一步步建立對 Prisma 的基本理解。


重點摘要

  • Prisma 是一套現代化的 Node.js ORM 工具,支援 PostgreSQL、MySQL、SQLite、SQL Server、MongoDB。
  • 使用 Prisma Schema 語言定義資料模型,並自動生成型別與查詢方法。
  • 透過 CLI 工具可快速進行資料庫遷移與型別同步。
  • 與 TypeScript 深度整合,享有 IDE 型別提示與靜態檢查。
  • Prisma Client 是用來查詢資料庫的自動產生型別安全工具。
  • 支援 transaction、raw SQL、relation 查詢等進階功能。

快速安裝與初始化

1. 初始化專案

mkdir my-prisma-app && cd my-prisma-app
npm init -y
npm install prisma --save-dev
npx prisma init

上述指令會建立以下結構:

.
├── prisma/
│ └── schema.prisma // 資料模型定義
└── .env // 資料庫連線字串

2. 設定資料庫

以 PostgreSQL 為例,在 .env 中加入:

DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

然後修改 prisma/schema.prisma 檔案:

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

定義資料模型

以下是一個簡單的 UserPost 的關聯模型範例:

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}

建立資料庫與生成 Prisma Client

npx prisma migrate dev --name init

這會:

  • 根據 schema 建立 migration SQL
  • 自動套用到資料庫
  • 產生型別安全的 Prisma Client

使用 Prisma Client 查詢資料

安裝並匯入 Prisma Client

npm install @prisma/client
// src/index.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

新增資料

const user = await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
posts: {
create: {
title: 'Hello World',
content: 'This is my first post.',
},
},
},
});
console.log(user);

查詢資料

const allUsers = await prisma.user.findMany({
include: {
posts: true,
},
});
console.dir(allUsers, { depth: null });

更新資料

await prisma.post.update({
where: { id: 1 },
data: { published: true },
});

刪除資料

await prisma.post.delete({
where: { id: 1 },
});

進階功能介紹

  • Relation 查詢:支援巢狀查詢與 lazy loading。
  • Transaction:支援多筆操作的交易一致性。
  • Raw SQL:如需彈性操作,可透過 prisma.$queryRaw 撰寫 SQL。
  • Prisma Studio:可視化資料庫管理 UI,透過 npx prisma studio 啟動。

總結

Prisma 是一套設計現代化、對開發者友善的 ORM 工具,特別適合搭配 TypeScript 開發的應用。它不僅簡化了資料庫操作流程,還提升了開發與除錯效率。無論你是從 Sequelize、TypeORM 轉移而來,或是初次使用 ORM,都能快速上手 Prisma 並有效管理你的資料層邏輯。

參考文件

  1. 官方文件

React Query 入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

前言

隨著前端應用程式越來越複雜,資料的取得、快取、同步更新以及狀態管理成為重要挑戰。傳統使用 useEffect 搭配 fetchaxios 取得資料,會讓程式碼變得冗長且難以維護,特別是需要處理快取、錯誤重試、背景重新整理(refetch)等功能時。

React Query 是一個強大的資料同步庫,它抽象並管理伺服器狀態(server state),讓你能輕鬆完成資料抓取、快取、更新與錯誤處理。透過簡潔的 API 和自動化行為,讓前端開發者能專注於業務邏輯,而非繁瑣的資料管理。

本篇教學將介紹 React Query 的核心概念,並提供簡單的實作範例,讓你快速理解如何在 React 專案中有效率且方便地使用 React Query。


重點摘要

  • React Query 是什麼? 專注於伺服器狀態管理的資料同步庫,提供資料取得、快取、自動重試、背景更新、分頁等功能。

  • 伺服器狀態(Server State) vs 本地狀態(Local State) React Query 管理的是伺服器端資料,不同於用 React 的 useState 管理元件內部的本地狀態。

  • 核心 Hook:useQuery 用來抓取和快取資料,接收一個 key 和一個 fetcher 函式,會自動處理 loading、error、資料快取。

  • 資料快取與自動同步 React Query 會自動快取請求結果,並依設定自動重新抓取,保持資料最新。

  • 背景重新整理(Refetching) 可設定在視窗獲得焦點時自動刷新資料,確保資料同步。

  • 錯誤重試機制 請求失敗時可自動重試,避免因網路波動導致資料錯誤。

  • useMutation 用於資料變更(新增、修改、刪除)操作,並支援變更後自動重新整理相關快取。

  • 全域 Query Client 使用 QueryClient 管理所有查詢狀態,需用 QueryClientProvider 包裹應用程式。

  • 方便整合各種請求函式庫 可搭配 fetchaxios 等任意資料請求方式。


實際範例

1. 安裝

npm install @tanstack/react-query

yarn add @tanstack/react-query

2. 基本設定

在 React 應用的根元件設定 QueryClientQueryClientProvider

import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
return (
<QueryClientProvider client={queryClient}>
<YourComponent />
</QueryClientProvider>
);
}

export default App;

3. 使用 useQuery 抓取資料

import React from 'react';
import { useQuery } from '@tanstack/react-query';

// 一個模擬的 fetch 函式,從 API 取得使用者清單
async function fetchUsers() {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
}

function UserList() {
// 使用 useQuery,第一參數為 key,第二參數為 fetch 函式
const { data, error, isLoading, isError } = useQuery(['users'], fetchUsers);

if (isLoading) return <div>載入中...</div>;

if (isError) return <div>錯誤:{error.message}</div>;

return (
<ul>
{data.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
);
}

export default UserList;

4. 使用 useMutation 進行資料更新

假設有一個新增使用者的 API,使用 useMutation 處理:

import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';

async function addUser(newUser) {
const res = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
body: JSON.stringify(newUser),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) throw new Error('新增失敗');
return res.json();
}

function AddUserForm() {
const [name, setName] = useState('');
const queryClient = useQueryClient();

const mutation = useMutation(addUser, {
onSuccess: () => {
// 新增成功後,重新整理 users 快取
queryClient.invalidateQueries(['users']);
},
});

const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({ name });
};

return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="使用者名稱" />
<button type="submit">新增使用者</button>
{mutation.isLoading && <p>新增中...</p>}
{mutation.isError && <p>錯誤:{mutation.error.message}</p>}
{mutation.isSuccess && <p>新增成功!</p>}
</form>
);
}

export default AddUserForm;

小結

React Query 提供一套完整且簡潔的 API 來管理伺服器狀態,包含資料取得、快取、錯誤處理與背景更新。透過 useQueryuseMutation,你能輕鬆地處理資料的讀取與更新,讓 React 應用程式更穩定且易維護。

建議在開發中逐步導入 React Query,取代傳統的 useEffect + fetch 模式,尤其是對於複雜的資料流與同步需求,能大幅提升開發效率與使用者體驗。

參考文件

  1. React Query 文件

使用 ESM 的 Swagger 入門教學(Node.js + Express) | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在現代後端開發中,撰寫標準化的 API 文件是必要的工作。Swagger(OpenAPI)讓我們可以定義並分享 API 結構,也支援透過 UI 測試 API。本文將示範如何在 ESM 模組架構的 Node.js 專案中 整合 Swagger,實現互動式 API 文件。


重點摘要

  • 使用 swagger-jsdoc 定義 API 文件(支援 YAML 格式註解)。
  • 使用 swagger-ui-express 提供互動式 Swagger UI 頁面。
  • 採用 ES Module(import / export)語法整合。
  • 使用 JSDoc 註解方式撰寫 API 描述。
  • Swagger 文件會自動從註解中產生,不需手動維護 JSON/YAML 文件。

安裝套件

npm install express swagger-jsdoc swagger-ui-express

並在 package.json 中加入:

{
"type": "module"
}

專案結構

project/
├── index.js
├── swagger.js
└── routes/
└── todos.js

1. 建立 swagger.js

// swagger.js
import swaggerJSDoc from 'swagger-jsdoc';

const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Todo API',
version: '1.0.0',
description: '使用 Swagger + ESM 撰寫的 Todo API 文件',
},
},
apis: ['./routes/*.js'], // 註解來源路徑
};

const swaggerSpec = swaggerJSDoc(options);

export default swaggerSpec;

2. 建立 routes/todos.js

// routes/todos.js
import express from 'express';

const router = express.Router();

/**
* @openapi
* /todos:
* get:
* summary: 取得所有代辦事項
* responses:
* 200:
* description: 成功取得清單
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Todo'
*/
/**
* @openapi
* /todos:
* post:
* summary: 建立一筆代辦事項
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Todo'
* responses:
* 201:
* description: 建立成功
*/
/**
* @openapi
* components:
* schemas:
* Todo:
* type: object
* required:
* - id
* - task
* properties:
* id:
* type: integer
* example: 1
* task:
* type: string
* example: 學習 Swagger
*/

router.get('/todos', (req, res) => {
res.json([{ id: 1, task: '學習 Swagger' }]);
});

router.post('/todos', (req, res) => {
res.status(201).json({ id: 2, task: req.body.task });
});

export default router;

3. 建立 index.js(入口檔)

// index.js
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import swaggerSpec from './swagger.js';
import todosRouter from './routes/todos.js';

const app = express();

app.use(express.json()); // 處理 JSON 請求
app.use('/api/todos', todosRouter);

// 提供 Swagger UI 文件頁面
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running: http://localhost:${PORT}`);
console.log(`Swagger Docs: http://localhost:${PORT}/api-docs`);
});

測試與結果

啟動伺服器後,開啟瀏覽器前往:

http://localhost:3000/api-docs

你會看到一個完整的互動式 API 文件頁面,可以點擊操作 GET /todosPOST /todos


小結

整合 Swagger 到 ESM 架構的 Express 專案中並不困難,關鍵在於:

  • 使用 swagger-jsdoc 從 JSDoc 註解產生規格。
  • 搭配 swagger-ui-express 呈現互動式 API 頁面。
  • 配合 ES Module 語法撰寫程式碼。

這樣的文件工具能大幅提升開發效率、降低溝通成本,特別適合多人協作或需要對外提供 API 文件的專案。


如果你想進一步整合 TypeScript、OpenAPI YAML 檔案、或搭配 FastAPI、NestJS 等框架,也可以告訴我,我可以提供對應版本的教學。是否需要?

參考文件

  1. 官方文件

Vue pinia 保持 reactive 入門教學筆記 | 學習筆記

· 閱讀時間約 2 分鐘
kdchang

保持 reactive 寫法:


1️. 用 computed

const todos = computed(() => todoStore.todos)

優點

  • 保持 reactive
  • 簡單直接
  • 在 template 裡 todos 可以直接使用

缺點

  • 如果只是讀值,其實有點多餘

建議用於:在 template 需要用 v-for="todo in todos" 這種情況下。


2️. 直接使用 todoStore.todos 在 template

不額外宣告變數,直接寫:

<ul>
<li v-for="todo in todoStore.todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>

優點

  • 直接讀取 store,最簡單
  • 不需要 computed

缺點

  • 如果在 script 多處使用 todos,每次都要寫 todoStore.todos

建議用於:只在 template 需要使用 todos 的情況。


3️. 使用 storeToRefs

這是 Pinia 官方推薦的方式,可以一次把 state 解構成 reactive ref:

import { storeToRefs } from 'pinia'

const todoStore = useTodoStore()
const { todos } = storeToRefs(todoStore)

優點

  • todos 是 reactive
  • 取值時不會失去 reactive
  • 適合同時取多個 state(例如 const { todos, count } = storeToRefs(todoStore)

缺點

  • 需要額外 import storeToRefs
  • 初學者可能不熟悉

建議用於:component 中需要多個 state 且偏好解構寫法時。


寫法比較

寫法是否 reactive適用場景
computed 包一層script 內多次使用
直接 todoStore.todos只在 template 使用
storeToRefs多個 state 解構需要時

建議

  • 如果只需要一個 state:computed 或直接用 todoStore.todos
  • 如果需要多個 state:storeToRefs

範例(使用 storeToRefs

<script setup>
import { useTodoStore } from '../stores/todoStore'
import { storeToRefs } from 'pinia'

const todoStore = useTodoStore()
const { todos } = storeToRefs(todoStore)

function addTodo() { ... }
function removeTodo(id) { ... }
function toggleComplete(id) { ... }
</script>

template 裡直接使用 todos,效果與 computed 相同。


總結
三種寫法都可行,主要差異在語法風格與使用場景。若不想意外解構成非 reactive 值,使用 storeToRefs 是最安全的。

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

· 閱讀時間約 4 分鐘
kdchang

前言

在現代 Web 應用中,使用者認證與授權機制是系統安全的重要基礎。傳統上,我們可能使用 Session 與 Cookie 搭配伺服器端儲存進行身份驗證;但在前後端分離、多平台(Web、Mobile、API Gateway)應用日益普及的情況下,更輕量、跨平台、無狀態的驗證方案逐漸成為主流。

JWT(JSON Web Token)正是這樣一種流行的解法。它是一種根據 JSON 格式加密後產生的 Token,可用來安全地在用戶與伺服器間傳遞資訊。


重點摘要

  • JWT 是一種開放標準(RFC 7519),用於在雙方之間以 JSON 格式安全傳遞資訊。

  • 結構由三部分組成:Header.Payload.Signature

  • 主要用途:

    • 身份認證(Authentication)
    • 授權(Authorization)
  • 特點:

    • 可跨語言與平台使用
    • 支援無狀態驗證(不需伺服器端儲存 session)
    • 可設定過期時間與自訂 Payload 欄位
  • 常見應用場景:API Token 驗證、行動裝置登入狀態維持、OAuth 搭配使用

  • JWT 須透過 HTTPS 傳遞以避免中間人攻擊(MITM)

  • 不適合儲存敏感資訊(如密碼、信用卡號)


JWT 結構說明

一個 JWT 字串長得像這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOiIxMjM0IiwibmFtZSI6IktEIiwiZXhwIjoxNzAwMDAwMDAwfQ.
sKPXrY3AvKb0aBQKgYF3mn7ZWh9yGpyF2X2NFie5TIU

它由三個部分組成,透過 . 分隔:

  1. Header:定義演算法(如 HS256)與類型(JWT)。
  2. Payload:承載實際資料(如使用者 ID、帳號、過期時間)。
  3. Signature:用密鑰加密前兩部分,用於驗證是否被竄改。

實作範例(Node.js + ESM)

安裝必要套件

npm install express jsonwebtoken dotenv

專案架構

project/
├── index.js
├── routes/
│ └── auth.js
├── .env

.env 檔(儲存密鑰)

JWT_SECRET=mysecretkey123

routes/auth.js

import express from 'express';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';

dotenv.config();
const router = express.Router();

// 模擬使用者登入
router.post('/login', (req, res) => {
const { username, password } = req.body;

// 模擬帳密驗證(實際應從 DB 查詢)
if (username === 'admin' && password === '123456') {
const payload = {
userId: 'abc123',
username: 'admin',
};

// 簽發 Token,過期時間為 1 小時
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '1h',
});

return res.json({ token });
}

res.status(401).json({ error: '帳號或密碼錯誤' });
});

// 受保護資源
router.get('/profile', (req, res) => {
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '請提供 Token' });
}

const token = authHeader.split(' ')[1];

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
res.json({ message: '驗證成功', user: decoded });
} catch (err) {
res.status(401).json({ error: 'Token 無效或過期' });
}
});

export default router;

index.js

import express from 'express';
import authRouter from './routes/auth.js';

const app = express();
app.use(express.json());

app.use('/api', authRouter);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});

測試流程

  1. 發送 POST /api/login 並附上正確帳密(如 admin / 123456),取得 JWT。
  2. 將該 JWT 作為 Authorization: Bearer <token> 放入 Header 中,請求 GET /api/profile
  3. 若驗證成功,API 會回傳對應使用者資訊;若失敗,則回傳錯誤訊息。

常見安全注意事項

  • 使用 HTTPS:JWT 應透過 HTTPS 傳輸,避免中間人攻擊。
  • 設定適當過期時間:避免長期有效的 Token 被盜用。
  • 避免儲存敏感資訊於 Payload:Payload 是可被解碼的(雖然不可修改),不應含有密碼、信用卡資訊等。
  • 支援 Token 失效機制(如 Token 黑名單):JWT 是無狀態的,若要強制登出或封鎖,需額外設計邏輯。

總結

JWT 是實作登入與驗證的重要工具,具有無狀態、跨平台、可擴充的特性,特別適合 API 驗證場景。本文透過簡單的 Node.js + Express 實作,展示如何產生與驗證 JWT,並說明常見應用與安全注意事項。

不論你是單頁應用 SPA 開發者,還是撰寫 RESTful API 的後端工程師,掌握 JWT 都將大幅提升你的系統安全與擴充能力。

HTTP/2 介紹與入門教學筆記 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

1. HTTP/2 簡介

HTTP/2 是 HTTP/1.1 的後繼版本,由 IETF HTTP 工作小組開發,並於 2015 年 5 月成為正式標準(RFC 7540)。HTTP/2 的主要目標是提高 Web 的性能,減少延遲,並優化資源的傳輸方式。

2. HTTP/2 的核心特性

2.1 二進制分幀層(Binary Framing Layer)

HTTP/2 以二進制格式進行數據傳輸,而非 HTTP/1.1 的純文字格式。這使得解析和處理更高效,減少了協議的開銷。

2.2 多路徑傳輸(Multiplexing)

在 HTTP/1.1 中,一個 TCP 連線同時只能處理一個請求,導致「線頭阻塞」問題。HTTP/2 允許在單個 TCP 連線中並行傳輸多個請求與回應,顯著提高效率。

2.3 流量控制與優先權(Stream Prioritization)

HTTP/2 允許客戶端為請求設定優先級,讓重要的資源(如 CSS、JS)優先傳輸,提高頁面載入速度。

2.4 頭部壓縮(Header Compression)

HTTP/2 使用 HPACK 壓縮技術來減少 HTTP 標頭的大小,避免 HTTP/1.1 中重複傳輸 Cookie、User-Agent 等大量標頭資訊的問題。

2.5 伺服器推送(Server Push)

伺服器可以主動推送客戶端尚未請求的資源,例如 HTML 請求回應時,伺服器可同時推送 CSS、JavaScript 檔案,減少額外的請求時間。

3. HTTP/2 與 HTTP/1.1 的比較

特性HTTP/1.1HTTP/2
資料格式純文字二進制
多路復用不支援支援
流量控制
頭部壓縮HPACK
伺服器推送

4. HTTP/2 的使用與設定

4.1 瀏覽器支援

現代主流瀏覽器(Chrome、Firefox、Edge、Safari)皆支援 HTTP/2,但通常需要搭配 HTTPS 使用。

4.2 伺服器支援

常見的 Web 伺服器如 Nginx、Apache、LiteSpeed 都已支援 HTTP/2,但需進行適當的設定。

4.2.1 在 Nginx 啟用 HTTP/2

若要在 Nginx 中啟用 HTTP/2,需要確保已安裝支援 HTTP/2 的 Nginx 版本(1.9.5 以上)並修改設定檔:

server {
listen 443 ssl http2;
server_name example.com;

ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/cert.key;

location / {
root /var/www/html;
index index.html;
}
}

listen 指令中加入 http2 即可啟用 HTTP/2。

4.2.2 在 Apache 啟用 HTTP/2

Apache 2.4.17 以上版本支援 HTTP/2,需要啟用 mod_http2 模組。

a2enmod http2
systemctl restart apache2

接著,在 VirtualHost 設定中加入:

<VirtualHost *:443>
Protocols h2 http/1.1
DocumentRoot /var/www/html
ServerName example.com
</VirtualHost>

5. HTTP/2 客戶端測試

可以使用 curl 測試 HTTP/2 是否正常運作:

curl -I --http2 https://example.com

若顯示 HTTP/2 200,則表示伺服器已成功支援 HTTP/2。

6. HTTP/2 的實際應用

6.1 使用 HTTP/2 Server Push

在 HTTP/2 中,可以使用 Link 標頭來主動推送資源。例如,在 Nginx 配置中加入:

location / {
add_header Link "</style.css>; rel=preload; as=style";
add_header Link "</script.js>; rel=preload; as=script";
}

這樣當客戶端請求 index.html 時,伺服器會自動推送 style.cssscript.js,減少 HTTP 請求數量。

6.2 透過瀏覽器 DevTools 觀察 HTTP/2

打開 Chrome 開發者工具(F12) → Network 分頁,查看 Protocol 欄位,若顯示 h2,則表示該請求使用了 HTTP/2。

7. 總結

HTTP/2 改進了 HTTP/1.1 的多項限制,透過多路徑傳輸、頭部壓縮與伺服器推送提高了 Web 效能。現代瀏覽器與伺服器已廣泛支援 HTTP/2,建議在新專案中啟用 HTTP/2,以提升用戶體驗與網站速度。

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

· 閱讀時間約 3 分鐘
kdchang

前言

Django 是一個由 Python 編寫的高階 Web 框架,強調快速開發與簡潔設計。它提供完整的功能模組如 ORM、Admin 介面、Form 處理、URL 路由與認證系統,讓開發者能專注於商業邏輯而非重複造輪子。本文將帶你一步一步建立一個基本的 Django 應用程式,並說明其核心概念。

一、環境安裝

首先,請確保你已安裝好 Python(建議 3.8+)與 pip。以下是建立虛擬環境並安裝 Django 的方式:

python -m venv myenv
source myenv/bin/activate # Windows 請使用 myenv\Scripts\activate
pip install django

確認安裝成功:

django-admin --version

二、建立 Django 專案

Django 使用專案(project)與應用程式(app)來組織程式碼。先建立一個新的專案:

django-admin startproject mysite
cd mysite

你會看到以下結構:

mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
asgi.py
wsgi.py

三、建立應用程式

應用程式是 Django 專案的功能模組。例如一個部落格、用戶系統或留言板就是一個 app。以下我們建立一個簡單的留言板:

python manage.py startapp guestbook

guestbook/ 資料夾中會看到以下結構:

guestbook/
models.py
views.py
admin.py
apps.py
tests.py
...

接著,在 mysite/settings.py 中註冊這個 app:

INSTALLED_APPS = [
...
'guestbook',
]

四、定義資料模型(Model)

編輯 guestbook/models.py,新增一個留言的資料模型:

from django.db import models

class Message(models.Model):
name = models.CharField(max_length=100)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
return f'{self.name} 說:{self.content[:20]}...'

建立資料表:

python manage.py makemigrations
python manage.py migrate

五、啟用後台管理介面

Django 提供強大的 admin 功能。先建立一個 superuser:

python manage.py createsuperuser

然後在 guestbook/admin.py 中註冊 model:

from django.contrib import admin
from .models import Message

admin.site.register(Message)

啟動伺服器並進入後台:

python manage.py runserver

前往瀏覽器開啟 http://127.0.0.1:8000/admin/,使用剛建立的帳號登入,即可操作資料。

六、建立 Views 與 Templates

接下來,我們撰寫一個簡單的留言列表與新增留言的畫面。

views.py

from django.shortcuts import render, redirect
from .models import Message

def message_list(request):
if request.method == "POST":
name = request.POST.get('name')
content = request.POST.get('content')
if name and content:
Message.objects.create(name=name, content=content)
return redirect('message_list')

messages = Message.objects.order_by('-created_at')
return render(request, 'guestbook/message_list.html', {'messages': messages})

templates

建立目錄 guestbook/templates/guestbook/message_list.html

<!DOCTYPE html>
<html>
<head>
<title>留言板</title>
</head>
<body>
<h1>留言板</h1>
<form method="post">
{% csrf_token %}
姓名:<input type="text" name="name"><br>
留言:<br>
<textarea name="content" rows="5" cols="40"></textarea><br>
<input type="submit" value="送出">
</form>
<hr>
{% for msg in messages %}
<p><strong>{{ msg.name }}</strong> 說:</p>
<p>{{ msg.content }}</p>
<p><small>{{ msg.created_at }}</small></p>
<hr>
{% endfor %}
</body>
</html>

七、設定 URL 路由

guestbook/ 建立 urls.py

from django.urls import path
from . import views

urlpatterns = [
path('', views.message_list, name='message_list'),
]

然後到 mysite/urls.py 中加入:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path('admin/', admin.site.urls),
path('', include('guestbook.urls')),
]

八、啟動開發伺服器

python manage.py runserver

在瀏覽器開啟 http://127.0.0.1:8000/,你將看到留言板畫面,並能提交留言。


總結

Django 是功能非常齊全且具可擴展性的 Web 框架,初學者可從資料模型、後台管理、表單處理與模板引擎著手。本文僅為入門教學,未來更多進階功能如使用 class-based views、form 類別、自訂 middleware、部署到雲端等,規劃在之後的教學筆記進行介紹。

setTimeout + for loop + closure 核心介紹入門教學筆記 | 學習筆記

· 閱讀時間約 2 分鐘
kdchang

一、varlet 的作用域差異

宣告方式作用域是否有暫時性死區(TDZ)
var函式作用域
let區塊作用域
const區塊作用域
  • var 宣告的變數在整個函式內都可存取
  • let 則只在所在區塊 {} 中有效

二、閉包(Closure)

閉包是指「函式能記住它被定義時的作用域,即使在外部執行也能存取當時的變數」。

function outer() {
let count = 0;
return function inner() {
console.log(count++);
};
}

const fn = outer();
fn(); // 0
fn(); // 1

這個例子中,inner 函式記住了 outer 中的 count 變數。


三、事件迴圈與 setTimeout

console.log("start");

setTimeout(() => {
console.log("timeout");
}, 0);

console.log("end");

輸出順序:

start
end
timeout

原因是 setTimeout 是非同步的,它會被放入「事件佇列」(event queue)中,等主程式執行完後才會被處理。


四、面試陷阱:varsetTimeout 搭配

for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}

這段程式的輸出是:

3
3
3

因為:

  • var 沒有區塊作用域,所有的 setTimeout 都引用同一個 i
  • setTimeout 執行時,迴圈已跑完,i 已是 3

五、正確解法:使用 let

for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}

輸出為:

0
1
2

因為 let 有區塊作用域,每次迴圈都建立新的 i,閉包會記住各自的值。


六、另一個解法:IIFE(立即執行函式)

若必須用 var,可以用 IIFE 綁定每次的 i

for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => {
console.log(j);
}, 1000);
})(i);
}

這樣也會正確輸出:

0
1
2

七、總結

  1. var 是函式作用域,容易被非同步邏輯影響
  2. let 是區塊作用域,與閉包搭配可解決常見陷阱
  3. setTimeout 是非同步,會延遲執行
  4. 通常面試會結合這些概念出題,考你對 JavaScript 執行流程、作用域與閉包的理解

SCSS 介紹入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

SASS(Syntactically Awesome Stylesheets)是一種 CSS 預處理器(CSS Preprocessor),它擴展了 CSS 的功能,使樣式表更具可讀性、模組化和可維護性。SASS 允許使用變數、巢狀(Nesting)、混入(Mixin)、繼承(Extend)等進階特性,讓開發者能更有效率地管理 CSS。

SASS 有兩種語法:

  1. SCSS(Sassy CSS):與傳統 CSS 語法相似,使用 {}; 來區分樣式,擴展性強且相容 CSS。
  2. SASS(縮排語法):省略 {};,使用縮排來表示層級關係,簡潔但較不常用。

目前,SCSS 語法較受歡迎,因此本文主要以 SCSS 為主進行介紹。


二、SASS 的優勢

使用 SASS 的主要優勢包括:

  1. 可維護性:透過模組化的結構管理樣式,避免冗長且難以維護的 CSS。
  2. 變數(Variables):可定義全站共用的變數,例如顏色、字體大小等,提高一致性。
  3. 巢狀結構(Nesting):讓樣式更具層次感,避免重複選擇器。
  4. 混入(Mixin):類似函式的概念,可重複使用樣式區塊,減少冗餘。
  5. 繼承(Extend):透過 @extend 共享樣式,減少重複編寫的代碼。
  6. 函式(Functions):內建函式如 lighten()darken() 可動態調整顏色,提高設計靈活性。

三、安裝與使用 SASS

1. 透過 npm 安裝

如果使用 Node.js,可透過 npm 安裝 SASS:

npm install -g sass

安裝後,可使用以下指令將 SCSS 轉譯為 CSS:

sass input.scss output.css

可使用 --watch 讓 SASS 自動監聽檔案變化並即時編譯:

sass --watch input.scss:output.css

2. 透過 CDN 使用

雖然 SASS 本身無法直接在瀏覽器運行,但可透過一些線上工具(如 CodePen)編寫 SCSS,並自動編譯成 CSS 進行預覽。


四、SASS 基礎語法

1. 變數(Variables)

SASS 允許使用變數來存儲顏色、字體大小、間距等常數,使樣式更具一致性。

$primary-color: #3498db;
$font-size: 16px;

body {
color: $primary-color;
font-size: $font-size;
}

2. 巢狀結構(Nesting)

在 CSS 中,我們通常需要重複撰寫父選擇器,但在 SASS 中可直接巢狀編寫,提高可讀性。

nav {
background: #333;
ul {
list-style: none;
li {
display: inline-block;
a {
color: white;
text-decoration: none;
}
}
}
}

這段 SCSS 會編譯成以下 CSS:

nav {
background: #333;
}
nav ul {
list-style: none;
}
nav ul li {
display: inline-block;
}
nav ul li a {
color: white;
text-decoration: none;
}

3. 混入(Mixin)

Mixin 可定義可重複使用的樣式區塊,並可接受參數,使樣式更加靈活。

@mixin button-style($bg-color) {
background-color: $bg-color;
color: white;
padding: 10px 15px;
border-radius: 5px;
}

.btn-primary {
@include button-style(#3498db);
}

.btn-secondary {
@include button-style(#2ecc71);
}

編譯後的 CSS 為:

.btn-primary {
background-color: #3498db;
color: white;
padding: 10px 15px;
border-radius: 5px;
}

.btn-secondary {
background-color: #2ecc71;
color: white;
padding: 10px 15px;
border-radius: 5px;
}

4. 繼承(Extend)

@extend 允許一個選擇器繼承另一個選擇器的樣式,避免重複撰寫代碼。

.message {
padding: 10px;
border-radius: 5px;
}

.success {
@extend .message;
background: #2ecc71;
}

.error {
@extend .message;
background: #e74c3c;
}

編譯後的 CSS:

.message, .success, .error {
padding: 10px;
border-radius: 5px;
}

.success {
background: #2ecc71;
}

.error {
background: #e74c3c;
}

5. 運算與函式

SASS 允許在樣式中進行運算,使數值調整更加靈活。

$base-size: 16px;

h1 {
font-size: $base-size * 2;
}

h2 {
font-size: $base-size * 1.5;
}

此外,SASS 內建許多函式,例如 darken()lighten() 可用來調整顏色:

$primary-color: #3498db;

button {
background: $primary-color;
&:hover {
background: darken($primary-color, 10%);
}
}

編譯後的 CSS:

button {
background: #3498db;
}
button:hover {
background: #217dbb;
}

五、SASS 進階技巧

1. 分割檔案

SASS 允許將樣式拆分成多個檔案,並透過 @import@use 來管理,提升可維護性。

// _variables.scss
$primary-color: #3498db;
$font-size: 16px;

// main.scss
@import 'variables';

body {
color: $primary-color;
font-size: $font-size;
}

2. 使用 @use

@use@import 的改進版,能避免變數命名衝突,推薦使用:

@use 'variables' as v;

body {
color: v.$primary-color;
font-size: v.$font-size;
}

六、結論

SASS 是一種強大的 CSS 預處理器,它提供變數、巢狀、Mixin、繼承等功能,使樣式管理更加高效與模組化。透過 SASS,開發者可以撰寫更具結構性、可讀性和可維護性的 CSS。

對於前端開發者來說,掌握 SASS 不僅能提升開發效率,還能讓專案的樣式管理更加清晰。在實際專案中,建議將樣式模組化,並善用變數與 Mixin,以確保程式碼的可重用性與一致性。