Nuxt.js组件基础 #

一、组件概述 #

Nuxt.js 的组件系统基于 Vue.js,并提供了自动导入功能。在 components/ 目录下创建的组件会自动注册,无需手动导入。

二、创建组件 #

2.1 基本组件 #

components/AppButton.vue

vue
<template>
  <button :class="['btn', `btn-${variant}`]" :disabled="disabled">
    <slot />
  </button>
</template>

<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'secondary' | 'danger'
  disabled?: boolean
}

withDefaults(defineProps<Props>(), {
  variant: 'primary',
  disabled: false
})
</script>

<style scoped>
.btn {
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
  cursor: pointer;
  font-size: 1rem;
  transition: all 0.2s;
}

.btn-primary {
  background: #3498db;
  color: white;
}

.btn-secondary {
  background: #95a5a6;
  color: white;
}

.btn-danger {
  background: #e74c3c;
  color: white;
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>

2.2 使用组件 #

vue
<template>
  <div>
    <AppButton>默认按钮</AppButton>
    <AppButton variant="secondary">次要按钮</AppButton>
    <AppButton variant="danger">危险按钮</AppButton>
  </div>
</template>

三、自动导入机制 #

3.1 命名规则 #

Nuxt.js 根据目录结构自动生成组件名称:

text
components/
├── AppButton.vue          → <AppButton />
├── AppHeader.vue          → <AppHeader />
├── blog/
│   ├── Card.vue           → <BlogCard />
│   └── List.vue           → <BlogList />
└── ui/
    ├── Button.vue         → <UiButton />
    └── Input.vue          → <UiInput />

3.2 目录前缀 #

目录名作为组件名前缀:

  • components/blog/Card.vueBlogCard
  • components/ui/Button.vueUiButton

3.3 禁用自动导入前缀 #

nuxt.config.ts 中配置:

typescript
export default defineNuxtConfig({
  components: [
    {
      path: '~/components',
      pathPrefix: false
    }
  ]
})

配置后:

  • components/blog/Card.vueCard

3.4 自定义组件目录 #

typescript
export default defineNuxtConfig({
  components: [
    '~/components',
    {
      path: '~/shared/components',
      prefix: 'Shared',
      global: true
    }
  ]
})

四、组件Props #

4.1 定义Props #

vue
<script setup lang="ts">
interface Props {
  title: string
  count?: number
  items: string[]
  config: {
    theme: string
    size: 'small' | 'medium' | 'large'
  }
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})
</script>

4.2 Props验证 #

vue
<script setup lang="ts">
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0,
    validator: (value: number) => value >= 0
  },
  status: {
    type: String as PropType<'active' | 'inactive'>,
    default: 'active'
  }
})
</script>

4.3 使用Props #

vue
<template>
  <UserCard
    title="用户信息"
    :count="10"
    :items="['item1', 'item2']"
    :config="{ theme: 'dark', size: 'medium' }"
  />
</template>

五、组件事件 #

5.1 定义事件 #

vue
<script setup lang="ts">
interface Emits {
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
  (e: 'change', event: Event): void
}

const emit = defineEmits<Emits>()

const handleClick = () => {
  emit('update', 'new value')
}
</script>

5.2 使用v-model #

vue
<script setup lang="ts">
interface Props {
  modelValue: string
}

interface Emits {
  (e: 'update:modelValue', value: string): void
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

const updateValue = (event: Event) => {
  const target = event.target as HTMLInputElement
  emit('update:modelValue', target.value)
}
</script>

<template>
  <input
    :value="modelValue"
    @input="updateValue"
  />
</template>

5.3 多个v-model #

vue
<script setup lang="ts">
interface Props {
  firstName: string
  lastName: string
}

interface Emits {
  (e: 'update:firstName', value: string): void
  (e: 'update:lastName', value: string): void
}

defineProps<Props>()
const emit = defineEmits<Emits>()
</script>

<template>
  <input
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

六、组件插槽 #

6.1 默认插槽 #

vue
<template>
  <div class="card">
    <slot />
  </div>
</template>

使用:

vue
<template>
  <AppCard>
    <p>卡片内容</p>
  </AppCard>
</template>

6.2 具名插槽 #

vue
<template>
  <div class="card">
    <header class="card-header">
      <slot name="header" />
    </header>
    <main class="card-body">
      <slot />
    </main>
    <footer class="card-footer">
      <slot name="footer" />
    </footer>
  </div>
</template>

使用:

vue
<template>
  <AppCard>
    <template #header>
      <h2>标题</h2>
    </template>
    
    <p>内容</p>
    
    <template #footer>
      <button>操作</button>
    </template>
  </AppCard>
</template>

6.3 作用域插槽 #

vue
<script setup lang="ts">
interface Item {
  id: number
  name: string
}

const items: Item[] = [
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' }
]
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="item.id">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

使用:

vue
<template>
  <ItemList>
    <template #default="{ item, index }">
      <span>{{ index }}: {{ item.name }}</span>
    </template>
  </ItemList>
</template>

七、动态组件 #

7.1 使用component标签 #

vue
<script setup lang="ts">
import { resolveComponent } from 'vue'

const currentComponent = ref('AppButton')

const components = {
  AppButton: resolveComponent('AppButton'),
  AppInput: resolveComponent('AppInput')
}
</script>

<template>
  <component :is="components[currentComponent]" />
</template>

7.2 动态组件切换 #

vue
<script setup lang="ts">
const tabs = ['Home', 'Profile', 'Settings']
const activeTab = ref('Home')
</script>

<template>
  <div>
    <button
      v-for="tab in tabs"
      :key="tab"
      @click="activeTab = tab"
    >
      {{ tab }}
    </button>
    
