Frontend Architecture Patterns: MVC, MVVM và Component-Based
So sánh các kiến trúc frontend phổ biến và khi nào sử dụng
Frontend Architecture Patterns: MVC, MVVM và Component-Based
Kiến trúc frontend đóng vai trò quan trọng trong việc xây dựng ứng dụng scalable và maintainable. Hiểu rõ các patterns giúp bạn chọn đúng architecture cho project.
MVC (Model-View-Controller)
Structure
Model View Controller
----- ---- ----------
Data ← UI ← User Input
│ │ │
└──────┐ │ │
│ │ │
▼ ▼ ▼
Business Logic → Update
Implementation với Vanilla JavaScript
// Model - Data và Business Logic
class UserModel {
constructor() {
this.users = [];
this.observers = [];
}
addUser(user) {
this.users.push(user);
this.notifyObservers();
}
notifyObservers() {
this.observers.forEach(observer => observer.update());
}
addObserver(observer) {
this.observers.push(observer);
}
}
// View - UI Rendering
class UserView {
constructor() {
this.controller = null;
}
setController(controller) {
this.controller = controller;
}
render(users) {
const userList = document.getElementById('user-list');
userList.innerHTML = users.map(user => `
<div class="user-item">
<span>${user.name}</span>
<button onclick="controller.deleteUser('${user.id}')">Delete</button>
</div>
`).join('');
}
}
// Controller - User Input và Coordination
class UserController {
constructor(model, view) {
this.model = model;
this.view = view;
this.view.setController(this);
this.model.addObserver(this.view);
}
addUser(name) {
const user = { id: Date.now(), name };
this.model.addUser(user);
}
deleteUser(id) {
this.model.users = this.model.users.filter(u => u.id !== id);
this.model.notifyObservers();
}
}
// Initialize
const model = new UserModel();
const view = new UserView();
const controller = new UserController(model, view);
MVVM (Model-View-ViewModel)
Structure
Model View ViewModel
----- ---- ---------
Data ← UI ← Data Binding
│ │ │
└──────┐ │ │
│ │ │
▼ ▼ ▼
Business Logic → Commands
Implementation với Vue.js
// Model
const userModel = {
users: [
{ id: 1, name: 'John', age: 25 },
{ id: 2, name: 'Jane', age: 30 }
],
getUsers() {
return this.users;
},
addUser(user) {
this.users.push({ ...user, id: Date.now() });
}
};
// ViewModel (Vue Component)
new Vue({
el: '#app',
data() {
return {
users: [],
newUser: { name: '', age: 0 }
};
},
computed: {
userCount() {
return this.users.length;
},
averageAge() {
return this.users.reduce((sum, user) => sum + user.age, 0) / this.users.length || 0;
}
},
methods: {
addUser() {
if (this.newUser.name && this.newUser.age > 0) {
userModel.addUser(this.newUser);
this.loadUsers();
this.newUser = { name: '', age: 0 };
}
},
loadUsers() {
this.users = userModel.getUsers();
}
},
mounted() {
this.loadUsers();
}
});
// View (HTML Template)
<div id="app">
<h2>Users ({{ userCount }})</h2>
<p>Average Age: {{ averageAge.toFixed(1) }}</p>
<form @submit.prevent="addUser">
<input v-model="newUser.name" placeholder="Name" required>
<input v-model.number="newUser.age" type="number" placeholder="Age" required>
<button type="submit">Add User</button>
</form>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.age }} years old
</li>
</ul>
</div>
Component-Based Architecture
React Implementation
// UserList Component
const UserList = ({ users, onDeleteUser }) => {
return (
<div className="user-list">
{users.map(user => (
<UserItem
key={user.id}
user={user}
onDelete={() => onDeleteUser(user.id)}
/>
))}
</div>
);
};
// UserItem Component
const UserItem = ({ user, onDelete }) => {
return (
<div className="user-item">
<span>{user.name}</span>
<button onClick={onDelete}>Delete</button>
</div>
);
};
// UserForm Component
const UserForm = ({ onAddUser }) => {
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (name.trim()) {
onAddUser({ id: Date.now(), name });
setName('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter user name"
/>
<button type="submit">Add User</button>
</form>
);
};
// App Component (Container)
const App = () => {
const [users, setUsers] = useState([]);
const addUser = (user) => {
setUsers(prevUsers => [...prevUsers, user]);
};
const deleteUser = (userId) => {
setUsers(prevUsers => prevUsers.filter(user => user.id !== userId));
};
return (
<div className="app">
<h1>User Management</h1>
<UserForm onAddUser={addUser} />
<UserList users={users} onDeleteUser={deleteUser} />
</div>
);
};
Architecture Comparison
When to Use MVC?
✅ Use MVC when:
- Simple applications
- Server-side rendering
- Clear separation needed
- Team familiar with pattern
❌ Avoid MVC when:
- Complex UI interactions
- Real-time updates needed
- Heavy client-side logic
When to Use MVVM?
✅ Use MVVM when:
- Data binding required
- Reactive UI updates
- Form-heavy applications
- Two-way binding needed
❌ Avoid MVVM when:
- Performance critical
- Simple UI logic
- Memory constraints
When to Use Component-Based?
✅ Use Component-Based when:
- Reusable UI components
- Large applications
- Team collaboration
- Modern frameworks
❌ Avoid Component-Based when:
- Very small projects
- Performance critical
- Simple static pages
Modern Hybrid Approach
Combining Patterns
// Component-based with MVVM (Vue.js)
export default {
name: 'UserProfile',
// Model (data)
data() {
return {
user: null,
loading: false,
error: null
};
},
// ViewModel (computed properties và methods)
computed: {
fullName() {
return `${this.user.firstName} ${this.user.lastName}`;
},
isAdult() {
return this.user.age >= 18;
}
},
methods: {
async fetchUser() {
this.loading = true;
try {
const response = await api.getUser(this.$route.params.id);
this.user = response.data;
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
},
updateUser(updatedData) {
this.user = { ...this.user, ...updatedData };
}
},
// Lifecycle
async created() {
await this.fetchUser();
}
};
Best Practices
Component Design Principles
// Single Responsibility
const UserAvatar = ({ user }) => (
<img src={user.avatar} alt={user.name} />
);
// Reusability
const Button = ({ variant, size, children, onClick }) => (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
>
{children}
</button>
);
// Composition over Inheritance
const Card = ({ header, body, footer }) => (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{body}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
State Management
// Local state for component-specific data
const [count, setCount] = useState(0);
// Global state for shared data (Context API)
const UserContext = createContext();
// External state management (Redux)
const store = createStore(rootReducer, applyMiddleware(thunk));
Performance Considerations
Optimization Techniques
// Memoization
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{/* expensive computation */}</div>;
});
// Lazy loading
const LazyComponent = lazy(() => import('./HeavyComponent'));
// Virtual scrolling for large lists
import { FixedSizeList } from 'react-window';
const LargeList = ({ items }) => (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={50}
width={300}
>
{({ index, style }) => (
<div style={style}>
{items[index]}
</div>
)}
</FixedSizeList>
);
Kết luận
Không có “one-size-fits-all” architecture. Chọn architecture phù hợp dựa trên:
- Project complexity
- Team experience
- Performance requirements
- Maintenance needs
Bài viết này là phần đầu tiên trong series về frontend architecture. Trong các bài tiếp theo, chúng ta sẽ explore micro-frontends và server-side rendering strategies.