vue-element-plus-admin 第6期|Element Plus 实战:组件设计与最佳实践

146 阅读13分钟

1. Element Plus 组件二次封装技巧

vue-element-plus-admin 项目基于 Element Plus 组件库进行了全面的二次封装,形成了一套符合业务需求的高阶组件体系,具有较强的扩展性和统一性。本部分将分析项目中组件二次封装的主要技巧和设计思路。

1.1 二次封装的核心思想

项目中对 Element Plus 组件的二次封装主要基于以下几个核心思想:

  1. 保留原有功能:通过透传 props、事件和插槽,确保原组件的所有功能特性在二次封装后依然可用
  2. 扩展新特性:针对业务场景需求,添加额外的功能和属性
  3. 简化调用:将常用配置封装为默认值,减少使用时的重复代码
  4. 统一样式:维持整个系统的设计风格一致性
  5. 类型支持:完善的 TypeScript 类型定义,提升开发体验

1.2 Props 处理与透传技巧

在项目中,组件封装普遍采用了 Props 透传与筛选的处理方式,以 Dialog 组件为例:

// src/components/Dialog/src/Dialog.vue
const getBindValue = computed(() => {
  const delArr: string[] = ['fullscreen', 'title', 'maxHeight']
  const attrs = useAttrs()
  const obj = { ...attrs, ...props }
  for (const key in obj) {
    if (delArr.indexOf(key) !== -1) {
      delete obj[key]
    }
  }
  return obj
})

