跳至主要内容

JavaScript 練習挑戰紀錄 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

Longest Word

當我們執行 paragraph.match(/[a-z]+/gi) 時,這段 JavaScript 程式碼的意思是:

目的:

paragraph 字串中找出所有連續的英文字母(不分大小寫)組成的片段,並以陣列形式回傳。


語法拆解:

paragraph.match(/[a-z]+/gi)
部分意思
paragraph要處理的字串(變數名)
.match(...)是一個字串的方法,會根據正則表達式去尋找所有符合條件的子字串
/[a-z]+/gi正則表達式:搜尋一或多個英文字母
[a-z]表示小寫英文字母 a 到 z(大寫會由 i 修飾符處理)
+表示「一個或多個」符合前面 [a-z] 的字元
gglobal 全域搜尋,找到所有符合條件的片段
iignore case 忽略大小寫,讓 [a-z] 同時能配對 A-Z

範例:

const paragraph = "Hello, 世界! This is a test123.";
const result = paragraph.match(/[a-z]+/gi);
console.log(result);

輸出:

["Hello", "This", "is", "a", "test"]

這段程式會:

  • 忽略中文與數字
  • 把英文字母串(不論大小寫)全都抓出來

好,我們真的慢慢來,我會用超直白的說法、一步一步演示,幫你理解 arr.sort((a, b) => a - b) 是怎麼把數字從小排到大的。


先記住一個核心概念:

arr.sort((a, b) => a - b);

這一行的意思是:

陣列裡的「每兩個數字 a 和 b」,如果 a 比 b 小,就讓 a 排在 b 前面。


一步一步來看:

假設這個陣列:

let arr = [30, 10, 5];

我們要用 sort() 排成:

[5, 10, 30]

JS 怎麼做的?

它會自動幫你「兩個兩個配對」來比較順序。

第一次比較:30 和 10

a = 30, b = 10
a - b = 30 - 10 = 20(是正數)

JavaScript 覺得:b(10)比較小 → 讓 10 排在 30 前面

結果變成這樣:

[10, 30, 5]

第二次比較:30 和 5

a = 30, b = 5
a - b = 30 - 5 = 25(是正數)

b(5)比較小 → 把 5 放到前面

結果變成:

[10, 5, 30]

第三次比較:10 和 5

a = 10, b = 5
a - b = 10 - 5 = 5(是正數)

b(5)比較小 → 把 5 放到前面

最後得到:

[5, 10, 30]

就是我們想要的「從小到大」排序!


最簡單的記法:

語法排序方式
(a, b) => a - b小到大
(a, b) => b - a大到小

小挑戰

let arr = [8, 3, 100, 1];
arr.sort((a, b) => a - b); // [1, 3, 8, 100]

Codeland Username Validation

符合條件的正則表達式:

正則表達式:

^[A-Za-z][A-Za-z0-9_]{2,23}[A-Za-z0-9]$

解釋:

  1. ^:字串的開始。
  2. [A-Za-z]:第一個字符必須是字母(無論是大寫還是小寫)。
  3. [A-Za-z0-9_]{2,23}:接下來可以包含字母、數字或底線,並且長度必須在 2 到 23 之間。這確保了整個字串的長度在 4 到 25 之間(包括起始的字母)。
  4. [A-Za-z0-9]:字串的結尾必須是字母或數字,不能是底線 _
  5. $:字串的結束。

規則解釋:

  1. 用戶名必須是 4 到 25 個字符。
  2. 用戶名的開頭必須是字母。
  3. 用戶名可以包含字母、數字和底線 _
  4. 用戶名不能以底線 _ 結尾。

範例:

  • "username123" 會匹配成功。
  • "user_name" 會匹配成功。
  • "user_name_" 會匹配失敗(因為底線 _ 在結尾)。
  • "123username" 會匹配失敗(因為開頭不是字母)。
  • "user" 會匹配成功。
  • "a_1" 會匹配失敗(因為長度太短,少於 4 個字符)。

2620. Counter

var createCounter = function(n) {
return function() {
return n++;
};
};

這樣做的好處是:

n 是外部函式的區域變數(closure

每次呼叫內部函式都能使用並修改 n

n++ 會先回傳 n 的值,再讓 n 加 1

小補充:n++ vs ++n n++:先回傳,再加一 ++n:先加一,再回傳

Jest 使用 ES Module 入門教學筆記 | 學習筆記

· 閱讀時間約 5 分鐘
kdchang

前言

在 JavaScript 開發中,ES Modules (ESM) 已成為標準。從 Node.js 12 開始,ESM 已獲得原生支援,而前端開發(如 React、Vue、Svelte 等框架)早已全面採用 ES Module。 然而,當我們使用 Jest 來撰寫與執行測試時,若要直接使用 ES Module,會遇到一些設定上的挑戰。本篇筆記將說明如何在專案中讓 Jest 正確執行 ES Module 程式碼,並透過實例展示操作流程。


1. Jest 是否支援 ES Module?

Jest 自 v27 版本開始實驗性支援 ES Module,到了 v28 以後更加穩定。但因為 Node.js 與 CommonJS、ESM 的處理邏輯不同,仍需要額外設定。

若你使用 ES Module(例如 .mjs 檔案、type: module),或是前端專案用 ES6 import / export,就必須進行相應的 Jest 設定。


2. 初始化專案與安裝 Jest

首先,我們建立一個 Node.js 專案:

mkdir jest-esm-demo
cd jest-esm-demo
npm init -y

接著安裝 Jest:

npm install --save-dev jest

重點!package.json 中加入 type: "module",讓整個專案採用 ES Module:

{
"name": "jest-esm-demo",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.0.0"
}
}

3. 撰寫 ES Module 程式碼

假設我們有一個簡單的 加法模組 sum.mjs

