Vue 3 模块化组件封装完全指南

引言

在现代前端开发中,模块化组件是构建可维护和可扩展应用的关键。本文将详细介绍 Vue 3 中组件封装的最佳实践,从基础结构到高级特性,帮助你构建高质量的可复用组件。

基础组件结构

1. 目录结构规范

推荐的组件目录结构:

components/
  └── MyComponent/
      ├── index.ts          # 组件入口文件
      ├── src/
      │   ├── types.ts      # 类型定义
      │   ├── hooks.ts      # 组件相关的钩子函数
      │   ├── constants.ts  # 常量定义
      │   └── utils.ts      # 工具函数
      ├── styles/
      │   └── index.scss    # 样式文件
      └── __tests__/        # 测试文件目录
          └── index.test.ts

2. 组件基础模板

// types.ts
export interface Props {
  title?: string
  type?: 'primary' | 'secondary'
  onClick?: (e: MouseEvent) => void
}

// index.ts
import { defineComponent } from 'vue'
import type { Props } from './src/types'

export default defineComponent({
  name: 'MyComponent',
  props: {
    title: {
      type: String,
      default: ''
    },
    type: {
      type: String as () => Props['type'],
      default: 'primary'
    }
  },
  emits: ['click'],
  setup(props, { emit }) {
    const handleClick = (e: MouseEvent) => {
      emit('click', e)
    }

    return {
      handleClick
    }
  }
})

组件封装技巧

1. 使用组合式函数(Composables)

// hooks.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowSize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)

  const update = () => {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }

  onMounted(() => {
    window.addEventListener('resize', update)
  })

  onUnmounted(() => {
    window.removeEventListener('resize', update)
  })

  return {
    width,
    height
  }
}

// 组件中使用
export default defineComponent({
  setup() {
    const { width, height } = useWindowSize()
    return { width, height }
  }
})

2. Props 和事件标准化

// types.ts
export interface TableColumn {
  key: string
  title: string
  width?: number
  sortable?: boolean
}

export interface TableProps {
  columns: TableColumn[]
  dataSource: Record<string, any>[]
  loading?: boolean
}

export interface TableEmits {
  (e: 'sort', key: string, order: 'asc' | 'desc'): void
  (e: 'select', selectedRows: Record<string, any>[]): void
}

// Table.vue
<script setup lang="ts">
import type { TableProps, TableEmits } from './types'

const props = defineProps<TableProps>()
const emit = defineEmits<TableEmits>()

const handleSort = (key: string, order: 'asc' | 'desc') => {
  emit('sort', key, order)
}
</script>

3. 插槽和作用域插槽

<template>
  <div class="my-table">
    <!-- 默认插槽 -->
    <slot name="header">
      <div class="table-header">
        <h3>{{ title }}</h3>
        <slot name="header-extra" />
      </div>
    </slot>
    
    <!-- 作用域插槽 -->
    <template v-for="item in dataSource" :key="item.id">
      <slot name="row" :data="item" :index="index">
        <div class="table-row">
          {{ item.name }}
        </div>
      </slot>
    </template>
    
    <slot name="footer" />
  </div>
</template>

4. 状态管理与依赖注入

// context.ts
import { provide, inject, InjectionKey } from 'vue'

export interface FormContext {
  model: Record<string, any>
  rules?: Record<string, any>
  validate: () => Promise<boolean>
}

export const FormContextKey: InjectionKey<FormContext> = Symbol('FormContext')

export function provideForm(context: FormContext) {
  provide(FormContextKey, context)
}

export function useForm() {
  const form = inject(FormContextKey)
  if (!form) throw new Error('useForm must be used within Form component')
  return form
}

高级特性

1. 组件实例类型声明

// global.d.ts
declare module 'vue' {
  export interface GlobalComponents {
    'MyComponent': typeof import('./components/MyComponent')['default']
  }
}

2. 可扩展的样式系统

// styles/variables.scss
$prefix: 'my-component';
$primary-color: var(--primary-color, #1890ff);
$font-size: var(--font-size, 14px);

// styles/mixins.scss
@mixin theme-light {
  --primary-color: #1890ff;
  --background-color: #ffffff;
}

@mixin theme-dark {
  --primary-color: #177ddc;
  --background-color: #141414;
}

// styles/index.scss
@import './variables.scss';
@import './mixins.scss';

.#{$prefix} {
  font-size: $font-size;
  
  &-header {
    color: $primary-color;
  }
  
  &--dark {
    @include theme-dark;
  }
}

3. 组件文档生成

// MyComponent.vue
/**
 * @component MyComponent
 * @description 一个示例组件
 * @example
 * <MyComponent
 *   title="标题"
 *   type="primary"
 *   @click="handleClick"
 * />
 */
export default defineComponent({
  props: {
    /**
     * 组件标题
     * @default ''
     */
    title: String,
    
    /**
     * 组件类型
     * @values primary, secondary
     * @default 'primary'
     */
    type: String
  }
})

测试规范

// __tests__/index.test.ts
import { mount } from '@vue/test-utils'
import MyComponent from '../index'

describe('MyComponent', () => {
  test('renders properly', () => {
    const wrapper = mount(MyComponent, {
      props: {
        title: 'Test Title'
      }
    })
    
    expect(wrapper.text()).toContain('Test Title')
  })

  test('emits click event', async () => {
    const wrapper = mount(MyComponent)
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeTruthy()
  })
})

最佳实践总结

  1. 组件设计原则

    • 单一职责
    • 可配置性
    • 可测试性
    • 松耦合
  2. 性能优化

    • 合理使用 v-memo 和 v-once
    • 组件懒加载
    • Props 变化监听优化
  3. 代码质量保证

    • TypeScript 类型检查
    • 单元测试覆盖
    • ESLint 和 Prettier 规范
  4. 文档维护

    • 组件示例
    • Props 和事件文档
    • 更新日志

结语

模块化组件封装是一个持续优化的过程。通过遵循以上规范和最佳实践,我们可以构建出高质量、可维护、可复用的 Vue 3 组件库。记住:好的组件设计应该是直观的、一致的、可测试的,并且有良好的文档支持。

参考资源

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