Vue3组合式API组件库设计模式与TypeScript类型封装实战

Vue3组合式API为前端工程化中的组件库设计提供了全新的架构思路。通过setup语法糖和TypeScript类型系统,可以构建类型安全、逻辑复用性强、API设计规范的企业级组件库。本文以表单组件库为例,完整演示Vue3组件设计模式、Props/Emits类型定义、组合式函数封装和样式方案。

组件库项目初始化

使用Vite搭建组件库开发环境,配置TypeScript严格模式和库模式打包:

# 创建项目
npm create vite@latest vue3-ui-lib -- --template vue-ts
cd vue3-ui-lib
npm install

# 安装开发依赖
npm install -D @vitejs/plugin-vue vue-tsc sass
npm install vue@3

vite.config.ts配置库模式打包:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: { '@': resolve(__dirname, 'src') }
  },
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'VueUI',
      fileName: (format) => `vue3-ui-lib.${format}.js`
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: { vue: 'Vue' },
        exports: 'named'
      }
    }
  }
})

组件Props类型设计与校验

以Input组件为例,演示TypeScript类型定义与Vue3 props的整合方式:

// src/components/Input/types.ts
import type { ExtractPropTypes, PropType } from 'vue'

export type InputSize = 'small' | 'medium' | 'large'
export type InputType = 'text' | 'password' | 'number' | 'email'

export const inputProps = {
  modelValue: {
    type: [String, Number] as PropType,
    default: ''
  },
  type: {
    type: String as PropType,
    default: 'text',
    validator: (val: string) => ['text', 'password', 'number', 'email'].includes(val)
  },
  size: {
    type: String as PropType,
    default: 'medium'
  },
  placeholder: { type: String, default: '' },
  disabled: { type: Boolean, default: false },
  readonly: { type: Boolean, default: false },
  clearable: { type: Boolean, default: false },
  maxlength: { type: Number, default: -1 },
  showCount: { type: Boolean, default: false }
} as const

export type InputProps = ExtractPropTypes

使用as const确保类型推导为字面量类型而非宽泛的string。ExtractPropTypes从props定义中自动生成完整类型,在IDE中获得精确的属性提示。

Emits事件类型定义

// src/components/Input/types.ts (续)
export const inputEmits = {
  'update:modelValue': (value: string | number) => true,
  'input': (value: string) => true,
  'change': (value: string) => true,
  'focus': (e: FocusEvent) => true,
  'blur': (e: FocusEvent) => true,
  'clear': () => true
}

export type InputEmits = typeof inputEmits

组合式函数封装可复用逻辑

将Input组件中可复用的状态管理和事件处理逻辑抽离为useInput组合式函数:

// src/components/Input/useInput.ts
import { ref, computed, watch } from 'vue'
import type { InputProps, InputEmits } from './types'

export function useInput(props: InputProps, emit: (e: string, ...args: any[]) => void) {
  const isFocused = ref(false)
  const isHovering = ref(false)
  
  const inputClasses = computed(() => ({
    'vui-input': true,
    [`vui-input--${props.size}`]: true,
    'vui-input--disabled': props.disabled,
    'vui-input--focused': isFocused.value
  }))
  
  const displayValue = computed({
    get: () => props.modelValue,
    set: (val) => {
      emit('update:modelValue', val)
      emit('input', String(val))
    }
  })
  
  const textLength = computed(() => {
    const val = props.modelValue
    return typeof val === 'string' ? val.length : String(val).length
  })
  
  const showClearIcon = computed(() => 
    props.clearable && !props.disabled && !props.readonly && displayValue.value !== '' && isHovering.value
  )
  
  const handleFocus = (e: FocusEvent) => {
    if (props.disabled || props.readonly) return
    isFocused.value = true
    emit('focus', e)
  }
  
  const handleBlur = (e: FocusEvent) => {
    isFocused.value = false
    emit('blur', e)
    emit('change', String(displayValue.value))
  }
  
  const handleClear = () => {
    displayValue.value = ''
    emit('clear')
  }
  
  return {
    isFocused,
    isHovering,
    inputClasses,
    displayValue,
    textLength,
    showClearIcon,
    handleFocus,
    handleBlur,
    handleClear
  }
}

组件模板实现