这段代码实现了:

  1. 获取所有传入的属性和事件(useAttrs()
  2. 与显式声明的 props 合并
  3. 移除已经在组件内部特殊处理的属性
  4. 将剩余属性透传给 Element Plus 原始组件

这种处理方式确保了二次封装不会破坏原组件的功能,同时可以添加自定义属性和逻辑。

1.3 插槽透传与增强

插槽的处理是二次封装的关键部分,项目中采用了多种方式处理插槽:

  1. 简单透传:直接将外部插槽内容传递给内部组件
<ElDialog v-bind="getBindValue">
  <template #default>
    <slot></slot>
  </template>
</ElDialog>
  1. 条件插槽:根据是否提供插槽内容决定渲染逻辑
<template #footer v-if="slots.footer">
  <slot name="footer"></slot>
</template>
  1. 增强插槽:在原插槽基础上增加额外内容
<template #header="{ close }">
  <div class="flex justify-between items-center h-54px pl-15px pr-15px relative">
    <slot name="title">
      {{ title }}
    </slot>
    <div class="...">
      <!-- 额外的控制按钮 -->
      <Icon v-if="fullscreen" ... @click="toggleFull" />
      <Icon ... @click="close" />
    </div>
  </div>
</template>

1.4 组合式函数与逻辑抽取

为了保持组件代码的简洁与可维护性,项目大量使用了组合式函数(Composable)抽取通用逻辑,例如 Dialog 组件的调整大小功能:

// src/components/Dialog/hooks/useResize.ts
export const useResize = (props?: {
  minHeightPx?: number
  minWidthPx?: number
  initHeight?: number
  initWidth?: number
}) => {
  // ...实现逻辑
  
  return {
    setupDrag,
    maxHeight,
    minWidth
  }
}

在 ResizeDialog 组件中使用:

// src/components/Dialog/src/ResizeDialog.vue
const { maxHeight, minWidth, setupDrag } = useResize({
  minHeightPx: props.minResizeHeight,
  minWidthPx: props.minResizeWidth,
  initHeight: props.initHeight,
  initWidth: props.initWidth
})

这种方式有以下优势:

  1. 关注点分离,使组件逻辑更加清晰
  2. 实现代码复用,避免重复逻辑
  3. 便于独立测试和维护

1.5 统一样式与设计系统

项目通过统一的设计变量和工具函数维持样式的一致性:

// 使用统一的样式前缀
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('icon')

同时,在样式中使用 Less 变量系统:

@prefix-cls: ~'@{adminNamespace}-icon';

.@{prefix-cls} {
  // 样式定义
}

这种方式确保了整个系统的样式命名一致,便于主题定制和样式覆盖。

2. 通用业务组件实现

项目中实现了一系列高度封装的业务组件,这些组件针对常见的业务场景提供了开箱即用的解决方案,大大提高了开发效率。

2.1 Form 组件的设计与实现

Form 组件是项目中最复杂的封装之一,它通过 schema 配置化的方式生成表单,支持多种布局和验证方式。

核心设计特点:

  1. Schema 驱动:通过统一的数据结构定义表单
// 使用示例
const schema: FormSchema[] = [
  {
    field: 'username',
    label: '用户名',
    component: 'Input',
    componentProps: {
      placeholder: '请输入用户名'
    },
    required: true
  }
]
  1. 动态表单项:支持动态增删和条件显示
// 动态增加表单项
const addSchema = (formSchema: FormSchema, index?: number) => {
  const { schema } = unref(getProps)
  if (index !== void 0) {
    schema.splice(index, 0, formSchema)
    return
  }
  schema.push(formSchema)
}
  1. 组件映射:通过映射表将字符串类型转换为实际组件
// src/components/Form/src/helper/componentMap.ts
import { ElInput, ElSelect, /* ... */ } from 'element-plus'

export const componentMap = {
  Input: ElInput,
  Select: ElSelect,
  // ... 其他组件映射
}
  1. TSX 渲染:使用 TSX 灵活构建表单结构
// 渲染表单项
const renderFormItem = (item: FormSchema) => {
  // ... 处理逻辑
  return (
    <ElFormItem
      v-show={!item.hidden}
      ref={(el: any) => setFormItemRefMap(el, item.field)}
      {...(item.formItemProps || {})}
      prop={item.field}
      label={item.label || ''}
    >
      {formItemSlots}
    </ElFormItem>
  )
}

核心功能实现:

Form 组件的核心在于将 schema 转换为实际的表单元素,并处理数据绑定、验证等逻辑。关键实现包括:

// 初始化表单数据
watch(
  () => unref(getProps).schema,
  (schema = []) => {
    formModel.value = initModel(schema, unref(formModel))
  },
  {
    immediate: true,
    deep: true
  }
)

// 处理不同类型的表单组件
if (item.component === ComponentNameEnum.SELECT) {
  slotsMap.default = !componentSlots.default
    ? () => renderSelectOptions(item)
    : () => {
        return componentSlots.default(
          unref((item?.componentProps as SelectComponentProps)?.options)
        )
      }
}

2.2 Table 组件的设计与实现

Table 组件针对后台管理系统的数据表格场景进行了高度封装,支持分页、排序、筛选、自定义列等功能。

核心设计特点:

  1. 列配置化:通过配置对象定义表格列
const columns: TableColumn[] = [
  { type: 'selection' },
  { type: 'index', width: '60' },
  {
    field: 'title',
    label: '标题',
    search: {
      show: true
    }
  }
]
  1. 动态控制:支持列的动态显示、隐藏和排序
// 设置列属性
const setColumn = (columnProps: TableSetProps[], columnsChildren?: TableColumn[]) => {
  const { columns } = unref(getProps)
  for (const v of columnsChildren || columns) {
    for (const item of columnProps) {
      if (v.field === item.field) {
        set(v, item.path, item.value)
      } else if (v.children?.length) {
        setColumn(columnProps, v.children)
      }
    }
  }
}
  1. 内容渲染:支持多种内容渲染方式,包括格式化、自定义插槽和多级表头
// 渲染表格列
const renderTableColumn = (columnsChildren?: TableColumn[]) => {
  // ... 处理逻辑
  return (columnsChildren || columns).map((v) => {
    // ... 处理每列的渲染逻辑
    return (
      <ElTableColumn
        showOverflowTooltip={showOverflowTooltip}
        align={align}
        headerAlign={headerAlign}
        {...props}
        prop={v.field}
      >
        {slots}
      </ElTableColumn>
    )
  })
}
  1. 媒体预览:内置图片和视频预览功能
const renderPreview = (url: string, field: string) => {
  const { imagePreview, videoPreview } = unref(getProps)
  return (
    <div class="flex items-center">
      {imagePreview.includes(field) ? (
        <ElImage
          src={url}
          fit="cover"
          class="w-[100%]"
          lazy
          preview-src-list={[url]}
          preview-teleported
        />
      ) : videoPreview.includes(field) ? (
        <BaseButton
          type="primary"
          icon={<Icon icon="vi-ep:video-play" />}
          onClick={() => {
            createVideoViewer({
              url
            })
          }}
        >
          预览
        </BaseButton>
      ) : null}
    </div>
  )
}

2.3 Dialog 组件的设计与实现

Dialog 组件扩展了 Element Plus 的 Dialog,添加了更多实用功能,如全屏、可调整大小等。

核心设计特点:

  1. 多功能头部:自定义头部,添加全屏切换按钮
<template #header="{ close }">
  <div class="flex justify-between items-center h-54px pl-15px pr-15px relative">
    <slot name="title">
      {{ title }}
    </slot>
    <div class="...">
      <Icon
        v-if="fullscreen"
        class="cursor-pointer is-hover !h-54px mr-10px"
        :icon="
          isFullscreen ? 'vi-radix-icons:exit-full-screen' : 'vi-radix-icons:enter-full-screen'
        "
        color="var(--el-color-info)"
        hover-color="var(--el-color-primary)"
        @click="toggleFull"
      />
      <Icon
        class="cursor-pointer is-hover !h-54px"
        icon="vi-ep:close"
        hover-color="var(--el-color-primary)"
        color="var(--el-color-info)"
        @click="close"
      />
    </div>
  </div>
</template>
  1. 可调整大小:ResizeDialog 组件通过自定义指令和 useResize 钩子实现大小调整功能
// 自定义指令实现可调整大小
const vResize = {
  mounted(el) {
    const observer = new MutationObserver(() => {
      const elDialog = el.querySelector('.el-dialog')
      if (elDialog) {
        setupDrag(elDialog, el)
      }
    })
    observer.observe(el, { childList: true, subtree: true })
  }
}
  1. 嵌套内容处理:使用 ElScrollbar 组件处理内容溢出
<ElScrollbar :style="dialogStyle">
  <slot></slot>
</ElScrollbar>

3. 自定义 Hook 设计与应用场景

项目大量使用自定义 Hook (组合式函数) 来封装和复用逻辑,这是 Vue 3 Composition API 的一个重要实践。

3.1 Hook 设计原则

项目中的 Hook 设计遵循以下原则:

  1. 单一职责:每个 Hook 只负责一个明确的功能点
  2. 组合优先:通过组合简单的 Hook 构建复杂功能
  3. 命名规范:统一使用 useXxx 命名约定
  4. 参数可配置:通过参数配置 Hook 的行为
  5. 返回值明确:返回值包含状态和操作方法

3.2 常用 Hook 分析

useClipboard - 剪贴板操作封装

// src/hooks/web/useClipboard.ts
const useClipboard = () => {
  const copied = ref(false)
  const text = ref('')
  const isSupported = ref(false)

  // 检查浏览器支持
  if (!navigator.clipboard && !document.execCommand) {
    isSupported.value = false
  } else {
    isSupported.value = true
  }

  // 复制功能实现
  const copy = (str: string) => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(str).then(() => {
        text.value = str
        copied.value = true
        resetCopied()
      })
      return
    }
    // 降级处理
    const input = document.createElement('input')
    // ... 实现细节
  }

  // 重置复制状态
  const resetCopied = () => {
    setTimeout(() => {
      copied.value = false
    }, 1500)
  }

  return { copy, text, copied, isSupported }
}

