設計模式
設計模式
指令模式
範例
code:javascript
class OrderManager() {
constructor() {
this.orders = []
}
placeOrder(order, id) {
this.orders.push(id)
return You have successfully ordered ${order} (${id});
}
trackOrder(id) {
return Your order ${id} will arrive in 20 minutes.
}
cancelOrder(id) {
this.orders = this.orders.filter(order => order.id !== id)
return You have canceled your order ${id}
}
}
const manager = new OrderManager();
manager.placeOrder("Pad Thai", "1234");
manager.trackOrder("1234");
manager.cancelOrder("1234");
假如日後決定將placeOrder改為addOrder,其他所有同樣呼叫的地方都要更改
重構
code:javascript
class OrderManager {
constructor() {
this.orders = [];
}
execute(command, ...args) {
return command.execute(this.orders, ...args);
}
}
class Command {
constructor(execute) {
this.execute = execute;
}
}
function PlaceOrderCommand(order, id) {
return new Command((orders) => {
orders.push(id);
return You have successfully ordered ${order} (${id});
});
}
function CancelOrderCommand(id) {
return new Command((orders) => {
orders = orders.filter((order) => order.id !== id);
return You have canceled your order ${id};
});
}
function TrackOrderCommand(id) {
return new Command(() => Your order ${id} will arrive in 20 minutes.);
}
const manager = new OrderManager();
manager.execute(new PlaceOrderCommand("Pad Thai", "1234"));
manager.execute(new TrackOrderCommand("1234"));
manager.execute(new CancelOrderCommand("1234"));
使用情景有限、多數情況只會增加boilerplate
工廠模式
使用工廠函式(factory functions)來建立新物件
回傳新物件,且不使用new關鍵字
code:javascript
const createUser = ({ firstName, lastName, email }) => ({
firstName,
lastName,
email,
fullName() {
return ${this.firstName} ${this.lastName};
},
});
優點
適合建立多個共享相同屬性的較小物件
能根據環境條件或使用者配置輕鬆回傳自訂物件
缺點
某些情況下,直接使用類別(class)與實例化可能更省記憶體
code:javascript
class User {
constructor(firstName, lastName, email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
fullName() {
return ${this.firstName} ${this.lastName};
}
}
const user1 = new User("John", "Doe", "john@doe.com");
const user2 = new User("Jane", "Doe", "jane@doe.com");
享元模式
在建立大量相似物件時,節省記憶體
code:javascript
class Book {
constructor(title, author, isbn) {
this.title = title;
this.author = author;
this.isbn = isbn;
}
}
const isbnNumbers = new Set();
const bookList = [];
const addBook = (title, author, isbn, availability, sales) => {
const book = {
...createBook(title, author, isbn),
sales,
availability,
isbn
};
bookList.push(book);
return book;
};
const createBook = (title, author, isbn) => {
const book = isbnNumbers.has(isbn);
if (book) {
return book;
} else {
const book = new Book(title, author, isbn);
isbnNumbers.add(isbn);
return book;
}
};
addBook("Harry Potter", "JK Rowling", "AB123", false, 100);
addBook("Harry Potter", "JK Rowling", "AB123", true, 50);
addBook("To Kill a Mockingbird", "Harper Lee", "CD345", true, 10);
addBook("To Kill a Mockingbird", "Harper Lee", "CD345", false, 20);
addBook("The Great Gatsby", "F. Scott Fitzgerald", "EF567", false, 20);
console.log("副本總數: ", bookList.length);
console.log("書籍總數: ", isbnNumbers.size);
仲介者/中介模式
讓元件透過一個中央點(仲介者)彼此互動
仲介者通常只是一個物件(object literal)或函式
範例:Express.js
code:javascript
const app = require("express")();
const html = require("./data");
app.use(
"/",
(req, res, next) => {
next();
},
(req, res, next) => {
console.log(Request has test header: ${!!req.headers["test-header"]});
next();
}
);
app.get("/", (req, res) => {
res.set("Content-Type", "text/html");
res.send(Buffer.from(html));
});
app.listen(8080, function() {
console.log("Server is running on 8080");
});
混入模式
在不使用繼承的情況下,將可重複使用的功能加入到另一個物件或類別中
例如setTimeout、setInterval、indexedDB、isSecureContext
都是WindowOrWorkerGlobalScope和WindowEventHandlers的mixin
模組模式
未使用export匯出的變數則會為私有變數
使用預設匯出(default export)時,匯入時不需加上大括號
使用*可匯入整個模組
使用import()可依據條件在有需求時才載入模組
觀察者模式
observers
陣列,儲存所有事件發生時會被通知的觀察者
subscribe()
新增觀察者到清單
unsubscribe()
從清單中移除觀察者
notify()
特定事件發生時,通知所有觀察者
code:javascript
import React from "react";
import { Button, Switch, FormControlLabel } from "@material-ui/core";
import { ToastContainer, toast } from "react-toastify";
import observable from "./Observable";
function handleClick() {
observable.notify("User clicked button!");
}
function handleToggle() {
observable.notify("User toggled switch!");
}
function logger(data) {
console.log(${Date.now()} ${data});
}
function toastify(data) {
toast(data, {
position: toast.POSITION.BOTTOM_RIGHT,
closeButton: false,
autoClose: 2000,
});
}
observable.subscribe(logger);
observable.subscribe(toastify);
export default function App() {
return (
<div className="App">
<Button variant="contained" onClick={handleClick}>
Click me!
</Button>
<FormControlLabel
control={<Switch name="" onChange={handleToggle} />}
label="Toggle me!"
/>
<ToastContainer />
</div>
);
}
優點
缺點
若觀察者邏輯過於複雜,通知所有訂閱者時可能會造成效能問題
原型模式
在多個相同型別的物件之間共享屬性
在JavaScript中,原型是種內建的物件,可透過原型鏈被其他物件存取
code:javascript
class Dog {
constructor(name) {
this.name = name;
}
bark() {
return Woof!;
}
}
const dog1 = new Dog("Daisy");
const dog2 = new Dog("Max");
const dog3 = new Dog("Spot");
// 後續新增方法到 prototype
Dog.prototype.play = () => console.log("Playing now!");
dog1.play(); // Playing now!
提供者模式
我們可以透過props將資料傳遞給元件,但如果所有元件都需要某個props值,就會變得相當麻煩
props drilling
把元件包在一個Provider裡,就可直接被多個元件存取
code:javascript
const DataContext = React.createContext()
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}
優點
不必一層一層傳遞props
降低重構時出錯的風險
更容易共享全域狀態
缺點
過度使用可能導致效能問題
消費Context的元件在狀態更新時都會重新算繪
頻繁更新的值被傳遞給過多元件,會影響效能
代理模式
透過Proxy物件,對與特定物件的互動擁有更多控制權 例如在取得某個值或設定某個值時的行為
code:javascript
const person = {
name: "John Doe",
age: 42,
nationality: "American",
};
const personProxy = new Proxy(person, {
get: (obj, prop) => {
console.log(Hmm.. this property doesn't seem to exist);
} else {
console.log(The value of ${prop} is ${obj[prop]});
}
},
set: (obj, prop, value) => {
if (prop === "age" && typeof value !== "number") {
console.log(Sorry, you can only pass numeric values for age.);
} else if (prop === "name" && value.length < 2) {
console.log(You need to provide a valid name.);
} else {
console.log(Changed ${prop} from ${obj[prop]} to ${value}.);
}
return true;
},
});
personProxy.name; // 存取 name
personProxy.age = 43; // 修改 age
personProxy.nonExistentProperty; // 嘗試存取不存在的屬性
personProxy.age = "44"; // 嘗試設定錯誤類型
personProxy.name = ""; // 嘗試設定空名字
單例模式
只能被實例化一次的類別,且可在全域範圍中存取
code:javascript
let instance;
let counter = 0;
class Counter {
constructor() {
if (instance) {
throw new Error("You can only create one instance!");
}
instance = this;
}
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}
const counter1 = new Counter();
const counter2 = new Counter(); // Error: You can only create one instance!
適合在需要全域狀態的的情境,但過度依賴會帶來可維護性與測試上的困難,需謹慎使用
靜態匯入
預設情況下,靜態匯入的所有模組都會被加入到初始的bundle中
動態匯入(dynamic import)可以依使用者互動再算繪,或延遲到頁面下方再算繪
算繪模式
介紹
一種模式對某個使用案例有利,但對另一個可能不適用
靜態算繪
純靜態算繪
最基本的方法,適用於幾乎沒有動態內容的頁面
TTFB極快,FCP和LCP也很快,且不會造成版面偏移
靜態算繪+客戶端fetch
頁面載入後,客戶端再從API抓取資料並更新顯示
提供良好的TTFB與FCP,但LCP表現欠佳
需在資料抓取後才能顯示,可能也會導致版面偏移
每次頁面請求都會呼叫 API,可能增加伺服器成本
靜態算繪+getStaticProps
在建構階段於伺服器端抓取資料
對大型站點(數百頁)會增加建構時間
若使用外部API也可能觸發請求限制或高額使用費
增量式靜態重建
Incremental Static Regeneration
允許部分頁面預算繪,動態頁面按需生成
按需增量式靜態重建
On-demand ISR
客戶端FCP與LCP接近,避免版面偏移
每次請求都需生成頁面,TTFB較靜態渲染長,且伺服器成本高
Edge SSR+HTTP Streaming
群島架構
在SSR中,只為小而專注的互動區塊提供JavaScript 在客戶端重新水合(rehydrate)
讓頁面先伺服器端算繪所有內容,但對於動態內容保留佔位符
模板式靜態網站生成器(如 Jekyll、Hugo)支援將靜態元件算繪到頁面上
大多數現代JavaScript框架也支援同構算繪(isomorphic rendering)
動畫化視圖過渡
效能模式
拆分bundle
壓縮JavaScript
動態匯入
互動時匯入
可見時匯入
最佳化載入順序
預抓取
Prefetch
預載入
Preload
PRPL模式
最佳化第三方資源載入
樹搖
清單虛擬化