<!-- src/components/Input/Input.vue -->
<template>
  <div
    :class="inputClasses"
    @mouseenter="isHovering = true"
    @mouseleave="isHovering = false"
  >
    <input
      :type="type"
      :value="displayValue"
      :placeholder="placeholder"
      :disabled="disabled"
      :readonly="readonly"
      :maxlength="maxlength > 0 ? maxlength : undefined"
      class="vui-input__inner"
      @focus="handleFocus"
      @blur="handleBlur"
      @input="displayValue = ($event.target as HTMLInputElement).value"
    />
    <span v-if="showClearIcon" class="vui-input__clear" @click="handleClear">
      &times;
    </span>
    <span v-if="showCount" class="vui-input__count">
      {{ textLength }}{{ maxlength > 0 ? `/${maxlength}` : '' }}
    </span>
  </div>
</template>

<script setup lang="ts">
import { inputProps, inputEmits } from './types'
import { useInput } from './useInput'

const props = defineProps(inputProps)
const emit = defineEmits(inputEmits)

const {
  isHovering, inputClasses, displayValue, textLength,
  showClearIcon, handleFocus, handleBlur, handleClear
} = useInput(props, emit)
</script>

defineProps和defineEmits使用编译宏,无需import,在编译时自动处理。传入之前定义的props和emits对象,获得完整类型推断。

SCSS样式与CSS变量主题系统

/* src/components/Input/style.scss */
.vui-input {
  display: inline-flex;
  align-items: center;
  width: 100%;
  border: 1px solid var(--vui-border-color, #dcdfe6);
  border-radius: var(--vui-border-radius, 4px);
  background: var(--vui-bg-color, #fff);
  transition: border-color 0.2s;
  
  &--small { height: 24px; font-size: 12px; }
  &--medium { height: 32px; font-size: 14px; }
  &--large { height: 40px; font-size: 16px; }
  
  &--focused {
    border-color: var(--vui-primary-color, #409eff);
  }
  
  &--disabled {
    background: var(--vui-disabled-bg, #f5f7fa);
    cursor: not-allowed;
  }
  
  &__inner {
    flex: 1;
    border: none;
    outline: none;
    background: transparent;
    padding: 0 12px;
    height: 100%;
    
    &:disabled { cursor: not-allowed; }
  }
  
  &__clear {
    cursor: pointer;
    padding: 0 8px;
    color: var(--vui-text-placeholder, #c0c4cc);
    &:hover { color: var(--vui-text-color, #606266); }
  }
  
  &__count {
    font-size: 12px;
    color: var(--vui-text-placeholder, #909399);
    padding-right: 8px;
  }
}

CSS变量方案使组件库支持运行时主题切换,用户只需覆盖 --vui-primary-color 等变量即可定制主题。响应式布局中通过媒体查询调整CSS变量值,实现自适应尺寸。

组件导出与install插件注册

// src/components/Input/index.ts
import Input from './Input.vue'
import type { App } from 'vue'

Input.install = (app: App) => {
  app.component(Input.name || 'VuiInput', Input)
}

export { Input }
export default Input

// src/index.ts
import type { App } from 'vue'
import { Input } from './components/Input'
import { Button } from './components/Button'
import { Form } from './components/Form'

const components = [Input, Button, Form]

export { Input, Button, Form }

export default {
  install(app: App) {
    components.forEach(c => app.use(c))
  }
}

每个组件带有install方法,支持全局注册和按需导入两种使用方式:

// 全局注册
import { createApp } from 'vue'
import VueUI from 'vue3-ui-lib'
const app = createApp(App)
app.use(VueUI)

// 按需导入(推荐,配合tree-shaking)
import { Input, Button } from 'vue3-ui-lib'
app.component('VuiInput', Input)
app.component('VuiButton', Button)

跨端小程序开发适配考虑

当组件库需要同时支持Web和小程序时,需分离DOM层和逻辑层。组合式函数天然可跨端复用,仅模板层需针对不同平台编写。以Taro或uni-app为例,将useInput逻辑保持不变,模板替换为对应平台组件:

// 逻辑层保持不变
import { useInput } from './useInput'

// Web模板
<input :value="displayValue" @input="..." />

// 小程序模板(条件编译)
<!-- #ifdef MP-WEIXIN -->
<input :value="displayValue" @input="..." />
<!-- #endif -->

Vue3组合式API的优势在于逻辑与视图解耦。同一套useForm、useInput、useValidation组合式函数可服务于Web、小程序、Flutter移动端的视图层,实现跨端逻辑复用。样式层通过平台条件编译切换CSS方案,保持逻辑一致性的同时尊重各平台差异。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zu-jian-ku-she-ji-mo-shi-yu-typescript/

(0)
小编小编
上一篇 11小时前
下一篇 11小时前

相关推荐