这个 Hook:

  • 封装了跨浏览器的剪贴板操作
  • 提供了复制状态反馈
  • 支持降级处理
  • 自动重置复制状态

useResize - 元素大小调整

// src/components/Dialog/hooks/useResize.ts
export const useResize = (props?: {
  minHeightPx?: number
  minWidthPx?: number
  initHeight?: number
  initWidth?: number
}) => {
  // ... 初始化参数
  
  // 设置拖拽调整大小
  const setupDrag = (elDialog: any, el: any) => {
    // ... 实现拖拽调整大小的逻辑
  }

  return {
    setupDrag,
    maxHeight,
    minWidth
  }
}

这个 Hook:

  • 接受可配置参数
  • 封装了复杂的 DOM 操作和事件处理
  • 返回状态和方法供组件使用

useCrudSchemas - 统一表单和表格配置

// src/hooks/web/useCrudSchemas.ts
export const useCrudSchemas = (
  crudSchema: CrudSchema[]
): {
  allSchemas: AllSchemas
} => {
  // 所有结构数据
  const allSchemas = reactive<AllSchemas>({
    searchSchema: [],
    tableColumns: [],
    formSchema: [],
    detailSchema: []
  })

  // 过滤处理各种 Schema
  const searchSchema = filterSearchSchema(crudSchema)
  allSchemas.searchSchema = searchSchema || []

  const tableColumns = filterTableSchema(crudSchema)
  allSchemas.tableColumns = tableColumns || []

  // ... 其他处理

  return {
    allSchemas
  }
}

