原文地址:Atomic Architecture: Revolutionizing Vue and Nuxt Project Structure,作者是Alex,一位来自德国的开发人员。在 25 年的 Vue Nation
上有演讲。
简介
清晰的写作需要清晰的思考。这同样适用于编码。在启动个人项目时,将所有组件放入一个文件夹可能会起作用。但随着项目的增长,尤其是对于较大的团队,这种方法会导致问题:
- 重复的代码
- 超大的多用途组件
- 难以测试的代码
Atomic Design 提供了一个解决方案。让我们看看如何将其应用于 Nuxt 项目。
什么是原子设计
Brad Frost 开发了 Atomic Design 作为创建设计系统的方法。它分为五个级别,灵感来自化学:
- 原子:基本构建块(例如表单标签、输入、按钮)
- 分子:简单的 UI 元素组(例如搜索表单)
- 生物体:由分子/原子制成的复杂成分(例如头部)
- 模板:页面级布局
- 页面:包含内容的模板的特定实例
提醒
为了更好地探索原子设计原则,我建议阅读 Brad Frost 的博客文章:原子 Web 设计
对于 Nuxt,我们可以调整这些定义:
- 原子:纯净的单一用途组件
- 分子:具有最小逻辑的原子组合
- 生物体:更大的、独立的、可重复使用的组件
- 模板:定义页面结构的 Nuxt 布局
- 页面:处理数据和 API 调用的组件
生物体与分子:有什么区别?
分子和生物体可能会令人困惑。以下是考虑它们的简单方法:
- 分子小而简单。它们就像乐高积木,可以卡在一起。例子:
- 搜索栏(输入 + 按钮)
- 登录表单(用户名输入 + 密码输入 + 提交按钮)
- 星级评定(5 个星形图标 + 评定量编号)
- 生物体更大、更复杂。它们就像预先构建的乐高套装。例子:
- 完整的网站标题(徽标 + 导航菜单 + 搜索栏)
- 商品卡(图片 + 标题 + 价格 + 加入购物车按钮)
- 评论部分(评论表单 + 评论列表)
请记住:分子是生物体的一部分,但生物体可以独立工作。
代码示例:之前和之后
考虑这个非 Atomic Design 待办事项应用程序组件
<script setup lang="ts"> import { onMounted, ref } from 'vue' interface Todo { id: number text: string } const newTodo = ref('') const todos = ref<Todo[]>([]) async function fetchTodos() { // Simulating API call todos.value = [ { id: 1, text: 'Learn Vue.js' }, { id: 2, text: 'Build a Todo App' }, { id: 3, text: 'Study Atomic Design' } ] } async function addTodo() { if (newTodo.value.trim()) { // Simulating API call const newTodoItem: Todo = { id: Date.now(), text: newTodo.value } todos.value.push(newTodoItem) newTodo.value = '' } } async function deleteTodo(id: number) { // Simulating API call todos.value = todos.value.filter(todo => todo.id !== id) } onMounted(fetchTodos) </script> <template> <div class="container mx-auto p-4"> <h1 class="text-2xl font-bold mb-4 text-gray-800 dark:text-gray-200"> Todo App </h1> <!-- Add Todo Form --> <form class="mb-4" @submit.prevent="addTodo"> <input v-model="newTodo" type="text" placeholder="Enter a new todo" class="border p-2 mr-2 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded" > <button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white p-2 rounded transition duration-300"> Add Todo </button> </form> <!-- Todo List --> <ul class="space-y-2"> <li v-for="todo in todos" :key="todo.id" class="flex justify-between items-center p-3 bg-gray-100 dark:bg-gray-700 rounded shadow-sm" > <span class="text-gray-800 dark:text-gray-200">{{ todo.text }}</span> <button class="bg-red-500 hover:bg-red-600 text-white p-1 rounded transition duration-300" @click="deleteTodo(todo.id)" > Delete </button> </li> </ul> </div> </template>
这种方法会导致大型、难以维护的组件。让我们使用 Atomic Design 进行重构:
这将是重构的结构
📐 Template (Layout) │ └─── 📄 Page (TodoApp) │ └─── 📦 Organism (TodoList) │ ├─── 🧪 Molecule (TodoForm) │ │ │ ├─── ⚛️ Atom (BaseInput) │ └─── ⚛️ Atom (BaseButton) │ └─── 🧪 Molecule (TodoItems) │ └─── 🧪 Molecule (TodoItem) [multiple instances] │ ├─── ⚛️ Atom (BaseText) └─── ⚛️ Atom (BaseButton)
重构的组件
模板
<script setup lang="ts"> import ThemeToggle from '~/components/ThemeToggle.vue' </script> <template> <div class="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-300"> <header class="bg-white dark:bg-gray-800 shadow"> <nav class="container mx-auto px-4 py-4 flex justify-between items-center"> <NuxtLink to="/" class="text-xl font-bold"> Todo App </NuxtLink> <ThemeToggle /> </nav> </header> <main class="container mx-auto px-4 py-8"> <slot /> </main> </div> </template>
页面
<script setup lang="ts"> import { onMounted, ref } from 'vue' import TodoList from '../components/organisms/TodoList' interface Todo { id: number text: string } const todos = ref<Todo[]>([]) async function fetchTodos() { // Simulating API call todos.value = [ { id: 1, text: 'Learn Vue.js' }, { id: 2, text: 'Build a Todo App' }, { id: 3, text: 'Study Atomic Design' } ] } async function addTodo(text: string) { // Simulating API call const newTodoItem: Todo = { id: Date.now(), text } todos.value.push(newTodoItem) } async function deleteTodo(id: number) { // Simulating API call todos.value = todos.value.filter(todo => todo.id !== id) } onMounted(fetchTodos) </script> <template> <div class="container mx-auto p-4"> <h1 class="text-2xl font-bold mb-4 text-gray-800 dark:text-gray-200"> Todo App </h1> <TodoList :todos="todos" @add-todo="addTodo" @delete-todo="deleteTodo" /> </div> </template>
生物体 (TodoList)
<script setup lang="ts"> import TodoForm from '../molecules/TodoForm.vue' import TodoItem from '../molecules/TodoItem.vue' interface Todo { id: number text: string } defineProps<{ todos: Todo[] }>() defineEmits<{ (e: 'add-todo', value: string): void (e: 'delete-todo', id: number): void }>() </script> <template> <div> <TodoForm @add-todo="$emit('add-todo', $event)" /> <ul class="space-y-2"> <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" @delete-todo="$emit('delete-todo', $event)" /> </ul> </div> </template>
分子(TodoForm 和 TodoItem)
<script setup lang="ts"> import TodoForm from '../molecules/TodoForm.vue' import TodoItem from '../molecules/TodoItem.vue' interface Todo { id: number text: string } defineProps<{ todos: Todo[] }>() defineEmits<{ (e: 'add-todo', value: string): void (e: 'delete-todo', id: number): void }>() </script> <template> <div> <TodoForm @add-todo="$emit('add-todo', $event)" /> <ul class="space-y-2"> <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" @delete-todo="$emit('delete-todo', $event)" /> </ul> </div> </template>
<script setup lang="ts"> import { ref } from 'vue' import BaseButton from '../atoms/BaseButton.vue' import BaseInput from '../atoms/BaseInput.vue' const emit = defineEmits<{ (e: 'add-todo', value: string): void }>() const newTodo = ref('') function addTodo() { if (newTodo.value.trim()) { emit('add-todo', newTodo.value) newTodo.value = '' } } </script> <template> <form class="mb-4" @submit.prevent="addTodo"> <BaseInput v-model="newTodo" placeholder="Enter a new todo" /> <BaseButton type="submit"> Add Todo </BaseButton> </form> </template>
原子(BaseButton, BaseInput, BaseText)
<script setup lang="ts"> defineProps<{ variant?: 'primary' | 'danger' }>() </script> <template> <button class="p-2 rounded transition duration-300" :class="[ variant === 'danger' ? 'bg-red-500 hover:bg-red-600 text-white' : 'bg-blue-500 hover:bg-blue-600 text-white', ]" > <slot /> </button> </template>
<script setup lang="ts"> defineProps<{ modelValue: string placeholder?: string }>() defineEmits<{ (e: 'update:modelValue', value: string): void }>() </script> <template> <input :value="modelValue" type="text" :placeholder="placeholder" class="border p-2 mr-2 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded" @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)" > </template>
提醒
想亲自查看完整示例吗?点击我
组件级别 | 工作 | 示例 |
---|---|---|
原子 | 纯净的单一用途组件 | BaseButton BaseInput BaseIcon BaseText |
分子 | 具有最小逻辑的原子组合 | SearchBar LoginForm StarRating Tooltip |
生物体 | 更大的、独立的、可重用的组件。可以执行副作用和复杂的作。 | TheHeader ProductCard CommentSection NavigationMenu |
模板 | 定义页面结构的 Nuxt 布局 | DefaultLayout BlogLayout DashboardLayout AuthLayout |
页面 | 处理数据和 API 调用的组件 | HomePage UserProfile ProductList CheckoutPage |
摘要
原子设计提供了一条通往更明显的代码结构的途径。它可以很好地作为许多项目的起点。但随着复杂性的增加,其他架构可能会更好地为您服务。想要探索更多选项?阅读我的帖子 如何选择 Vue 项目架构.它涵盖了 Atomic Design 之外的方法,当您的项目超出其初始结构时。