// sum.mjs
export function sum(a, b) {
return a + b;
}

接著建立測試檔案 sum.test.mjs

// sum.test.mjs
import { sum } from './sum.mjs';

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

這時候直接執行 npm test 會報錯:

SyntaxError: Cannot use import statement outside a module

因為 Jest 預設使用 CommonJS,不認得 ES Module,需要額外設定。


4. 設定 Jest 支援 ES Module

方案一:使用 jest.config.js 並指定 transform

首先,安裝 Babeljest-esm-transformer 之類的工具。如果希望簡單一點,可以直接使用 Jest 的內建 ESM 模式(推薦)。

建立 jest.config.js

// jest.config.js
export default {
transform: {},
extensionsToTreatAsEsm: ['.mjs'],
testEnvironment: 'node',
};

重點設定解釋:

  • transform: {} 表示不使用 Babel 或其他轉譯器
  • extensionsToTreatAsEsm 告訴 Jest 哪些副檔名視為 ESM
  • testEnvironment: 'node' 使用 Node 環境(非 jsdom)

執行 npm test,這時候仍會報錯:

Jest encountered an unexpected token

因為 Jest 內建 transform 無法處理 ES Module 語法(即便 Node.js 支援,但 Jest 內部解析流程不同)。


5. 方案二:使用 babel-jest 轉譯 ESM

安裝 Babel 及相關套件:

npm install --save-dev @babel/preset-env babel-jest

建立 .babelrc

{
"presets": ["@babel/preset-env"]
}

更新 jest.config.js

export default {
transform: {
'^.+\\.m?js$': 'babel-jest',
},
extensionsToTreatAsEsm: ['.mjs'],
testEnvironment: 'node',
};

這樣 Jest 會用 babel-jest 處理 .js.mjs 檔案,並當作 ESM 解析。

此時執行 npm test,測試就會通過:

PASS  ./sum.test.mjs
✓ adds 1 + 2 to equal 3 (5 ms)

6. 測試更複雜情境

假設我們有一個 fetch 模組,使用 async function:

// fetchData.mjs
export async function fetchData() {
return 'peanut butter';
}

測試檔:

// fetchData.test.mjs
import { fetchData } from './fetchData.mjs';

test('returns peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});

一樣執行 npm test,因為已經設定好 Babel 與 ESM,非同步測試也能正常運作。


7. 使用 import.meta.url 注意事項

如果你的程式中有用 import.meta.url(例如讀取檔案、動態載入),要注意 Jest 執行時 context 與 Node.js 直跑不同

例如:

// fileUtil.mjs
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

export async function readConfig() {
const filePath = join(__dirname, 'config.json');
const content = await readFile(filePath, 'utf-8');
return JSON.parse(content);
}

測試時要確保 import.meta.url 能被正確解析(可能需要 jest-environment-node,或 Mock 檔案路徑)。


8. 常見錯誤與解法

問題:SyntaxError: Cannot use import statement outside a module

  • 確認 jest.config.js 中有 extensionsToTreatAsEsm
  • 測試檔、副程式檔是否副檔名為 .mjs

問題:Unexpected token export

  • 確認有設定 transform 使用 babel-jest
  • 確認 .babelrc 正確啟用 @babel/preset-env

問題:ESM 測試檔找不到 module

  • 確認 package.jsontype: module
  • 相對路徑記得補 .mjs 副檔名

9. 實用 Jest CLI 指令

  • 執行單一測試檔:
npx jest sum.test.mjs
  • 只跑某個測試:
test.only('專跑這個測試', () => { ... });
  • 顯示覆蓋率:
npx jest --coverage

總結

Jest 預設是為 CommonJS 設計,但隨著 ES Module 在 Node.js 與前端日漸普及,支援 ESM 變得越來越重要。

透過本文介紹,我們可以知道:

  1. type: module 啟用 ESM
  2. Jest 需要設定 transformextensionsToTreatAsEsm
  3. 使用 babel-jest 來處理 ESM
  4. 注意 import.meta.url、路徑、非同步等 ESM 細節

雖然設定比 CJS 稍微複雜,但一旦設定好後,整個測試流程一樣流暢,也能為未來更符合現代標準的專案奠定基礎。

建議未來若有使用 TypeScript、React、Vue 等,也可以結合對應的 transformer 與設定,讓 Jest 完全支援你的開發。

Jest AAA 測試原則入門教學筆記 | 學習筆記

· 閱讀時間約 2 分鐘
kdchang

範例測試:saveMoney 方法

atm.js

class ATM {
constructor(balance) {
this.balance = balance;
}
saveMoney(amount) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
this.balance += amount;
return this.balance;
}
withdrawMoney(amount) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
if (amount > this.balance) {
throw new Error('Insufficient balance');
}
this.balance -= amount;
return this.balance;
}
getBalance() {
return this.balance;
}
reset() {
this.balance = 0;
}
setBalance(balance) {
if (balance < 0) {
throw new Error('Balance cannot be negative');
}
this.balance = balance;
}
}

export default ATM;

atm.test.js

import ATM from '../atm.js';

test('saveMoney adds money to balance', () => {
// Arrange: 建立一個 ATM 實例,初始餘額 0
const atm = new ATM(0);

// Act: 存入 1000
atm.saveMoney(1000);

// Assert: 餘額應該變成 1000
expect(atm.balance).toBe(1000);
});

逐步對應:

步驟內容
Arrange建立 ATM 物件並給初始值
Act呼叫 saveMoney(1000)
Assert驗證 atm.balance 是否等於 1000

另一個範例:檢查錯誤拋出

如果要測試當金額是負數時會丟錯誤:

test('saveMoney throws error when amount <= 0', () => {
// Arrange
const atm = new ATM(0);

// Act & Assert
expect(() => atm.saveMoney(0)).toThrow('Amount must be positive');
});