这个 Hook:

  • 接受统一的配置结构
  • 转换为不同组件所需的配置格式
  • 减少重复配置的工作量

3.3 Hook 与组件的结合使用

项目中的 Hook 往往与组件紧密结合,有两种主要使用模式:

  1. 外部引入:在组件中直接导入并使用 Hook
<script setup lang="ts">
import { useClipboard } from '@/hooks/web/useClipboard'

const { copy, copied, isSupported } = useClipboard()

// 组件中使用
const handleCopy = () => {
  copy('要复制的文本')
}
</script>
  1. 内部定义:在组件文件中定义并使用特定于该组件的 Hook
// src/components/Menu/src/components/useRenderMenuItem.tsx
export const useRenderMenuItem = (menuMode) => {
  const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
    // ... 渲染逻辑
  }

  return {
    renderMenuItem
  }
}

4. 组件通信模式与事件处理

项目采用了多种组件通信模式,根据不同场景选择最合适的方式。

4.1 Props/Emit 基础通信

最基本的组件通信方式是通过 Props 传递数据和通过 Emit 触发事件:

// Props 定义
const props = defineProps({
  modelValue: propTypes.bool.def(false),
  title: propTypes.string.def('Dialog'),
  fullscreen: propTypes.bool.def(true),
  maxHeight: propTypes.oneOfType([String, Number]).def('400px')
})

// 事件触发
emit('update:currentPage', val)

在父组件中使用:

<Table 
  v-model:currentPage="currentPage"
  v-model:pageSize="pageSize"
  :columns="columns"
  :data="data"
  @register="registerTable"
/>

4.2 Expose/Ref 实例引用

对于复杂组件,项目使用 expose 和 ref 实现直接调用组件方法:

// 子组件暴露方法
expose({
  setValues,
  formModel,
  setProps,
  delSchema,
  addSchema,
  setSchema,
  getComponentExpose,
  getFormItemExpose
})

// 父组件引用
const formRef = ref<ComponentRef<typeof Form>>()

// 调用方法
const setSchemaField = async () => {
  await nextTick()
  unref(formRef)?.setSchema([
    {
      field: 'field1',
      path: 'componentProps.options',
      value: [{ label: '选项1', value: 1 }]
    }
  ])
}

4.3 provide/inject 依赖注入

对于深层嵌套的组件通信,项目使用 provide/inject 机制:

// 在上层组件提供数据
provide(configProviderContextKey, props.configGlobal)

// 在深层组件中注入数据
const configGlobal = inject(configProviderContextKey, {})

4.4 事件总线

对于复杂的跨组件通信,项目实现了基于 mitt 的事件总线:

// src/hooks/event/useEventBus.ts
import mitt from 'mitt'
import { onBeforeUnmount } from 'vue'

const emitter = mitt()

export const useEventBus = (key: string) => {
  // 监听事件
  function on(callback: Fn) {
    emitter.on(key, callback)
  }
  
  // 触发事件
  function emit<T = any>(event?: T) {
    emitter.emit(key, event)
  }
  
  // 组件卸载时自动移除监听
  onBeforeUnmount(() => {
    emitter.off(key)
  })
  
  return {
    on,
    emit
  }
}

使用示例:

// 组件 A 触发事件
const { emit } = useEventBus('refresh-table')
emit()

