如何用Typescript装饰器在VUE中来生成表单组件和校验

358 阅读5分钟

前言

大家好,我是最后的Hibana。

这篇文章主要来给大家分享一种利用Typescript中的装饰器来实现表单组件和校验的方法。我在之前也写了篇关于用装饰器来做表单校验的文章(使用TypeScript装饰器在VUE中实现form校验),没有看过的朋友们也可以去看看。

由于Typescript装饰器十分有意思,所以想分享给大家关于它的一些用法。即使你对装饰器不熟悉也可以继续看下去,它十分简单也很有趣。

接下来我会先用代码来介绍通过装饰器生成表单组件和检验的方法,然后再简单解释下实现的原理。

Form表单类

由于装饰器只能用在类上,所有我们必须使用类的方式来写form表单。那么先来写一个简单的form表单类,然后再给它安上装饰器。

// 这是一个简单的创建user的表单,还没有任何装饰器。
class Profile {  
    realName: string
    description?: string
}
class CreateUserForm { 
    username: string 
    email: string  
    phone: string 
    password: string 
    // 如果有表单里还有对象,可以再创建一个类来实现
    profile: Profile
}

class-validator装饰器

接下来添加表单校验的装饰器,这里的装饰器用的是一个class-validator库里的装饰器,当然也可以自己实现自定义的检验装饰器。通过装饰器的名称我们可以很轻易的理解它的作用。例如@IsOptional()意思是可选的,@Length()检验了字段长度,@IsEmail()校验邮箱,@IsMobilePhone()校验手机号码等等。

class Profile {  
    @Length(2, 4, { message: '姓名长度应在2到4间' }) 
    realName: string 

    @IsOptional() 
    description?: string
}

class CreateUserForm { 
    @Length(4, 12, { message: '用户名长度应在4到12间' }) 
    username: string = ''

    @ValidateIf(o => !isMobilePhone(o.phone))
    @IsEmail({}, { message: '请填写正确的邮箱' }) 
    email: string = ''

    @IsMobilePhone('zh-CN', null, { message: '请输入正确的手机号码' }) 
    phone: string = ''

    @MinLength(4, { message: '密码长度不应低于4' })
    @MaxLength(12, { message: '密码长度不应大于12' }) 
    password: string = ''

    // 用来关联表单校验
    @Type(() => Profile)
    @ValidateNested() 
    profile: Profile = new Profile()
}

那么使用起来也很简单,我们在useValidator方法里传入我们的表单CreateUserForm类,就实现了表单验证功能,它会返回关于表单校验的对象和方法,并且会根据表单变化响应式的判断错误。这里用的是tsx,其中field是自己实现的组件。

export default defineComponent({
    setup() {
        const { form, errors, validateForm, clearError, toInit, isValid } = useValidator(CreateUserForm)
        return () => (
            <div class='container'>
                <field label='用户名' v-model={form.username} error={errors.username}></field> 
                <field label='邮箱' v-model={form.email} error={errors.email}></field>
                <field label='手机' v-model={form.phone} error={errors.phone}></field>
                <field label='密码' v-model={form.password} error={errors.password}></field>
                <field label='姓名'  v-model={form.profile.realName}  error={errors.profile?.realName} ></field> 
                <div>
                    <button onClick={() => validateForm()}>验证</button>
                    <button onClick={() => clearError()}>清除错误</button>
                    <button onClick={() => toInit()}>初始化</button>
                </div>
                <p>{isValid.value ? '验证通过' : '验证不通过'}</p>
            </div>
        )
    }
})

很好,我们现在已经实现了form的表单校验,接下来要用装饰器来注册组件。

增加装饰器组件

这里主要用了@Component()@ComponentNested()两个装饰器,一个用来注册组件,传入自定义的表单组件,然后第二个参数传入props就行了。另一个用来关联表单组件。

class Profile {  
    @Length(2, 4, { message: '姓名长度应在2到4间' }) 
    @Component(field, { label: '姓名' })
    realName: string 

    @IsOptional() 
    @Component(field, { label: '描述' })
    description?: string
}

class CreateUserForm { 
    // 自定义的装饰器,用来通过接口来初始化表单属性
    @InjectUsername()
    @Length(4, 12, { message: '用户名长度应在4到12间' }) 
    @Component(field, { label: '用户名' })
    username: string = '还没有名字'

    @ValidateIf(o => !isMobilePhone(o.phone))
    @IsEmail({}, { message: '请填写正确的邮箱' }) 
    @Component(field, { label: '邮箱' })
    email: string = ''