這裡因為 actassert 綁在一起,所以在 expect 裡包了一個 function,來驗證是否拋出錯誤。


完整測試檔(用 AAA 標註)

import ATM from '../atm.js';

test('saveMoney adds money to balance', () => {
// Arrange
const atm = new ATM(0);

// Act
atm.saveMoney(1000);

// Assert
expect(atm.balance).toBe(1000);
});

test('saveMoney throws error when amount <= 0', () => {
// Arrange
const atm = new ATM(0);

// Act & Assert
expect(() => atm.saveMoney(0)).toThrow('Amount must be positive');
});

Google Lighthouse 介紹與入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

一、什麼是 Lighthouse?

Google Lighthouse 是 Google 開發的開源自動化工具,主要用來評估網頁的品質,包含 效能 (Performance)、無障礙 (Accessibility)、最佳化 (Best Practices)、SEO、漸進式網頁應用 (PWA) 等五大面向。透過 Lighthouse,開發者可以快速找到網站問題與優化建議,幫助網站在使用者體驗與搜尋引擎上表現更好。

Lighthouse 可以透過以下方式執行:

  • Chrome DevTools(瀏覽器內建)
  • Node.js CLI(命令列工具)
  • Lighthouse CI(持續整合工具)
  • Web 版https://pagespeed.web.dev/)

本教學以 Chrome DevTools 為主,搭配 命令列工具輔助說明。


二、如何在 Chrome 使用 Lighthouse

1. 開啟 Chrome DevTools

  • 使用 Chrome 瀏覽器打開我們想分析的網站
  • 按下 F12Ctrl+Shift+I (Mac: Cmd+Option+I) 開啟 DevTools
  • 點選 「Lighthouse」 分頁(如果沒有看到,點選 >> 更多選項即可)

2. 設定 Lighthouse 報告

在 Lighthouse 分頁中,可以看到幾個選項:

  • Categories:選擇要測試的項目(預設全選)
  • Device:選擇模擬裝置(Mobile 或 Desktop)

一般來說,建議從 Mobile 開始測試,因為 Google 搜尋主要使用行動端指標作為排名依據。

3. 開始產生報告

設定好後,點擊 「Analyze page load」(或「Generate report」),Lighthouse 會開始分析。分析過程會自動重新載入頁面並執行模擬測試,過程大約 30 秒至 1 分鐘。

完成後,會生成一份報告,包含分數、每個項目的問題說明與建議。


三、報告解讀與優化建議

以下為報告中幾個重要指標:

  1. Performance(效能)

    • First Contentful Paint (FCP):第一次內容繪製時間
    • Largest Contentful Paint (LCP):主要內容繪製完成時間
    • Time to Interactive (TTI):頁面可互動時間
    • Cumulative Layout Shift (CLS):累積版面位移

優化方向範例

  • 壓縮圖片(使用 WebP)
  • 延遲非必要 JavaScript 載入(lazy loading)
  • 使用 CSS/JS minify 工具
  • 啟用瀏覽器快取 (cache)
  1. Accessibility(無障礙)

    • 圖片是否有 alt 屬性
    • 表單元素是否有 label
    • 按鈕是否有可辨識的名稱

優化方向範例

  • 確保所有互動元件有適當的 ARIA 標籤
  • 保持足夠的色彩對比
  1. Best Practices(最佳化)

    • 是否使用 HTTPS
    • 是否避免過時的 API
    • 檢查瀏覽器安全設定
  2. SEO

    • 是否有 meta 描述
    • 是否設定 <title>
    • 是否有 robots.txt
    • 頁面是否可被索引

四、實際範例:分析一個網頁

前往 https://example.com 網站,操作步驟如下:

使用 Chrome DevTools:

  1. 用 Chrome 瀏覽 https://example.com
  2. 開啟 DevTools → Lighthouse 分頁
  3. 選擇 Mobile + 全部 Categories
  4. 點擊 Analyze page load

執行後,我們會看到一份報告,例如:

CategoryScore
Performance68
Accessibility92
Best Practices85
SEO90

針對效能分數 68,Lighthouse 會提出具體建議,例如:

  • "Serve images in next-gen formats" → 建議將 JPG/PNG 圖片轉換為 WebP
  • "Eliminate render-blocking resources" → 建議將 CSS/JS 非同步或延遲載入

此時,我們可以採取以下修正:

  • 使用 ImageMagickSquoosh 等工具壓縮並轉換圖片
  • 加上 <link rel="preload"> 標籤預先載入必要資源
  • script 標籤加上 defer 屬性

五、使用命令列執行 Lighthouse

如果需要自動化測試或整合到 CI/CD,可以用 Node.js 安裝 Lighthouse:

1. 安裝

npm install -g lighthouse

2. 執行

lighthouse https://example.com --view

執行後會產生一個 HTML 報告並自動打開。

可以用額外參數調整輸出:

lighthouse https://example.com --output json --output html --output-path ./report.html --preset desktop

六、實務應用與建議

  • 開發階段就導入:開發過程中就應該多次使用 Lighthouse,而不是到上線前才檢查。
  • 設定目標分數:通常建議 Mobile 效能達到 80 分以上。
  • 結合 CI/CD:用 Lighthouse CI 在部署時自動檢查網站品質,確保每次更新不會退步。

如果是大型專案,也可以與 WebPageTest、PageSpeed Insights 搭配,取得更廣泛的性能數據。


七、總結

Google Lighthouse 是一個功能強大的網站品質檢測工具,不僅能協助提升效能,還能兼顧 SEO、無障礙與最佳實務。無論是初學者或資深前端工程師,都建議將 Lighthouse 納入開發流程中,定期檢查與優化,為網站帶來更好的使用者體驗與搜尋排名。