    <component :is="`${activeTab}Tab`" />
  </div>
</template>

八、异步组件 #

8.1 懒加载组件 #

Nuxt.js 提供了 Lazy 前缀来懒加载组件:

vue
<template>
  <div>
    <LazyHeavyComponent v-if="showHeavy" />
    <button @click="showHeavy = true">加载组件</button>
  </div>
</template>

<script setup lang="ts">
const showHeavy = ref(false)
</script>

8.2 懒加载带loading #

vue
<template>
  <div>
    <LazyHeavyComponent v-if="showHeavy">
      <template #fallback>
        <div>加载中...</div>
      </template>
    </LazyHeavyComponent>
  </div>
</template>

8.3 手动导入 #

vue
<script setup lang="ts">
const HeavyComponent = defineAsyncComponent(() =>
  import('~/components/HeavyComponent.vue')
)
</script>

九、组件最佳实践 #

9.1 组件分类 #

text
components/
├── base/           # 基础组件
│   ├── Button.vue
│   ├── Input.vue
│   └── Icon.vue
├── layout/         # 布局组件
│   ├── Header.vue
│   ├── Footer.vue
│   └── Sidebar.vue
├── features/       # 功能组件
│   ├── SearchBar.vue
│   └── UserMenu.vue
└── ui/             # UI组件
    ├── Modal.vue
    ├── Toast.vue
    └── Dropdown.vue

9.2 单文件组件结构 #

vue
<template>
  <div class="component">
  </div>
</template>

<script setup lang="ts">
interface Props {}

interface Emits {}

defineProps<Props>()
defineEmits<Emits>()
</script>

<style scoped>
.component {
}
</style>

9.3 组件文档 #

vue
<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'secondary'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
}

withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'medium',
  disabled: false
})
</script>

<template>
  <button :disabled="disabled">
    <slot />
  </button>
</template>

十、完整示例 #

10.1 数据表格组件 #

components/DataTable.vue

vue
<template>
  <div class="data-table">
    <table>
      <thead>
        <tr>
          <th
            v-for="column in columns"
            :key="column.key"
            @click="column.sortable && handleSort(column.key)"
          >
            {{ column.label }}
            <span v-if="column.sortable && sortKey === column.key">
              {{ sortOrder === 'asc' ? '↑' : '↓' }}
            </span>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(row, index) in sortedData" :key="index">
          <td v-for="column in columns" :key="column.key">
            <slot
              :name="`cell-${column.key}`"
              :row="row"
              :value="row[column.key]"
            >
              {{ row[column.key] }}
            </slot>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts">
interface Column {
  key: string
  label: string
  sortable?: boolean
}

interface Props {
  columns: Column[]
  data: Record<string, any>[]
}

const props = defineProps<Props>()

const sortKey = ref<string | null>(null)
const sortOrder = ref<'asc' | 'desc'>('asc')

const handleSort = (key: string) => {
  if (sortKey.value === key) {
    sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
  } else {
    sortKey.value = key
    sortOrder.value = 'asc'
  }
}

const sortedData = computed(() => {
  if (!sortKey.value) return props.data
  
  return [...props.data].sort((a, b) => {
    const aVal = a[sortKey.value!]
    const bVal = b[sortKey.value!]
    
    if (aVal < bVal) return sortOrder.value === 'asc' ? -1 : 1
    if (aVal > bVal) return sortOrder.value === 'asc' ? 1 : -1
    return 0
  })
})
</script>

<style scoped>
.data-table table {
  width: 100%;
  border-collapse: collapse;
}

.data-table th,
.data-table td {
  padding: 0.75rem;
  border: 1px solid #e2e8f0;
  text-align: left;
}

.data-table th {
  background: #f7fafc;
  cursor: pointer;
}
</style>

使用:

vue
<template>
  <DataTable :columns="columns" :data="users">
    <template #cell-actions="{ row }">
      <button @click="editUser(row)">编辑</button>
      <button @click="deleteUser(row)">删除</button>
    </template>
  </DataTable>
</template>

<script setup lang="ts">
const columns = [
  { key: 'name', label: '姓名', sortable: true },
  { key: 'email', label: '邮箱', sortable: true },
  { key: 'role', label: '角色' },
  { key: 'actions', label: '操作' }
]

const users = ref([
  { name: '张三', email: 'zhang@example.com', role: '管理员' },
  { name: '李四', email: 'li@example.com', role: '用户' }
])
</script>

十一、总结 #

本章介绍了 Nuxt.js 组件基础:

  • 组件自动导入机制
  • 定义和使用 Props
  • 组件事件和 v-model
  • 插槽的使用
  • 动态组件和异步组件
  • 组件最佳实践

组件是构建应用的基本单元,下一章我们将学习组件通信。

最后更新:2026-03-28