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ức | Thực hành |
---|---|
Tách component | TodoItem, TodoList, AddTodo |
Tách store | todoStore.js dùng Pinia |
Chia layout | Views: HomeView , AddView |
Router riêng | Tách router.js |
Tổ chức code chuẩn | Giống production thật sự |
Thảo luận