Đăng nhập

BÀI 14: TÁCH COMPONENT & TỔ CHỨC DỰ ÁN VUE CHUẨN

Cấu trúc dự án Vue chuẩn + Tách component chuyên nghiệp là một kỹ năng quan trọng nếu bạn muốn xây dựng ứng dụng lớn, bảo trì dễ dàng và quy chuẩn như dev chuyên nghiệp.

Mục tiêu:

  • Hiểu cách tổ chức project Vue như chuyên gia
  • Tách component rõ ràng: TodoItem.vue, TodoList.vue, AddTodo.vue
  • Tạo project thật bằng Vite (hoặc CDN nếu bạn chưa cài Node)

Phần 1: Cấu trúc thư mục chuẩn (khi dùng Vite)

todo-app/
├── public/
├── src/
│   ├── components/
│   │   ├── TodoList.vue
│   │   ├── TodoItem.vue
│   │   └── AddTodo.vue
│   ├── store/
│   │   └── todoStore.js
│   ├── views/
│   │   ├── HomeView.vue
│   │   └── AddView.vue
│   ├── App.vue
│   ├── main.js
│   └── router.js
├── index.html
└── package.json

Phần 2: Tạo Project mới (nếu dùng Vite)

npm create vite@latest todo-app --template vue
cd todo-app
npm install
npm install pinia vue-router
npm run dev

Phần 3: Tách Component – Ví dụ thực tế

src/components/TodoItem.vue

<template>
  <div class="todo" :class="{ done: todo.completed }">
    <input type="checkbox" :checked="todo.completed" @change="$emit('toggle', todo.id)">
    {{ todo.title }}
    <button @click="$emit('delete', todo.id)">❌</button>
  </div>
</template>

<script setup>
defineProps(['todo'])
defineEmits(['toggle', 'delete'])
</script>

<style scoped>
.todo {
  padding: 10px;
  border: 1px solid #ccc;
  margin-bottom: 8px;
  border-radius: 5px;
}
.done {
  text-decoration: line-through;
  color: gray;
}
</style>

src/components/TodoList.vue

<template>
  <div>
    <p v-if="store.loading">⏳ Đang tải...</p>
    <p v-if="store.error" class="error">❌ {{ store.error }}</p>
    <TodoItem
      v-for="todo in store.todos"
      :key="todo.id"
      :todo="todo"
      @toggle="store.toggleTodo"
      @delete="store.deleteTodo"
    />
  </div>
</template>

<script setup>
import { useTodoStore } from '@/store/todoStore'
import { onMounted } from 'vue'
import TodoItem from './TodoItem.vue'

const store = useTodoStore()
onMounted(() => store.fetchTodos())
</script>

src/components/AddTodo.vue

<template>
  <div>
    <input v-model="newText" placeholder="Nhập công việc..." />
    <button @click="handleAdd">Thêm</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useTodoStore } from '@/store/todoStore'

const newText = ref('')
const store = useTodoStore()

function handleAdd() {
  if (newText.value.trim()) {
    store.addTodo(newText.value.trim())
    newText.value = ''
  }
}
</script>

src/store/todoStore.js

import { defineStore } from 'pinia'

export const useTodoStore = defineStore('todo', {
  state: () => ({
    todos: [],
    loading: false,
    error: null
  }),
  actions: {
    async fetchTodos() {
      this.loading = true
      try {
        const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5')
        this.todos = await res.json()
      } catch (e) {
        this.error = 'Lỗi khi gọi API'
      } finally {
        this.loading = false
      }
    },
    async addTodo(title) {
      const res = await fetch('https://jsonplaceholder.typicode.com/todos', {
        method: 'POST',
        body: JSON.stringify({ title, completed: false }),
        headers: { 'Content-Type': 'application/json' }
      })
      const data = await res.json()
      this.todos.unshift(data)
    },
    async deleteTodo(id) {
      await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
        method: 'DELETE'
      })
      this.todos = this.todos.filter(todo => todo.id !== id)
    },
    async toggleTodo(id) {
      const todo = this.todos.find(t => t.id === id)
      const updated = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
        method: 'PUT',
        body: JSON.stringify({ ...todo, completed: !todo.completed }),
        headers: { 'Content-Type': 'application/json' }
      })
      todo.completed = (await updated.json()).completed
    }
  }
})

src/views/HomeView.vue

<template>
  <div>
    <h3>📋 Danh sách công việc</h3>
    <TodoList />
  </div>
</template>

<script setup>
import TodoList from '@/components/TodoList.vue'
</script>

src/views/AddView.vue

<template>
  <div>
    <h3>➕ Thêm công việc mới</h3>
    <AddTodo />
  </div>
</template>

<script setup>
import AddTodo from '@/components/AddTodo.vue'
</script>

src/router.js

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'
import AddView from '@/views/AddView.vue'

export default createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: HomeView },
    { path: '/add', component: AddView }
  ]
})

src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'

const app = createApp(App)
app.use(router)
app.use(createPinia())
app.mount('#app')

src/App.vue

<template>
  <h2>Todo App (Vite + Pinia)</h2>
  <nav>
    <router-link to="/">Trang chủ</router-link> |
    <router-link to="/add">Thêm mới</router-link>
  </nav>
  <router-view></router-view>
</template>

<style>
nav a {
  margin-right: 10px;
  text-decoration: none;
}
</style>

Tổng kết bạn đã học:

Kiến thứcThực hành
Tách componentTodoItem, TodoList, AddTodo
Tách storetodoStore.js dùng Pinia
Chia layoutViews: HomeView, AddView
Router riêngTách router.js
Tổ chức code chuẩnGiống production thật sự

Thảo luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Đăng ký nhận tin mới

Nhận bài học, tài nguyên và cơ hội việc làm qua email hàng tuần.

Chúng tôi cam kết không spam. Bạn có thể hủy bất cứ lúc nào.