透過本篇教學,相信我們已能夠:

  1. 知道如何使用 Chrome DevTools 產生報告
  2. 理解報告中指標與優化方式
  3. 用命令列執行 Lighthouse 以自動化分析

後續可以根據團隊需求,進一步探索 Lighthouse CI、API 或與其他性能工具整合的進階應用。

Progressive Web App(PWA)入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

一、前言

在行動裝置普及的今天,使用者越來越期待 Web 應用程式能提供與原生 App 相近的使用體驗。然而,傳統 Web 應用程式在離線支援、效能與通知推送等方面仍有所不足。為了彌補這些缺點,Progressive Web App(PWA) 應運而生。

PWA 結合了 Web 技術與原生 App 的優勢,使網站具備離線可用、快速載入、可安裝、推播通知等功能,進而提升使用者體驗與互動黏著度。對開發者而言,PWA 是一種較低成本就能達到跨平台效果的解法。本文將帶你快速認識 PWA 的核心概念與實作方式,適合初學者或 Web 開發者作為入門參考。


二、重點摘要

  • PWA 定義:一種利用現代 Web API 提供類似原生 App 體驗的 Web 應用程式。

  • 三大核心技術

    • HTTPS:保護傳輸安全並啟用 Service Worker。
    • Web App Manifest:描述應用程式的基本資訊,例如名稱、圖示、顯示模式等。
    • Service Worker:一種可攔截網路請求並實現快取、離線功能的背景腳本。
  • PWA 特性

    • 可安裝(Installable)
    • 離線可用(Offline capable)
    • 背景推播(Push Notification)
    • 響應式設計(Responsive)
    • 快速載入(Fast loading)
  • 使用情境

    • 新創或中小企業開發跨平台 App
    • 強化使用者體驗的內容網站或工具型網站
    • 電商網站提升轉換率與留存

三、實際範例:打造一個簡單的 PWA

我們將從零開始建立一個基本的 PWA 網站,具備可安裝與離線快取功能。

1. 建立專案目錄與基本檔案

建立一個資料夾,內含下列檔案:

pwa-demo/
├── index.html
├── app.js
├── service-worker.js
├── manifest.json

2. 撰寫 index.html

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PWA Demo</title>
<link rel="manifest" href="manifest.json" />
</head>
<body>
<h1>Hello, PWA!</h1>
<script src="app.js"></script>
</body>
</html>

3. 撰寫 manifest.json

{
"name": "PWA Demo",
"short_name": "PWADemo",
"start_url": "./index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

請準備對應的 icon-192.pngicon-512.png 圖示檔案放入根目錄。

4. 撰寫 app.js 並註冊 Service Worker

if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('service-worker.js')
.then((reg) => {
console.log('Service Worker 註冊成功:', reg.scope);
})
.catch((err) => {
console.log('Service Worker 註冊失敗:', err);
});
});
}

5. 撰寫 service-worker.js

const CACHE_NAME = 'pwa-demo-cache-v1';
const urlsToCache = ['/', '/index.html', '/app.js', '/icon-192.png', '/icon-512.png'];

self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache)));
});

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => response || fetch(event.request))
);
});

6. 本地測試與部署

PWA 需在 HTTPS 或 localhost 環境下運行,因此建議使用以下方式測試:

npx serve

或是使用 VS Code 的 Live Server 插件。


四、補充建議與工具

  • Lighthouse:使用 Chrome DevTools 的 Lighthouse 工具分析網站是否符合 PWA 標準。
  • Workbox:Google 提供的套件,簡化 Service Worker 撰寫與管理。
  • Vite / Next.js:這些框架和建造工具支援套件快速啟用 PWA,例如 Vite Plugin PWA。

五、總結

Progressive Web App 是 Web 應用向原生體驗邁進的重要里程碑。透過簡單的設計與實作,我們可以為網站加入離線能力、安裝功能與更佳的效能表現。PWA 不僅提升使用者體驗,也為開發者帶來更高的轉換率與技術可能性。若你正考慮提升網站互動性與可達性,不妨從簡單的 PWA 嘗試開始,逐步拓展應用場景與技術深度。

React Compound Component 模式介紹與入門教學 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在開發大型或複雜的 UI 元件時,傳統的 props 傳遞方式很容易導致元件層層嵌套、可讀性差與維護困難。Compound Component(複合元件)是一種設計模式,能提升 React 元件的可組合性與彈性。它讓父元件扮演「邏輯控制中心」的角色,子元件則能共享上下文資訊,專注於呈現。這種模式常見於設計系統或第三方 UI 套件(例如:<Tabs><Accordion><Dropdown> 等),能夠建立更清晰、有彈性的 API。


重點摘要

  • 定義:Compound Component 是一組彼此配合使用的 React 元件,透過共享上下文管理狀態與邏輯。

  • 優點

    • 增強元件 API 的彈性與可擴充性。
    • 父元件集中邏輯,子元件只負責 UI。
    • 避免 props drilling,提升可維護性。
  • 技術關鍵

    • 使用 React.createContext() 建立 Context。
    • 利用 Children.map 搭配 cloneElement 傳遞資料(進階用法)。
    • 常結合 static property 方式導出子元件(如 MyComponent.Header)。
  • 常見應用場景

    • Tabs 分頁元件
    • Accordion 折疊面板
    • Dropdown 下拉選單
    • Modal 對話框元件

實際範例:建立一個自訂的 <Toggle> 元件

此範例展示如何實作一個 Compound Component:<Toggle>, <Toggle.On>, <Toggle.Off>, <Toggle.Button>

1. ToggleContext.js

import { createContext, useContext } from 'react';

const ToggleContext = createContext();

export function useToggleContext() {
const context = useContext(ToggleContext);
if (!context) {
throw new Error('Toggle compound components must be used within <Toggle />');
}
return context;
}

