使用TypeScript装饰器在VUE中实现form校验和其他有意思的功能

885 阅读5分钟

前言

大家好,我是最后的Hibana。

Typescript装饰器是一种非常有意思的声明方法,通过装饰器可以轻松实现代理模式来使代码更简洁以及实现其它一些更有趣的能力。我在上一篇 在VUE中使用TypeScript装饰器来实现表单验证 中介绍了如何使用class-validator装饰器和自制的Validator类来实现表单验证,如果大家有没看过的可以先去看看那篇。接下来我将用另外一种方式来实现表单验证。

使用useValidator

在使用useValidator前,我们先来看看之前写的表单类是怎么样子的。

class Profile {
    @IsOptional()
    avatar?: string

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

    @IsOptional()
    description?: string
}

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

    @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()
}

其中@Reactive装饰器和Validator类是我自己封装的,其他表单验证的装饰器是用class-validator库的。现在看起来是没什么问题,但是如果表单一多,每个表单都得用@Reactive和继承Validator类,如果说还有关联表单,关联的表单也继承了Validator类,会显得十分臃肿。

export default defineComponent({
setup() {
    const form = new CreateUserForm()
    return  () => 
        <div>
            <field label='用户名' v-model={form.username} error={form.getError().username}></field>
            <field label='姓名' v-model={form.profile.realName} error={form.getError().profile?.realName}></field>
            <field label='邮箱' v-model={form.email} error={form.getError().email}></field>
            <field label='手机' v-model={form.phone} error={form.getError().phone}></field>
            <field label='密码' v-model={form.password} error={form.getError().password}></field>
            <button onClick={() => form.validate()}>验证</button>
            <button onClick={() => form.clearError()}>清空错误</button>
        </div> 
}})

实例出来的form继承了Validator类,有getError和validate等方法。

我希望表单类是一个十分纯粹的类,就只有表单和验证表单用的装饰器,这样以后如果有其他需求,扩展也很方便,继承了Validator类之后就不纯粹了。

之后我实现了一个useValidator方法,将表单和表单验证相关的区分了开来,useValidator用法是这样的。

export default defineComponent({
setup() {
    //往里面传入表单类,然后可以返回这些对象和方法,想用什么就解构出什么。
    const { form, errors, validateForm, clearError, toInit, toJSON } = useValidator(CreateUserForm)
    return () => (
            <div class='container'>
                <field label='用户名' v-model={form.username} error={errors.username}></field> 
                <field label='姓名' v-model={form.profile.avatar} error={errors.profile?.avatar}></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>
                <div>
                    <button onClick={() => validateForm()}>验证</button>
                    <button onClick={() => clearError()}>取消验证</button>
                    <button onClick={() => toInit()}>初始化</button>
                </div>
            </div>
        )
}
})

这样通过useValidator方法返回的值,能明显的区分开来各个功能,form也只是一个纯粹的form。想要什么功能,就解构出什么功能。

当然,里面的功能跟继承类是相似的,只是一个是调用类的方法,一个是使用函数返回的方法。也有点看个人喜好的。

PS:用表单装饰器还有一个好处,那就是很清晰的代码提示,如下图。

tip.gif

不需要再去找这个属性的表单验证是怎么样的了,直接可以在属性上看到,是不是很方便。

动态给表单设置初始值

我在之前那篇文章里有提到过通过接口动态设置表单初始值,那么我来简单实现一下。例如我现在用@InjectUsername给username来设置初始值,它将返回一个username叫最后的Hibana的值。

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

我们来简单实现@InjectUsername装饰器。

const api = {
    //一个获取用户名的api
    getUsername() {
        return Promise.resolve('最后的Hibana')
    }
}

const InjectUsername = () => (target: any, key: string) =>
    // 定义一个叫'inject:username'的元数据,传入获取用户名api的方法
    Reflect.defineMetadata('inject', api.getUsername, target, key)

// useInject来实现将api返回的值写入到username中
function useInject<T extends object>(form: T) {
    for (const key in form) {
        const api = Reflect.getMetadata('inject', form, key)
        if (api) {
            api().then(res => {
                form[key] = res
            })
        }
    }
}

那使用的话也很简单,只要调用useInject就行了,username就能动态赋值了。

export default defineComponent({
setup() {
    //往里面传入表单类,然后可以返回这些对象和方法,想用什么就解构出什么。
    const { form, errors, validateForm, clearError, toInit, toJSON } = useValidator(CreateUserForm)
    useInject(form)
    return () => ( 
         <div>
             {/* ..... */} 
         </div>
        )
}
})

如果不用装饰器,可能我们就需要在setup里调用接口来赋值。用了装饰器后,可以帮助我们隐藏这些操作。

注意:我这里实现的都是最简单的写法,其实要注意的细节有很多。比如大部分调用的api接口需要传参,那么参数该怎么传递;赋值的装饰器还可以写得再通用点;通过useInject是不是能返回更多的状态,如loading,error等,是不是也能通过这个传参。

Typescript装饰器扩展

其实装饰器不单单可以用在表单类上,也可以用在vue组件(用类的写法)上。这里我也是简单的举例一下装饰器的可能用法。

class Page {
    itemList = ref([])
    //可以实现一个Watch装饰器,监听params变化
    @Watch(page => page.loadData())
    params = reactive({
        pageIndex: 1,
        pageSize: 15,
        //...
    })

    loading = ref(false)

    @Message('加载成功','获取数据失败') // 成功/错误 消息提示
    @Loading() // 自动设置loading状态
    async loadData() {
        this.itemList = await getItemList(this.params)
    }
    
    @Debounce(1000) // debounce防止多次点击
    handleLoadClick() {
        this.loadData()
    }
}

然后在setup中使用,大概就是这样子。

export default defineComponent({
    setup() {
        const {itemList,loading,handleLoadClick} = useInject(Page)
        // ....
    }
})

我这里就不实现这些装饰器的写法了,有兴趣的小伙伴可以自己去尝试看看。

结尾

我在这篇文章介绍了使用useValidator方法来实现表单验证,以及装饰器的一些用法。正如我开头所说,Typescript装饰器是一种非常有意思的声明方法,通过装饰器可以轻松实现代理模式来使代码更简洁以及实现其它一些更有趣的能力。希望能引起小伙伴们对装饰器的兴♂趣。

另外,表单验证相关的代码有兴趣的大家可以去 AndSpark/vue-class-validator (github.com) 看看。