翻译:Vue 和 Nuxt 中的 Atomic Design

翻译:Vue 和 Nuxt 中的 Atomic Design

原文地址:Atomic Architecture: Revolutionizing Vue and Nuxt Project Structure,作者是Alex,一位来自德国的开发人员。在 25 年的 Vue Nation 上有演讲。


简介

清晰的写作需要清晰的思考。这同样适用于编码。在启动个人项目时,将所有组件放入一个文件夹可能会起作用。但随着项目的增长,尤其是对于较大的团队,这种方法会导致问题:

  • 重复的代码
  • 超大的多用途组件
  • 难以测试的代码

Atomic Design 提供了一个解决方案。让我们看看如何将其应用于 Nuxt 项目。

什么是原子设计

Brad Frost 开发了 Atomic Design 作为创建设计系统的方法。它分为五个级别,灵感来自化学:

  1. 原子:基本构建块(例如表单标签、输入、按钮)
  2. 分子:简单的 UI 元素组(例如搜索表单)
  3. 生物体:由分子/原子制成的复杂成分(例如头部)
  4. 模板:页面级布局
  5. 页面:包含内容的模板的特定实例
提醒

为了更好地探索原子设计原则,我建议阅读 Brad Frost 的博客文章:原子 Web 设计

对于 Nuxt,我们可以调整这些定义:

  • 原子:纯净的单一用途组件
  • 分子:具有最小逻辑的原子组合
  • 生物体:更大的、独立的、可重复使用的组件
  • 模板:定义页面结构的 Nuxt 布局
  • 页面:处理数据和 API 调用的组件

生物体与分子:有什么区别?

分子和生物体可能会令人困惑。以下是考虑它们的简单方法:

  • 分子小而简单。它们就像乐高积木,可以卡在一起。例子:
    • 搜索栏(输入 + 按钮)
    • 登录表单(用户名输入 + 密码输入 + 提交按钮)
    • 星级评定(5 个星形图标 + 评定量编号)
  • 生物体更大、更复杂。它们就像预先构建的乐高套装。例子:
    • 完整的网站标题(徽标 + 导航菜单 + 搜索栏)
    • 商品卡(图片 + 标题 + 价格 + 加入购物车按钮)
    • 评论部分(评论表单 + 评论列表)

请记住:分子是生物体的一部分,但生物体可以独立工作。

代码示例:之前和之后

考虑这个非 Atomic Design 待办事项应用程序组件

这种方法会导致大型、难以维护的组件。让我们使用 Atomic Design 进行重构:

这将是重构的结构

text
📐 Template (Layout)
   │
   └─── 📄 Page (TodoApp)
        │
        └─── 📦 Organism (TodoList)
             │
             ├─── 🧪 Molecule (TodoForm)
             │    │
             │    ├─── ⚛️ Atom (BaseInput)
             │    └─── ⚛️ Atom (BaseButton)
             │
             └─── 🧪 Molecule (TodoItems)
                  │
                  └─── 🧪 Molecule (TodoItem) [multiple instances]
                       │
                       ├─── ⚛️ Atom (BaseText)
                       └─── ⚛️ Atom (BaseButton)

重构的组件

模板
vue
<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>
页面
生物体 (TodoList)
TodoList.vuevue
<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)
TodoForm.vuevue
<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>
TodoItem.vuevue
<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)
BaseButton.vuevue
<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>
BaseInput.vuevue
<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 之外的方法,当您的项目超出其初始结构时。

近日小结【随机犯病篇】
翻译:如何选择 Vue 项目架构