    @IsMobilePhone('zh-CN', null, { message: '请输入正确的手机号码' })
    @Component(field, { label: '手机' })
    phone: string = ''

    @MinLength(4, { message: '密码长度不应低于4' })
    @MaxLength(12, { message: '密码长度不应大于12' }) 
    @Component(field, { label: '密码' })
    password: string = ''

    
    @Type(() => Profile)
    @ValidateNested()  
    @ComponentNested(Profile)
    profile: Profile = new Profile()
}

那么接下来就是使用useComponent的时刻了。是的,没错,useComponent会返回与useValidator一样的东西,但是它返回还多了一个component组件,我们可以直接写在下面的tsx中。相比于之前,省去了我们手动在tsx中写组件的功夫。

export default defineComponent({
    setup() {
        const {component: userForm, form,errors,validateForm, clearError, toInit, isValid } = useComponent(CreateUserForm)
        
        // 通过这个来实现InjectUsername装饰器的功能,异步初始化用户名
        useInject(form)
        return () => (
            <div class='container'>
                <user-form />
                <div>
                    <button onClick={() => validateForm()}>验证</button>
                    <button onClick={() => clearError()}>清除错误</button>
                    <button onClick={() => toInit()}>初始化</button>
                </div>
                <p>{isValid.value ? '验证通过' : '验证不通过'}</p>
            </div>
        )
    }
})

那么,来看看效果如何。

Kapture 2022-04-08 at 20.57.22.gif

简单的useComponent

现在我们已经通过Typescript装饰器在VUE中来生成简单的表单组件和校验功能了,那么是如何实现的呢。关于useValidator的实现,可以去看看我在本文开头分享的文章。

关于component装饰器以及useComponent的实现,其实很简单,代码如下。

export function Component(component: any, props: Record<string, any> = {}) {
    return function (target: any, key: string) {
        return Reflect.defineMetadata('component', { component, props }, target, key)
    }
}

export function ComponentNested<T extends { new (...args: any[]): any }>(constructor: T) {
    return function (target: any, key: string) {
        return Reflect.defineMetadata('componentNested', constructor, target, key)
    }
}

// 通过Component和ComponentNested来收集需要注册的组件
// 在useComponent中获取需要注册的组件,然后返回整个表单组件
export function useComponent<T extends { new (...args: any[]): any }>(constructor: T) {
    const { form, errors, validateForm, clearError, toInit, toJSON, isValid } =  useValidator(constructor)
    const list = getFormComponent(form)
    const component = defineComponent({
        name: constructor.name,
        setup() {
            return () => createFormComponent(list, form, errors)
        }
    })

    return { component, form, errors, validateForm, clearError, toInit, toJSON, isValid }
}

// 获取每个属性被注册的表单组件
function getFormComponent(form) {
    const componentList: Record<string, any> = {}
    for (const key in form) {
        if (Object.prototype.hasOwnProperty.call(form, key)) {
            const info = Reflect.getMetadata('component', form, key)
            const componentNested = Reflect.getMetadata('componentNested', form, key)
            if (componentNested) {
                componentList[key] = getFormComponent(form[key])
            }
            if (info) {
                componentList[key] = info
            }
        }
    }
    return componentList
}

//生成表单组件,当然也可以用jsx来生成
function createFormComponent(componentList, form, errors) {
    return h('div', null, [
        Object.entries(componentList).map(([key, v]: [string, any]) => {
            if (typeof form[key] === 'object') {
                return createFormComponent(componentList[key], form[key], errors[key])
            }
            const props = {
                modelValue: form[key],
                'onUpdate:modelValue': (value: any) => form[key] = value,
                error: errors && errors[key],
                ...v.props
            }
            return h(v.component, props)
        })
    ])
}

我这里的实现都是相对比较简单的,当然适用范围也会相对单一。对于一些要求不高,但重复度很高的表单,我觉得用这个也蛮方便的。

结尾

出于对装饰器的喜欢,本篇文章简单介绍了下如何用Typescript装饰器在VUE中来生成表单组件和校验,真的只是入门版本。实现表单组件的生成和校验也有很多种方法,装饰器也只是其中一种罢了。

希望本文能带给大家一些关于装饰器的认识,能想出更多装饰器的可能性。

github地址:github.com/AndSpark/vu…

通过npm来尝试使用看看:

npm install vue-class-validator class-validator class-transformer reflect-metadata

import {useValidator,useComponent,...} from 'vue-class-validator'