export default ToggleContext;

2. Toggle.js

import { useState } from 'react';
import ToggleContext from './ToggleContext';

export function Toggle({ children }) {
const [on, setOn] = useState(false);
const toggle = () => setOn((prev) => !prev);

return <ToggleContext.Provider value={{ on, toggle }}>{children}</ToggleContext.Provider>;
}

Toggle.On = function ToggleOn({ children }) {
const { on } = useToggleContext();
return on ? children : null;
};

Toggle.Off = function ToggleOff({ children }) {
const { on } = useToggleContext();
return !on ? children : null;
};

Toggle.Button = function ToggleButton() {
const { on, toggle } = useToggleContext();
return <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>;
};

3. App.js(使用方式)

import { Toggle } from './Toggle';

export default function App() {
return (
<div>
<h1>Compound Component 範例</h1>
<Toggle>
<Toggle.On>狀態為:開啟</Toggle.On>
<Toggle.Off>狀態為:關閉</Toggle.Off>
<Toggle.Button />
</Toggle>
</div>
);
}

延伸說明

  • Context 的角色ToggleContext 扮演資訊橋梁,讓所有子元件能共用 on 狀態與 toggle 函式。
  • 錯誤處理useToggleContext 中若未包在 <Toggle> 裡使用,會主動拋出錯誤,提醒開發者正確使用上下文。
  • 可擴充性:我們可以進一步加入 <Toggle.Icon><Toggle.Label> 等元件,複用相同邏輯,保持一致行為。
  • 比傳統 Props 傳遞更清晰:使用 <Toggle.On> 等方式比 showOn={true} 更語意化且易於理解。

總結

Compound Component 是 React 開發中一個極具表達力與彈性的設計模式,尤其適用於 UI 元件庫的開發。透過 Context 的使用,我們能解耦邏輯與顯示元件,提升可讀性、可維護性與開發效率。雖然初期設計較為繁瑣,但一旦建立模式後,能帶來長遠效益,尤其在大型專案或多人協作的情境下更具價值。


延伸學習

  • React.cloneElement() 的進階應用(自動注入 props)
  • TypeScript 搭配 Compound Component 的型別設計
  • 使用 Zustand、Jotai 或 Redux 搭配 Compound Component

SQL 入門語法教學筆記 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

SQL 是操作關聯式資料庫使用的語法。以下介紹常用 SQL 入門語法:

一、資料庫基本概念

資料庫 (Database):儲存資料的容器。 資料表 (Table):儲存資料的表格,每列 (row) 為一筆紀錄,每欄 (column) 為一種資料屬性。


二、基本 SQL 語法

1. 建立資料庫

CREATE DATABASE my_database;

2. 使用資料庫

USE my_database;

3. 建立資料表

CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
email VARCHAR(100),
age INT
);

4. 查詢資料

查詢所有欄位

SELECT * FROM users;

選擇特定欄位

SELECT name, email FROM users;

5. 插入資料

INSERT INTO users (name, email, age) VALUES ('John', 'john@example.com', 25);

6. 更新資料

UPDATE users SET age = 26 WHERE id = 1;

7. 刪除資料

DELETE FROM users WHERE id = 1;

三、條件查詢

1. WHERE 條件

SELECT * FROM users WHERE age > 20;

2. AND、OR、NOT

SELECT * FROM users WHERE age > 20 AND name = 'John';
SELECT * FROM users WHERE age > 20 OR age < 18;
SELECT * FROM users WHERE NOT age = 25;

3. LIKE 模糊查詢

SELECT * FROM users WHERE name LIKE 'J%';  -- 以J開頭
SELECT * FROM users WHERE email LIKE '%@gmail.com';

4. ORDER BY 排序

SELECT * FROM users ORDER BY age ASC;  -- 遞增排序
SELECT * FROM users ORDER BY age DESC; -- 遞減排序

5. LIMIT 限制筆數

SELECT * FROM users LIMIT 5;

四、聚合函數

1. 計算筆數

SELECT COUNT(*) FROM users;

2. 最大/最小值

SELECT MAX(age) FROM users;
SELECT MIN(age) FROM users;

3. 平均/總和

SELECT AVG(age) FROM users;
SELECT SUM(age) FROM users;

五、分組查詢

1. GROUP BY

GROUP BY 查詢欄位僅能包含 GROUP BY 和聚合函數

SELECT age, COUNT(*) FROM users GROUP BY age;

2. HAVING 搭配 GROUP BY 當作查詢條件

SELECT age, COUNT(*) FROM users GROUP BY age HAVING COUNT(*) > 1;

六、資料表連接 (JOIN)

1. INNER JOIN

內連接,僅返回兩個資料表中「符合交集條件」的資料。

SELECT users.id, users.name, orders.amount 
FROM users
INNER JOIN orders ON users.id = orders.user_id;

2. LEFT JOIN

左連接,返回左表 (users) 所有資料,即使右表 (orders) 無對應資料,也會顯示左表資料,右表無資料則會顯示 NULL。

SELECT users.id, users.name, orders.amount 
FROM users
LEFT JOIN orders ON users.id = orders.user_id;

3. RIGHT JOIN

右連接,返回右表 (orders) 所有資料,即使左表 (users) 無對應資料,也會顯示右表資料,左表無資料則會顯示 NULL。

SELECT users.id, users.name, orders.amount 
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;

4. FULL JOIN (部分資料庫支援)

全外連接,返回兩個表中所有資料,無對應資料則顯示 NULL。(MySQL 不支援 FULL JOIN,需使用 UNION 模擬)

SELECT users.id, users.name, orders.amount 
FROM users
FULL JOIN orders ON users.id = orders.user_id;

七、合併查詢 (UNION)

1. UNION

SELECT name, email FROM users WHERE age > 30
UNION
SELECT name, email FROM users WHERE age < 20;