// 组件 B 监听事件
const { on } = useEventBus('refresh-table')
on(() => {
  fetchData()
})

4.5 组件注册与回调

项目中广泛使用"注册回调"模式,通过 register 事件实现对组件实例的引用:

// 组件内部
onMounted(() => {
  emit('register', unref(elTableRef)?.$parent, elTableRef)
})

// 使用组件
const [registerTable] = useTable({
  columns,
  loading: () => loading.value
})

// 组件注册
<Table @register="registerTable" />

这种模式使得父组件可以优雅地控制子组件,同时保持组件的封装性。

5. 函数式组件与 JSX/TSX 实践

项目中大量使用了 JSX/TSX 来构建复杂的组件结构,特别是需要动态渲染的场景。

5.1 JSX/TSX 的使用场景

在项目中,JSX/TSX 主要用于以下场景:

  1. 动态渲染组件:如 Form 组件根据 schema 生成表单项
  2. 复杂条件渲染:包含多层条件判断的渲染逻辑
  3. 递归组件:如菜单树的渲染
  4. 辅助渲染函数:作为组件的辅助渲染工具

5.2 useRenderMenuItem - 菜单项渲染

这是项目中 TSX 应用的典型示例,用于动态渲染菜单项:

// src/components/Menu/src/components/useRenderMenuItem.tsx
export const useRenderMenuItem = (menuMode) => {
  const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
    return routers
      .filter((v) => !v.meta?.hidden)
      .map((v) => {
        const meta = v.meta ?? {}
        const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
        const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path)

        if (
          oneShowingChild &&
          (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
          !meta?.alwaysShow
        ) {
          return (
            <ElMenuItem
              index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
            >
              {{
                default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
              }}
            </ElMenuItem>
          )
        } else {
          return (
            <ElSubMenu
              index={fullPath}
              teleported
              popperClass={unref(menuMode) === 'vertical' ? `${prefixCls}-popper--vertical` : ''}
            >
              {{
                title: () => renderMenuTitle(meta),
                default: () => renderMenuItem(v.children!, fullPath)
              }}
            </ElSubMenu>
          )
        }
      })
  }

  return {
    renderMenuItem
  }
}

这个实现的优点:

  1. 声明式代码:JSX 使复杂的条件渲染更加清晰
  2. 递归渲染:轻松处理嵌套菜单结构
  3. 动态属性:可以根据条件动态设置组件属性
  4. 组合插槽:使用对象语法构造插槽内容

5.3 表单组件中的渲染函数

Form 组件使用 TSX 实现了复杂的动态渲染逻辑:

// 渲染 Form 包装
const renderWrap = () => {
  const { isCol } = unref(getProps)
  const content = isCol ? (
    <ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
  ) : (
    renderFormItemWrap()
  )
  return content
}

// 渲染表单项包装
const renderFormItemWrap = () => {
  const { schema = [], isCol } = unref(getProps)

  return schema
    .filter((v) => !v.remove)
    .map((item) => {
      // 如果是 Divider 组件,需要自己占用一行
      const isDivider = item.component === 'Divider'
      const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
      return isDivider ? (
        <Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
      ) : isCol ? (
        // 如果需要栅格,需要包裹 ElCol
        <ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
      ) : (
        renderFormItem(item)
      )
    })
}

5.4 组件渲染辅助函数

项目封装了一些辅助函数,简化 JSX/TSX 的使用:

// src/utils/tsxHelper.ts
import { Slots } from 'vue'

export const getSlot = (slots: Slots, slot = 'default', data?: any) => {
  if (!slots || !Reflect.has(slots, slot)) {
    return null
  }
  if (!data) {
    return slots[slot]?.()
  }
  const slotFn = slots[slot]
  if (!slotFn) return null
  return slotFn(data)
}

这个辅助函数使得在 TSX 中处理插槽变得简单:

// 使用辅助函数渲染插槽
{getSlot(slots, 'footer')}

// 或者带数据的插槽
{getSlot(slots, 'content', item)}

6. 组件设计的最佳实践总结

通过对 vue-element-plus-admin 项目组件设计的分析,我们可以总结出以下最佳实践:

