系列文章: 前端工程師的 Modern Web 實踐之道 - Day 8
預計閱讀時間: 10 分鐘
難度等級: ⭐⭐⭐☆☆
在前一篇文章中,我們建立了完整的現代化前端工作流。今天我們將深入探討現代前端開發的核心概念——元件化思維,這個概念將徹底改變你對前端架構的理解。
還記得那個用 jQuery 撐起整個前端世界的年代嗎?那時候我們這樣寫程式碼:
// jQuery 時代的典型寫法(2010-2015)
$(document).ready(function() {
// 初始化輪播圖
$('.carousel').slick({
autoplay: true,
dots: true,
arrows: false
});
// 表單驗證
$('#contact-form').validate({
rules: {
email: {
required: true,
email: true
}
}
});
// 彈窗功能
$('.modal-trigger').click(function() {
var target = $(this).data('target');
$(target).modal('show');
});
// 更多功能...
});
看起來沒什麼問題對吧?但當專案越來越大,問題就出現了:
傳統 jQuery 開發的核心痛點:
window
或 $
上現代元件化不僅僅是語法的改變,更是思維模式的徹底革命:
// 現代元件化思維(2023+)
interface CarouselProps {
images: string[];
autoplay?: boolean;
showDots?: boolean;
showArrows?: boolean;
}
interface CarouselState {
currentSlide: number;
isPlaying: boolean;
}
class Carousel {
private state: CarouselState;
private props: CarouselProps;
private container: HTMLElement;
constructor(container: HTMLElement, props: CarouselProps) {
this.props = { showDots: true, showArrows: true, ...props };
this.state = { currentSlide: 0, isPlaying: !!props.autoplay };
this.container = container;
this.init();
}
private init(): void {
this.render();
this.bindEvents();
this.props.autoplay && this.startAutoplay();
}
private render(): void {
this.container.innerHTML = `
<div class="carousel-wrapper">
<div class="carousel-slides">
${this.props.images.map((src, index) =>
`<img src="${src}" class="carousel-slide ${index === this.state.currentSlide ? 'active' : ''}" />`
).join('')}
</div>
${this.props.showDots ? this.renderDots() : ''}
${this.props.showArrows ? this.renderArrows() : ''}
</div>
`;
}
private setState(newState: Partial<CarouselState>): void {
this.state = { ...this.state, ...newState };
this.render();
}
// 清理資源,避免記憶體洩漏
public destroy(): void {
this.container.innerHTML = '';
// 清理事件監聽器
}
}
// ❌ 違反單一職責原則
class SuperWidget {
// 處理資料獲取
async fetchData() { /* ... */ }
// 處理 UI 渲染
render() { /* ... */ }
// 處理表單驗證
validate() { /* ... */ }
// 處理動畫
animate() { /* ... */ }
// 處理 SEO
updateMetaTags() { /* ... */ }
}
// ✅ 遵循單一職責原則
class DataService {
async fetchUserData(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return response.json();
}
}
class UserCard {
constructor(
private container: HTMLElement,
private dataService: DataService
) {}
async render(userId: string): Promise<void> {
try {
const user = await this.dataService.fetchUserData(userId);
this.container.innerHTML = this.template(user);
} catch (error) {
this.renderError(error);
}
}
private template(user: User): string {
return `
<div class="user-card">
<img src="${user.avatar}" alt="${user.name}" />
<h3>${user.name}</h3>
<p>${user.email}</p>
</div>
`;
}
}
// 基礎元件接口定義
interface Component {
render(): void;
destroy(): void;
}
// 可複用的基礎元件
class BaseComponent implements Component {
constructor(protected container: HTMLElement) {}
render(): void {
throw new Error('Render method must be implemented');
}
destroy(): void {
this.container.innerHTML = '';
}
protected emit(eventName: string, data?: any): void {
this.container.dispatchEvent(
new CustomEvent(eventName, { detail: data })
);
}
protected on(eventName: string, handler: EventListener): void {
this.container.addEventListener(eventName, handler);
}
}
// 具體的可複用元件
class SearchBox extends BaseComponent {
private searchTerm: string = '';
render(): void {
this.container.innerHTML = `
<div class="search-box">
<input type="text" placeholder="搜尋..." />
<button type="submit">搜尋</button>
</div>
`;
this.bindEvents();
}
private bindEvents(): void {
const input = this.container.querySelector('input')!;
const button = this.container.querySelector('button')!;
input.addEventListener('input', (e) => {
this.searchTerm = (e.target as HTMLInputElement).value;
this.emit('search:input', { term: this.searchTerm });
});
button.addEventListener('click', () => {
this.emit('search:submit', { term: this.searchTerm });
});
}
}
// 元件組合使用
class ProductSearchPage {
private searchBox: SearchBox;
private resultsList: ProductList;
constructor(container: HTMLElement) {
const searchContainer = container.querySelector('.search-section')!;
const resultsContainer = container.querySelector('.results-section')!;
this.searchBox = new SearchBox(searchContainer);
this.resultsList = new ProductList(resultsContainer);
this.bindComponentEvents();
}
private bindComponentEvents(): void {
this.searchBox.on('search:submit', (event: CustomEvent) => {
this.resultsList.search(event.detail.term);
});
}
}
// 狀態管理的現代化解決方案
class ComponentState<T> {
private state: T;
private listeners: Array<(state: T) => void> = [];
constructor(initialState: T) {
this.state = { ...initialState };
}
public getState(): T {
return { ...this.state };
}
public setState(updates: Partial<T>): void {
this.state = { ...this.state, ...updates };
this.notifyListeners();
}
public subscribe(listener: (state: T) => void): () => void {
this.listeners.push(listener);
// 返回取消訂閱函式
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
};
}
private notifyListeners(): void {
this.listeners.forEach(listener => listener(this.state));
}
}
// 實際使用範例
interface TodoState {
items: Array<{ id: string; text: string; completed: boolean }>;
filter: 'all' | 'active' | 'completed';
}
class TodoApp extends BaseComponent {
private state: ComponentState<TodoState>;
private unsubscribe: () => void;
constructor(container: HTMLElement) {
super(container);
this.state = new ComponentState<TodoState>({
items: [],
filter: 'all'
});
// 訂閱狀態變化,自動重新渲染
this.unsubscribe = this.state.subscribe(() => {
this.render();
});
}
render(): void {
const { items, filter } = this.state.getState();
const filteredItems = this.filterItems(items, filter);
this.container.innerHTML = `
<div class="todo-app">
<header>
<input type="text" placeholder="新增待辦事項..." class="new-todo" />
</header>
<main>
${filteredItems.map(item => this.todoTemplate(item)).join('')}
</main>
<footer>
${this.filterTemplate()}
</footer>
</div>
`;
this.bindEvents();
}
private addTodo(text: string): void {
const { items } = this.state.getState();
const newItem = {
id: Date.now().toString(),
text: text.trim(),
completed: false
};
this.state.setState({
items: [...items, newItem]
});
}
public destroy(): void {
this.unsubscribe();
super.destroy();
}
}
interface LifecycleHooks {
beforeCreate?(): void;
created?(): void;
beforeMount?(): void;
mounted?(): void;
beforeUpdate?(): void;
updated?(): void;
beforeDestroy?(): void;
destroyed?(): void;
}
abstract class LifecycleComponent implements Component, LifecycleHooks {
private _mounted: boolean = false;
constructor(protected container: HTMLElement) {
this.beforeCreate?.();
this.created?.();
}
public mount(): void {
if (this._mounted) return;
this.beforeMount?.();
this.render();
this._mounted = true;
this.mounted?.();
}
public update(): void {
if (!this._mounted) return;
this.beforeUpdate?.();
this.render();
this.updated?.();
}
public destroy(): void {
if (!this._mounted) return;
this.beforeDestroy?.();
this.container.innerHTML = '';
this._mounted = false;
this.destroyed?.();
}
abstract render(): void;
}
// 實際使用
class DataTable extends LifecycleComponent {
private data: any[] = [];
created(): void {
console.log('DataTable created');
}
async mounted(): void {
console.log('DataTable mounted, loading data...');
await this.loadData();
}
beforeDestroy(): void {
console.log('DataTable will be destroyed, cleaning up...');
// 清理定時器、事件監聽器等
}
render(): void {
// 渲染邏輯
}
}
// 服務接口定義
interface ApiService {
get<T>(url: string): Promise<T>;
post<T>(url: string, data: any): Promise<T>;
}
interface NotificationService {
show(message: string, type: 'success' | 'error'): void;
}
// 具體實作
class HttpApiService implements ApiService {
async get<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
}
async post<T>(url: string, data: any): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
}
// 依賴注入容器
class DIContainer {
private services = new Map<string, any>();
register<T>(name: string, service: T): void {
this.services.set(name, service);
}
resolve<T>(name: string): T {
const service = this.services.get(name);
if (!service) {
throw new Error(`Service ${name} not found`);
}
return service;
}
}
// 可測試的元件
class UserManagementComponent extends BaseComponent {
constructor(
container: HTMLElement,
private apiService: ApiService,
private notificationService: NotificationService
) {
super(container);
}
async loadUsers(): Promise<void> {
try {
const users = await this.apiService.get<User[]>('/api/users');
this.renderUsers(users);
} catch (error) {
this.notificationService.show('載入使用者失敗', 'error');
}
}
// 測試友善的方法
private renderUsers(users: User[]): void {
// 渲染邏輯與外部依賴分離
}
}
// 單元測試範例
class MockApiService implements ApiService {
async get<T>(): Promise<T> {
return [] as any;
}
async post<T>(): Promise<T> {
return {} as any;
}
}
// 測試中使用
const mockApi = new MockApiService();
const mockNotification = { show: jest.fn() };
const component = new UserManagementComponent(
document.createElement('div'),
mockApi,
mockNotification
);