UNION 用於合併兩個或多個查詢的結果。 預設會去除重複資料。 欄位數量與型態須一致。

2. UNION ALL

SELECT name, email FROM users WHERE age > 30
UNION ALL
SELECT name, email FROM users WHERE age < 20;

與 UNION 類似,但不會去除重複資料。


以上整理了 SQL 入門常見的基本語法,可以基本處理資料庫操作需求。

將 Vue 3 應用部署到 GitHub Pages 入門語法教學筆記 | 學習筆記

· 閱讀時間約 2 分鐘
kdchang

要將 Vue 3 應用部署到 GitHub Pages,這裡有一個詳細的步驟說明,指導你如何使用 gh-pages 部署你的應用。

1. 安裝 gh-pages 套件

首先,你需要安裝 gh-pages 套件來將你的專案部署到 GitHub Pages。

在專案目錄中執行以下命令:

npm install --save-dev gh-pages

2. 配置 vite.config.js

為了將專案部署到 GitHub Pages,你需要配置 vite.config.js 文件中的 base 屬性,讓它使用 GitHub 的存儲庫名稱作為基本路徑。

打開 vite.config.js,並根據你的 GitHub 用戶名和存儲庫名稱來設定 base 屬性。假設你的 GitHub 存儲庫名稱是 my-vue-app,配置應該是:

// vite.config.js
import { defineConfig } from "vite";

export default defineConfig({
base: "/my-vue-app/", // 用你的 GitHub repository 名稱替換
});

3. 更新 package.json

你需要在 package.json 中添加兩個腳本來處理部署。打開 package.json,並在 scripts 部分添加 predeploydeploy 腳本:

"scripts": {
"dev": "vite",
"build": "vite build",
"predeploy": "npm run build",
"deploy": "gh-pages -d dist"
}
  • predeploy 會先執行 npm run build,這會構建你的應用。
  • deploy 會將 dist 目錄(即構建後的文件)推送到 GitHub Pages。

4. 設定 GitHub Pages

如果你還沒設定過 GitHub Pages,請先確保你的 GitHub 存儲庫設定了 GitHub Pages。

  1. 在 GitHub 上打開你的存儲庫。
  2. 點擊 Settings
  3. 滾動到 Pages 部分,並將 Source 設定為 gh-pages 分支。

5. 部署到 GitHub Pages

完成上述配置後,你就可以將專案部署到 GitHub Pages 了。

執行以下命令來構建並部署專案:

npm run deploy

gh-pages 會自動將構建後的 dist 目錄推送到 gh-pages 分支。你可以在 GitHub 上查看部署情況。

6. 訪問你的應用

部署完成後,你可以使用以下 URL 來訪問你的應用:

https://<你的 GitHub 用戶名>.github.io/my-vue-app/

請將 <你的 GitHub 用戶名>my-vue-app 替換為你實際的 GitHub 用戶名和存儲庫名稱。

7. 自動化部署(可選)

如果我們希望每次推送代碼時自動部署到 GitHub Pages,你可以使用 GitHub Actions 來自動化這個過程。GitHub 提供了許多現成的 Actions 來進行自動部署,像是 GitHub Action for deploying to GitHub Pages

JavaScript ES6 入門語法教學筆記 | 學習筆記

· 閱讀時間約 5 分鐘
kdchang

ECMAScript 6 又稱 ECMAScript 2015,是 JavaScript 語言的新一代標準,讓 JavaScript 可以更容易撰寫大型複雜的應用程式並避免不必要的錯誤。

以下介紹常用 ES6 入門語法:

一、let & const 變數宣告

  1. let:用於宣告變數,可重新賦值。
let name = 'John';
name = 'Mike'; // 可以重新賦值
  1. const:用於宣告常數,賦值後不可更改。
const pi = 3.14;
pi = 3.1415; // 會報錯

建議預設使用 const,僅需變更時使用 let。兩者作用域為 block scope

ES6 中,let區塊作用域(Block Scope) 是它與舊有的 var 最大的不同之一。


什麼是 Block Scope(區塊作用域)

  • 使用 let 宣告的變數,只能在該程式區塊 {} 內部存取。
  • 區塊作用域指的是任何用 {} 包起來的範圍,例如:
    • ifforwhile 等程式區塊。
    • 一般 {} 花括號內的區域。

範例說明

1. let 在區塊內的作用範圍

{
let x = 10;
console.log(x); // 10
}
console.log(x); // ReferenceError: x is not defined
  • x{} 區塊內宣告,僅在該區塊內有效。
  • 區塊外存取會出錯。

2. var 沒有區塊作用域(舊語法對比)

{
var y = 20;
console.log(y); // 20
}
console.log(y); // 20
  • var 沒有區塊作用域,y 雖在 {} 內宣告,但可在區塊外存取。

3. for 迴圈中的 let

for (let i = 0; i < 3; i++) {
console.log(i); // 0, 1, 2
}
console.log(i); // ReferenceError: i is not defined
  • i 只在 for 迴圈內有效。

let 的區塊作用域優點

  1. 避免變數污染:let 限制變數在區塊內,避免影響區塊外的程式碼。
  2. 防止重複定義:同一區塊內不能重複宣告相同變數。
    let a = 1;
    let a = 2; // SyntaxError: Identifier 'a' has already been declared
  3. 更安全、可預期的變數管理。

總結

關鍵字區塊作用域重複宣告提升(Hoisting)行為
let不可提升但不初始化(TDZ)
var提升並初始化 undefined

建議盡量用 letconst,避免使用 var
這樣可以減少潛在的 bug,也符合現代 JavaScript 開發的最佳實踐。


二、模板字串(Template Literals)

以前字串串變數要使用 +,現在可以使用反引號 (``) 定義字串,可插入變數。

