Nội dung

DaiPhan

DaiPhan

Full-Stack Developer

Full-stack developer passionate about modern web technologies, best practices, and sharing knowledge with the community.

Skills & Expertise

JavaScript TypeScript React Node.js DevOps
150+
Articles
50k+
Readers
4.9
Rating

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.