6.1 统一的组件架构

  1. 目录结构标准化:每个组件都遵循相似的目录结构

    ComponentName/
      ├── index.ts        # 导出入口
      └── src/
          ├── ComponentName.vue  # 主组件
          ├── types/             # 类型定义
          ├── components/        # 子组件
          └── hooks/             # 相关 hooks
    
  2. 类型系统:使用 TypeScript 提供完善的类型支持

    export interface TableColumn extends TableColumnParams {
      children?: TableColumn[]
      field: string
      label: string
    }
    
  3. 统一的命名规范:前缀一致,命名有意义

    const prefixCls = getPrefixCls('form')
    

6.2 可配置化设计

  1. Schema 驱动:通过配置对象描述组件结构

    const schema: FormSchema[] = [
      { field: 'name', label: '姓名', component: 'Input' }
    ]
    
  2. 默认值合理化:为常用配置提供合理默认值

    const pagination = computed(() => {
      return Object.assign(
        {
          small: false,
          background: false,
          pagerCount: 7,
          layout: 'sizes, prev, pager, next, jumper, ->, total',
          pageSizes: [10, 20, 30, 40, 50, 100],
          disabled: false,
          hideOnSinglePage: false,
          total: 10
        },
        unref(getProps).pagination
      )
    })
    
  3. 扩展点预留:通过插槽和回调函数预留扩展点

6.3 代码复用策略

  1. 组合式 API 优先:使用 Composition API 抽取复用逻辑

    // 提取可复用的逻辑
    const useClipboard = () => {
      // ...实现
      return { copy, text, copied, isSupported }
    }
    
  2. Mixin → Hook 转换:将旧的 Mixin 模式转换为 Hook 模式

  3. 渲染函数复用:将复杂的渲染逻辑抽取为单独函数

    const renderMenuItem = (routers, parentPath) => {
      // ...渲染逻辑
    }
    

6.4 性能优化考量

  1. 组件按需加载:全局组件和按需导入组件分离

    // 全局组件注册
    export const setupGlobCom = (app: App<Element>): void => {
      app.component('Icon', Icon)
      app.component('Permission', Permission)
      app.component('BaseButton', BaseButton)
    }
    
  2. 计算属性缓存:使用 computed 缓存计算结果

    const getBindValue = computed(() => {
      // ...计算逻辑
      return bindValue
    })
    
  3. 合理的响应性控制:使用 unref 优化响应式对象访问

    // 解包响应式对象
    const props = { ...unref(getProps) }
    

6.5 可维护性设计

  1. 关注点分离:逻辑、UI、样式分离

    // 逻辑提取到 hook
    const { maxHeight, minWidth, setupDrag } = useResize(props)
    
  2. 命名语义化:函数和变量名能表达其用途

    const toggleFull = () => {
      isFullscreen.value = !unref(isFullscreen)
    }
    
  3. 文档化注释:关键函数和组件有注释说明

    /**
     * @description: 获取表单组件实例
     * @param filed 表单字段
     */
    const getComponentExpose = (filed: string) => {
      return unref(formComponents)[filed]
    }
    

7. 总结

vue-element-plus-admin 项目的组件设计体现了现代化前端项目的最佳实践,通过组件封装、Hook 设计和 TSX 应用,实现了高度可复用、可配置的组件库。

这些设计不仅提高了开发效率,也使得代码更具可维护性和扩展性。对于大型中后台项目,这些实践提供了宝贵的参考:

  1. 二次封装不是简单包装,而是要深入理解原组件设计意图,在保留原功能的基础上提供更高层次的抽象

  2. 组合式 API 的优势在项目中得到充分体现,实现了更清晰的关注点分离和逻辑复用

  3. TSX/JSX 在复杂渲染场景中的价值显著,特别是在需要大量条件渲染和动态组件的场景

  4. 配置驱动的理念贯穿整个组件设计,通过 Schema 和声明式配置简化了开发

  5. 类型系统的重要性不可忽视,完善的类型定义使得组件使用更加安全可靠

这些实践对于构建企业级 Vue 3 应用具有很强的指导意义,值得在实际项目中借鉴和应用。