const name = 'John';
const age = 25;
console.log(`我叫 ${name},今年 ${age}`);

三、箭頭函式(Arrow Functions)

  1. 基本語法:
const add = (a, b) => {
return a + b;
}
  1. 簡寫形式:
const add = (a, b) => a + b;
  1. 單一參數可省略括號:
const square = n => n * n;

箭頭函式不會綁定自己的 this,繼承外層作用域的 this


沒錯!這句話是 箭頭函式(Arrow Function) 很重要的特性之一,這裡幫你拆解得更清楚一點:


什麼是 this

this 代表函式執行時所屬的物件,依照函式被呼叫的方式不同,this 的值也會不同。

例如:

function normalFunction() {
console.log(this);
}
normalFunction(); // 在瀏覽器環境中,this 會是 window 物件

如果這個函式被某個物件呼叫:

const obj = {
name: 'John',
sayHi: function() {
console.log(this.name);
}
};
obj.sayHi(); // John,this 指向 obj

箭頭函式的 this 特性

箭頭函式不會綁定自己的 this,它會「繼承外層作用域」的 this

也就是說:

  • 傳統函式:this 依賴呼叫方式來決定。
  • 箭頭函式:this 取決於箭頭函式宣告時所在的外層作用域的 this

範例說明:

傳統函式 vs 箭頭函式

const obj = {
name: 'John',
normalFunc: function() {
console.log(this.name); // this 指向 obj
},
arrowFunc: () => {
console.log(this.name); // this 指向外層(通常是 window 或 undefined)
}
};

obj.normalFunc(); // John
obj.arrowFunc(); // undefined(或瀏覽器中可能是 window.name)

常見應用場景:回呼函式(callback)中的 this

假設我們有一個計時器:

const obj = {
name: 'John',
timer: function() {
setTimeout(function() {
console.log(this.name); // undefined 或 window.name
}, 1000);
}
};

obj.timer();

因為 setTimeout 裡的傳統函式,它的 this 在執行時會指向 window

若改用箭頭函式:

const obj = {
name: 'John',
timer: function() {
setTimeout(() => {
console.log(this.name); // John
}, 1000);
}
};

obj.timer();

箭頭函式不會綁定自己的 this,會繼承 timer 函式的 this,因此會正確印出 John


常見疑問

為什麼箭頭函式不綁定自己的 this

主要是為了解決回呼函式中 this 易出錯的問題

以前會這樣解法:

const that = this; // 變數 that 保存正確的 this
setTimeout(function() {
console.log(that.name);
}, 1000);

現在有箭頭函式,就不用這麼麻煩。


小結

類型this 綁定方式一般用途
傳統函式 function執行時決定物件方法、建構函式
箭頭函式 =>定義時決定callback 回呼函式、內部函式需要使用外部 this 的情境

總結:

  • 一般物件方法用傳統函式。this 由呼叫的物件決定
  • callback 回呼函式、內部函式用箭頭函式。

這樣就可以避免大部分 this 的混亂狀況!

四、解構賦值(Destructuring)

  1. 陣列解構:
const arr = [1, 2, 3];
const [a, b, c] = arr;
  1. 物件解構:
const person = { name: 'John', age: 25 };
const { name, age } = person;

五、展開運算符(Spread Operator)

  1. 陣列展開:
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
  1. 物件展開:
const obj1 = { name: 'John', age: 25 };
const obj2 = { ...obj1, city: 'Taipei' };

六、預設參數(Default Parameters)

函式參數可設定預設值:

const greet = (name = '訪客') => {
console.log(`Hello, ${name}!`);
}
greet(); // Hello, 訪客!
greet('John'); // Hello, John!

這些 ES6 基礎語法,是現代 JavaScript 開發的常用技巧,掌握這些概念能大幅提升程式撰寫效率。

Web 資訊安全(Security)簡明入門教學指南

· 閱讀時間約 7 分鐘
kdchang

隨著越來越多的服務和資料連上網路,Web 資訊安全已經是 Web 開發中一個重要的環節,然而許多開發者往往希望專注在應用程式的研發,而忽略了資訊安全的重要性。不過若是沒有嚴謹地考慮資訊安全的問題,等到事情發生後反而會造成更嚴重的財務和名譽上的損失。本文希望整理一些 Web 常見資訊安全(Security)的議題和學習資源和讀者一起教學相長,下次建構網路服務時可以更留心 Web 的資訊安全,甚至努力成為一個好的白帽駭客(White Hat Hacker)。

常見 Web 資訊安全(Security)議題

一般而言 Web 資訊安全(Security)需要符合三點安全要素:

  1. 保密性:透過加密等方法確保資料的保密性
  2. 完整性:要求使用者取得的資料是完整不可被竄改的
  3. 可用性:保證網站服務的持續可訪問性

以下列出常見影響 Web 資訊安全(Security)的攻擊手法:

  1. SQL Injection

    使用惡意的 SQL 語法去影響資料庫內容:

    // -- 為忽略掉後面的 SQL
    /user/profile?id=1";DROP TABLE user--

    SELECT * FROM USER WHERE id = "1"; DROP TABLE user--

    使用者登入:

    // password" AND 1=1--
    SELECT * FROM USER WHERE username = "Mark"; AND 1=1-- AND PASSWORD="1234"

    簡易防範方式:不信任使用者輸入的資料,確保使用者輸入都要 escape 掉,目前許多成熟 Web 框架都有支援 ORM 服務,大部分都基本防範了 SQL Injection。

  2. XSS(Cross-Site Scripting) XSS 亦即將惡意程式碼注入到網頁,讓看到網頁的使用者會受影響,常見的受災戶包括論壇、討論區等網路服務。事實上 XSS 概念很簡單,透過表單輸入建立一些惡意網址、惡意圖片網址或是 JavsScript 程式碼在 HTML 中注入,當使用者觀看頁面時即會觸發。

    <img src="" onerror="alert('XSS')" />

    更多關於 XSS 資料可以參考 XSS Filter Evasion Cheat Sheet。另外也有簡體中文版

    簡易防範方式:不信任使用者輸入的資料,將所有輸入內容編碼並過濾。

  3. CSRF

    CSRF 跨站請求偽造 又被稱為 one-click attack 或者 session riding,通常縮寫為 CSRF 或者 XSRF, 是一種挾制用戶在當前已登入的 Web 應用程式上執行非本意的操作的攻擊方法。

    舉維基百科上的例子:假如一家銀行用以執行轉帳操作的 URL 地址如下:

    http://www.examplebank.com/withdraw?account=AccoutName&amount=10000&for=PayeeName

    那麼,一個惡意攻擊者可以在另一個網站上放置如下代碼:

    <img src="http://www.examplebank.com/withdraw?account=Mark&amount=10000&for=Bob">

    若是使用者的登入資訊尚未過期的話就會損失 10000 元的金額。

    簡易防範方式:

    1. 檢查 Referer 欄位 這是比較基本的驗證方式,通常 HTTP 標頭中有一個 Referer 欄位,其應該和 Request 位置在同一個網域下,因此可以透過驗證是否是在同一個網域來驗證是否為惡意的請求,但會有被更改偽裝的可能。

    2. 添加驗證 token 一般現在許多的 Web Framework 都有提供在表單加入由 Server 生成的隨機驗證 CSRF 的碼,可以協助防止 CSRF 攻擊。

  4. DoS Dos 阻斷式攻擊(Denial of Service Attack)又稱為洪水攻擊,是一種網路攻擊手法,其目的在於使目標電腦的網路或系統資源耗盡,使服務暫時中斷或停止,導致真正的使用者無法使用服務。

    根據維基百科:DoS 攻擊可以具體分成兩種形式:頻寬消耗型 以及 資源消耗型,它們都是透過大量合法或偽造的請求占用大量網路以及器材資源,以達到癱瘓網路以及系統的目的。

    頻寬消耗攻擊又分洪泛攻擊或放大攻擊:洪泛攻擊的特點是利用殭屍程式傳送大量流量至受損的受害者系統,目的在於堵塞其頻寬。放大攻擊和洪泛攻擊類似,是通過惡意放大流量限制受害者系統的頻寬;其特點是利用殭屍程式通過偽造的源 IP(即攻擊目標)向某些存在漏洞的伺服器傳送請求,伺服器在處理請求後向偽造的源 IP 傳送應答,由於這些服務的特殊性導致應答包比請求包更長,因此使用少量的頻寬就能使伺服器傳送大量的 Response 到目標主機上。

    資源消耗型又分為協定分析攻擊(SYN flood,SYN 洪水)、LAND attack、CC 攻擊、殭屍網路攻擊、Application level floods(應用程式級洪水攻擊)等。

    簡易防範方式:

    1. 防火牆 設定規則阻擋簡單攻擊

    2. 交換機 大多交換機有限制存取控制功能

    3. 路由器 大多路由器有限制存取控制功能

    4. 黑洞啟動 將請求轉到空介面或是不存在的位置

  5. 檔案上傳漏洞 許多網路應用程式可以讓使用者上傳檔案到伺服器端,由於我們不知道使用者會上傳什麼類型的檔案,若不留意就會造成很大的問題。

    簡單防範方式:

    1. 阻止非法文件上傳

      • 設定檔名白名單
      • 文件標頭判斷
    2. 阻止非法文件執行

      • 存儲目錄與 Web 應用分離
      • 存儲目錄無執行權限
      • 文件重命名
      • 圖片壓縮
  6. 加密安全 有許多的網路服務有提供會員註冊的服務,當使用者使用註冊時注意不要將密碼明碼存入資料庫。若是你使用的服務會在忘記密碼時寄明碼密碼給你很有可能該服務就是使用明碼加密,此時就很容易會榮登我的密碼沒加密的網站。不過儘管將密碼加密也未必安全,像是網路上就存在一些破解網站彩虹表 可以破解加密的密碼。所以通常我們會針對不同使用者使用隨機產生的 salt 字串來加鹽後加密的方式來提高密碼的強健性。

    sha3(salt + gap + password)

簡易資安入侵流程

  1. 偵查(Reconnaissance) 攻擊者準備攻擊之前進行的調查,使用 Google 或是社交工程找尋目標的相關資訊,以利之後的攻擊

  2. 掃描(Scanning) 掃描目標主機的弱點,取得主機作業系統、服務和運作狀況等相關資訊

  3. 取得權限(Gaining Access) 利用系統弱點取得主機權限

  4. 維持權限(Maintaining Access) 維持目前取得的權限,以便日後再次存取而不需繁雜的步驟

  5. 清除足跡(Clearing Tracks) 清除入侵的痕跡

總結

以上整理一些 Web 常見資訊安全(Security)的議題和學習資源和讀者一起教學相長,成為一個好的白帽駭客(White Hat Hacker)。隨著網路科技的發展,資訊安全的議題只會越來越重要,當下次當有產品要上線到正式環境時,不妨使用 The Security Checklist 確認一下有哪些資安注意事項是我們沒有注意到的。

延伸閱讀

  1. Web Security 網站安全基礎篇(一)
  2. Web Security 網站安全基礎篇(二)
  3. 3 個免費的 Web 資訊安全自動化測試工具
  4. HITCON 2016 投影片 - Bug Bounty 獎金獵人甘苦談 那些年我回報過的漏洞
  5. FallibleInc/security-guide-for-developers
  6. [資訊安全]防範 Cross-Site-Scripting(XSS)
  7. 網站防範 XSS 攻擊的關鍵思考