来给defineComponent附魔

2021-11-08 09:11:47 浏览数 (1)

前言

  • Vue3发布已经有很长一段时间了,相信各位小伙伴已经看过不少关于Vue3 typescript相关的文章,特别是前不久正式发布的script setup typescript,这个确实香,用过的小伙伴都说好!
  • 但是本文的重点并不是这个,而是jsx typescript;不知道有多少小伙伴像小编一样,既用template做开发,也用jsx做开发;反复横跳,来回切换;当然这个不是随意的,一般情况下小编写组件的时候会用jsx typescript,写页面的时候通常都是用template typescript
  • 作为一名四年多拥有数百个组件开发经验的前端开发者,今天小编来给大家分享一下在Vue3中使用jsx typescript开发组件的一些心路历程;

相关资料:

  • 官方文档 渲染函数;
  • 官方文档: jsx-next;

背景

比如现在要开发一个步进器组件,双向绑定一个数字变量。点击加号的时候绑定值加一,点击减号的时候绑定值减一;大概是长这个样子的:

先上一段使用defineComponent对这个组件简单实现的源码:

代码语言:javascript复制
const DefineNumber = defineComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        'update:modelValue': (val?: number) => true,
        'add-num': (val: number) => true,
        'sub-num': (val: number) => true,
    },
    setup(props, ctx) {
        const handler = {
            onClickAdd: () => {
                const val = props.modelValue == null ? 1 : props.modelValue   1
                ctx.emit('add-num', val)
                ctx.emit('update:modelValue', val)
            },
            onClickSub: () => {
                const val = props.modelValue == null ? 1 : props.modelValue - 1
                ctx.emit('sub-num', val)
                ctx.emit('update:modelValue', val)
            },
        }
        return () => (
            <div>
                <button onClick={handler.onClickSub}>-</button>
                <button>{props.modelValue == null ? 'N' : props.modelValue}</button>
                <button onClick={handler.onClickAdd}> </button>
            </div>
        )
    },
})
复制代码

在父组件中使用这个步进器组件:

代码语言:javascript复制
export const DemoPage = defineComponent(() => {

    const state = reactive({
        count: 123
    })

    return () => <>
        <h1>Hello world:{state.count}</h1>
        <DefineNumber
            v-model={state.count}
            onAdd-num={val => console.log('add', val)}
        />
    </>
})
复制代码

这是一个受控组件,如果没有v-model绑定值或者 state.count 不是一个响应式变量,那么这个组件将无法使用;

事件

可以看到,定义事件类型的时候是这样定义的:

代码语言:javascript复制
emits: {
    'update:modelValue': (val?: number) => true,
    'add-num': (val: number) => true,
    'sub-num': (val: number) => true,
},
复制代码

监听事件的时候,是这样监听的:

代码语言:javascript复制
onAdd-num={val => console.log('add', val)}
复制代码

add-num这种类型的普通事件,目前是正确的,可以得到正确的类型提示。但是双向绑定的事件就不行了;

  • 比如v-model,修改state.count的值为一个对象{},会发现v-model没有提示错误,实际上来说,目前这个版本的defineComponent(Vue@3.2.21)并没有推导出来v-model的类型应该为modelValue的类型;
  • 如果要监听update:modelValue事件,在template中可以这样监听:@update:modelValue;但是在tsx中,并不能像onAdd-num那样的写法实现监听事件,onUpdate:modelValue在tsx中会报错,因为这个冒号并不是一个可以编译的特殊符号。在TSX中要监听这个事件只能是这样写:
代码语言:javascript复制
<DefineNumber
    v-model={state.count}
    onAdd-num={val => console.log('add', val)}
    {...{
        'onUpdate:modelValue': (val) => console.log('get change', val)
    }}
/>
复制代码

接下来看一下经过小编改良之后的写法;

代码语言:javascript复制
import {designComponent} from 'plain-ui-composition'

const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    setup({props, event}) {

        const {emit} = event

        const handler = {
            onClickAdd: () => {
                const val = props.modelValue == null ? 1 : props.modelValue   1
                emit.onAddNum(val)
                emit.onUpdateModelValue(val)
            },
            onClickSub: () => {
                const val = props.modelValue == null ? 1 : props.modelValue - 1
                emit.onSubNum(val)
                emit.onUpdateModelValue(val)
            },
        }

        return () => (
            <div>
                <button onClick={handler.onClickSub}>-</button>
                <button>{props.modelValue == null ? 'N' : props.modelValue}</button>
                <button onClick={handler.onClickAdd}> </button>
            </div>
        )
    },
})

// 在父组件中使用
export const DemoPage = defineComponent(() => {
    const state = reactive({
        count: 123
    })
    return () => <>
        <h1>Hello world:{state.count}</h1>
        <DefineNumber
            v-model={state.count}
            onAdd-num={val => console.log('add', val)}
            {...{
                'update:modelValue': (val) => console.log('get change', val)
            }}
        />
        <DesignNumber
            v-model={state.count}
            onAddNum={val => console.log('add', val)}
            onUpdateModelValue={val => console.log('get update value', val, val?.toFixed(0))}
            onChange={val => console.log('get change value', val, val?.toFixed(0))}
        />
    </>
})
复制代码
  • 首先是defineComponent变成了designComponent
  • 然后是事件定义的写法有了不少变化。其中emits选项在定义事件类型的时候,事件的名称就是在TSX中监听事件的名称,但是在运行时派发事件的时候,会自动转化为横岗命名。比如onAddNum事件,在运行时派发事件的时候(event.emit.onAddNum(val)),会自动派发名称为add-num的事件,这样无论是在template中@add-num还是在tsx中onAddNum,都可以正确监听到事件,并且得到正确的类型提示;
  • 同时designComponent还会将v-model的类型推导为modelValue的类型,所以此时如果 state.count的类型不是number|undefined,那么DesignNumber的v-model属性就会有ts类型检测错误;
  • designComponent内部还有一个隐式的规则,那就是在派发事件onUpdateModelValue的时候,会一次性派发三个事件,顺序如下:
    • update-model-value
    • update:modelValue
    • change
  • 派发第一个事件的原因是为了适配在tsx中监听onUpdateModelValue事件;
  • 派发第二个事件的原因是为了适配v-model语法糖双向绑定值;
  • 派发第三个事件,是为了方便开发者在绑定事件的时候,同时能够方便地监听组件的值变化;比如开发者希望在一次change中,得到这一次绑定值的新值和旧值,那么可以这样写:
代码语言:javascript复制
<DesignNumber
    v-model={state.count}
    onUpdateModelValue={val => console.log([
        ['参数为新值', val],
        ['此时state.count为旧值', state.count]
    ])}
    onChange={val => console.log([
        ['参数为新值', val],
        ['此时state.count也是新值', state.count]
    ])}
/>
复制代码
  • 因为派发事件update:modelValue是一个同步的过程,所以在这个事件派发执行之前,onUpdateModelValue得到的绑定值state.count是旧值,在这个事件执行之后,onChange得到的绑定值state.count是新值;
  • 除此之外,在Vue3中去除了组件内部的事件监听机制,这里designComponent又给加上了,有大量组件开发经验的同学应该知道,事件监听机制对于组件开发来说有多重要。这里结合emits选项事件定义,deisgnComponent内部设计了一套以类型提示为优先的组件内部事件API,使用示例如下所示:
代码语言:javascript复制
const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    setup({props, event}) {

        /*派发事件*/
        event.emit.onAddNum(100)

        /*监听事件*/
        const eject = event.on.onAddNum(val => {
            // 这里val会自动推导为number类型
            console.log('on_1', val)
        })

        /*注销事件*/
        eject()

        /*监听一次事件*/
        event.once.onAddNum(val => {
            // 这里val会自动推导为number类型
            console.log('once', val)
        })

        /*监听事件,并且在组件销毁的时候移除事件*/
        /*一般来说,组件销毁的时候,自身的事件会自动注销。但是如果当前组件监听的是别的没有销毁的组件的事件的时候,就需要在销毁的时候注销这个事件监听*/
        onBeforeUnmount(event.on.onAddNum(val => {
            console.log('on_2', val)
        }))

        /*手动注销事件*/
        const handler: Parameters<typeof event.on.onAddNum>[0] = val => {
            console.log('on_2', val)
        }
        event.on.onAddNum(handler)
        setTimeout(() => {
            event.off.onAddNum(handler)
        })

        return () => null
    },
})
复制代码

插槽

  • 在Vue3中,并没有对插槽的定义有特别的说明,当小编看到Vue3的正式版本发布之后,对于这一块内容有一些小小的失望。因为插槽的维护,在之前Vue2的版本中曾经对小编造成很大的困扰;
  • 在之前的Vue2中,组件在定义的时候不需要声明事件、不需要声明插槽。组件所派发的事件,以及使用的插槽遍布在文件的各个角落,有时候甚至不确定派发的事件是不是在其他组件内调用的。需要调整别人写的组件的时候,经常是需要在组件内部搜索$emits,slot等关键字,来判断这个组件会派发哪些事件,会有哪些插槽。哪些插槽是普通插槽,哪些插槽是作用域插槽,作用域插槽的参数类型是什么。这些基本上全靠开发者自觉,把这些信息作为注释补充在组件内。有时候想要强制执行这个注释规范,也无从下手,因为没有办法对老的组件做这样规范的调整,也没有办法完全把控组件开发者的代码质量;
  • 早在Vue2的@vue/composition-api的时候,designComponent就有一套定义事件类型的选项,同样的也有定义插槽以及作用域插槽的选项,如下所示;

比如现在DesignNumber组件需要能够自定义加减按钮的内容(插槽),以及显示值的内容(作用域插槽,参数为当前值);示例代码如下所示:

代码语言:javascript复制
const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    slots: [
        'add',
        'sub',
    ],
    scopeSlots: {
        default: (scope?: number) => {},
    },
    setup({props, event: {emit}, slots, scopeSlots}) {

        const handler = {
            onClickAdd: () => {
                const val = props.modelValue == null ? 1 : props.modelValue   1
                emit.onAddNum(val)
                emit.onUpdateModelValue(val)
            },
            onClickSub: () => {
                const val = props.modelValue == null ? 1 : props.modelValue - 1
                emit.onSubNum(val)
                emit.onUpdateModelValue(val)
            },
        }

        return () => (
            <div>
                <button onClick={handler.onClickSub}>{slots.sub(<span>-</span>)}</button>
                {
                    scopeSlots.default(
                        props.modelValue,
                        <button>{props.modelValue == null ? 'N' : props.modelValue}</button>,
                    )
                }
                <button onClick={handler.onClickAdd}>{slots.add(<span> </span>)}</button>
            </div>
        )
    },
})

// 使用组件
export const DemoPage = defineComponent(() => {
    const state = reactive({
        count: 123
    })
    return () => <>
        <h1>Hello world:{state.count}</h1>
        {/*<DefineNumber
            v-model={state.count}
            onAdd-num={val => console.log('add', val)}
            {...{
                'update:modelValue': (val) => console.log('get change', val)
            }}
        />*/}

        <DesignNumber v-model={state.count}/>

        <DesignNumber v-model={state.count} v-slots={{
            add: () => <span>add</span>,
            sub: () => <span>sub</span>,
            default: val => <input type="text" value={val}/>
        }}/>
    </>
})
复制代码
  • 插槽
    • slots选项是一个字符串数组;
    • setup函数会得到一个slots对象,slots对象每个value都是一个函数,函数参数就是默认的插槽内容;当组件接收到自定义插槽内容的时候,就使用这个自定义内容,否则使用默认插槽内容;
  • 作用域插槽
    • scopeSlots选项是一个对象,对象的key就是插槽名称,值就是作用域插槽的函数类型。这个函数只有一个参数,这个参数所定义的类型就是使用这个组件的时候得到的作用域对象类型;
    • setup函数会得到一个scopeSlots对象,每个value都是一个渲染作用域插槽内容的函数。函数有两个参数,第一个参数是作用域对象,第二个参数就是默认的内容。当父组件没有自定义这个作用域插槽时,渲染的就是这个默认内容;
  • v-slots
    • 在jsx中给组件传递插槽的方式有两种,这个是官方自带的。一个是通过v-slots传递一个对象,对象的key就是插槽的名称,value必须是一个函数。另一种就是通过组件的children的位置传递;比如上面例子中的写法可以改为:
代码语言:javascript复制
<DesignNumber v-model={state.count}>
    {{ 
        add: () => <span>add</span>,
        sub: () => <span>sub</span>,
        default: val => <input type="text" value={val}/>
    }}
</DesignNumber>
复制代码

注意的是,plain-ui-composition目前仅支持v-slots带类型推导,上面这种通过children方式传递的方式目前仍不支持类型推导(也就是说,上面的代码,default插槽函数中的val参数会推导为隐式的any类型)。但是plain-design-composition是支持children的方式传递并且带类型推导的;这个只能说小编目前学艺不精,暂时无法实现定义组件children的类型。

引用

  • 父子组件间的通信最常用的方式应该就是父组件向子组件传递属性,父组件监听子组件派发的事件;不过这种方式有比较大的限制,灵活性不高。这时候子组件为了能够充分发挥自己的能力,可以通过暴露一些方法以及状态变量,父组件得到子组件的引用之后使用这些暴露的方法以及变量以便实现更加复杂的功能;比较常见的一个场景就是,在写表单的时候,提交表单之前要先调用表单组件的校验函数,校验通过之后才可以将表单数据提交到后台;
  • 获取引用一般就两种:
    • 获取dom节点的引用;
    • 获取自定义组件的引用;
  • 在designComponent中,为了能够在获取引用的时候得到充分的类型提示,提供了一个叫做useRefs的一个函数来管理对子节点的引用,示例如下所示;
代码语言:javascript复制
const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    setup({props, event: {emit}}) {
        const handler = {
            onClickAdd: () => {
                const val = props.modelValue == null ? 1 : props.modelValue   1
                emit.onAddNum(val)
                emit.onUpdateModelValue(val)
            },
            onClickSub: () => {
                const val = props.modelValue == null ? 1 : props.modelValue - 1
                emit.onSubNum(val)
                emit.onUpdateModelValue(val)
            },
        }
        const methods = {
            reset: (val?: number) => {
                emit.onUpdateModelValue(val == null ? 0 : val)
            },
        }
        return {
            refer: {
                methods,
            },
            render: () => (
                <div>
                    <button onClick={handler.onClickSub}>-</button>
                    <button>{props.modelValue == null ? 'N' : props.modelValue}</button>
                    <button onClick={handler.onClickAdd}> </button>
                </div>
            ),
        }
    },
})

// 使用组件的代码
export const DemoPage = defineComponent(() => {

    const {refs, onRef} = useRefs({
        number: DesignNumber,                   // 获取DesignNumber组件的引用
        btn: iHTMLButtonElement,                // 获取button节点的引用
    })

    const state = reactive({
        count: 123
    })

    const handler = {
        onReset: () => {
            console.log(refs)
            refs.number?.methods.reset()
        },
    }

    return () => <>
        <h1>Hello world:{state.count}</h1>
        <DesignNumber v-model={state.count} ref={onRef.number}/>
        <button ref={onRef.btn} onClick={handler.onReset}>reset value</button>
    </>
})
复制代码
  • 首先是使用useRefs声明需要引用的子组件,会得到refs以及onRef两个对象;
  • 需要将onRef中的的值赋值给对应子组件的ref属性,之后就可以把refs当做一个总的组件引用对象来使用了。
  • 除此之外还有类型提示的功能;比如refs.number的类型为DesignNumber组件最后返回的refer对象,如果示例代码中的 reset 函数的val参数类型去掉问号,变成必须的参数,此时DemoPage中的refs.number?.methods.reset()就会有类型提示错误,缺少必填参数val;同理此时refs.btnHTMLButtonElementdom对象,可以得到对应的类型提示;
  • iHTMLButtonElement是一个匿名对象{},但是类型为HTMLButtonElement
    • 源码:export const iHTMLButtonElement = {} as typeof HTMLButtonElement
    • 原因是某些非浏览器环境,比如小程序,比如SSR中是没有HTMLButtonElement这个对象的,这里plain-ui-omposition导出这个用来辅助类型提示的一个简单对象;
    • 如果可以确保代码运行在浏览器环境,那么把iHTMLButtonElement换成HTMLButtonElement也是可以的;

注入

  • 上面提到了父组件在引用子组件的时候如何得到类型提示,这个仅适用于父子组件的情况。当使用provide/inject,与子孙组件通信的时候,这个方法就不适用了。
  • 接下来示例如何在注入的时候得到注入对象的类型;
代码语言:javascript复制
// 向子孙组件提供状态的父组件
const DesignNumber = designComponent({
    provideRefer: true,
    name: 'design-number',
    setup() {
        const methods = {
            reset: (val?: number) => {
                console.log('reset', val)
            },
        }
        return {
            refer: {
                methods,
            },
            render: () => null,
        }
    },
})

// 子孙组件注入对象
const DesignNumberChild = designComponent({
    setup() {
        // inject没有给默认值,这里意思为必须注入父组件DesignNumber,否则运行时错误
        const certainParent = DesignNumber.use.inject()
        console.log(certainParent.methods.reset())

        // inject有默认值null,当没有注入父组件DesignNumber的时候,默认值就是这个null
        const uncertainParent = DesignNumber.use.inject(null)
        console.log(uncertainParent?.methods.reset())   // uncertainParent后面得加上可选操作符?,否则会有 Object is possibly null的ts错误
        
        return () => null
    },
})
复制代码
  • 首先是向子孙组件提供数据的父组件DesignNumber需要提供两个选项:
    • name:'design-number'
    • provideRefer:true
  • 这样在 组件运行的时候会自动执行 provide('@@design-number',refer)
  • 然后子组件只需要调用DesignNumber.use.inject()就可以注入父组件提供的状态变量。这个inject函数与Vue3标准的inject函数一样,只是这个inject函数会提供类型提示的功能;

继承

  • 在Vue3中,给一个子组件传递属性,如果某些属性并没有在props以及emits中声明,那么这个属性会存到attrs中,并且默认情况下会传递给这个子组件的根节点,如果这个子组件是多根节点,那么就会触发运行时的警告;
  • 在tsx中,给一个组件传递没有定义在props或者emits中的属性,会导致ts编译错误;
  • 接下来示例如何在designComponent中,声明继承的属性类型;
代码语言:javascript复制
const DesignNumber = designComponent({
    props: {
        modelValue: {type: Number},
        max: {type: Number},
        min: {type: Number,},
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
        onAddNum: (val: number) => true,
        onSubNum: (val: number) => true,
    },
    setup() {
        return () => null
    },
})
const InheritHTMLButton = designComponent({
    inheritPropsType: iHTMLButtonElement,
    props: {
        // 自定义type属性,覆盖button的type属性
        // 由于定义在了props中,所以type不会自动传递给根节点button
        type: {type: Number}
    },
    setup() {
        return () => <button/>
    },
})
const InheritDesignNumber = designComponent({
    inheritPropsType: DesignNumber,
    props: {
        // 覆盖DesignNumber的max属性类型为string
        max: {type: String},
        // 自定义必传的属性
        precision: {type: Number, required: true}
    },
    emits: {
        // 覆盖DesignNumber的onAddNum事件的参数类型为string
        onAddNum: (val: string) => true,
        onAddNum2: (val: number) => true,
    },
    setup() {
        return () => <DesignNumber/>
    },
})

export const DemoPage = defineComponent(() => {
    const state = reactive({
        count: 123
    })
    return () => <>
        {/*tabindex,继承button的属性*/}
        {/*type,覆盖的属性*/}
        <InheritHTMLButton tabindex={1} type={100}/>

        {/*precision,为自定义必填的属性*/}
        {/*max,覆盖类型为string*/}
        {/*min,继承属性*/}
        {/*onAddNum,覆盖类型为函数,函数参数为string*/}
        {/*onAddNum2,自定义的事件*/}
        {/*onSubNum,继承事件类型*/}
        <InheritDesignNumber
            precision={100}
            max={"100"}
            min={100}
            onAddNum={val => console.log(val.charAt(0))}
            onAddNum2={val => console.log(val.toPrecision(0))}
            onSubNum={val => console.log(val.toPrecision(0))}
        />
    </>
})
复制代码
  • 示例中有两个继承属性的组件:
    • InheritHTMLButton继承原生button组件的属性;
    • InheritDesignNumber继承自定义组件DesignNumber的属性;
  • 定义组件的时候,通过inheritPropsType选项就可以指定继承的属性类型;这个选项的唯一作用也是提供继承属性类型提示,运行时是没有任何作用的;
  • 如果组件本身定义的属性和事件与继承的属性事件名称冲突,那么最后这个同名的属性事件,以组件本身定义的为主,因为此时这个属性不会被自动传递到根节点;
  • inheritPropsType结合inheritAttrs选项还可以有另外一种用法;
    • 比如现在要基于input原生组件封装一个PlInput组件,但是这个PlInput组件的根节点不是input,而是一个div,因为有这个div可以丰富PlInput组件的功能,比如显示后缀图标、前置内容插槽,后置内容插槽等等;
    • 这种情况下,似乎给PlInput在定义继承属性类型的时候,设置为HTMLDivElement比较合理,但是在真实的开发场景中,往往对input节点设置属性的情况比较多,反而对根节点div设置属性的场景不多。
    • 基于这种场景可以这么做:1、设置 inheritPropsType 继承属性类型仍然为HTMLInputElement; 2、设置 inheritAttrs:false,不自动将额外的属性传递给根节点,而是在setup函数中,手动将attrs传递给input节点,示例代码如下所示:
代码语言:javascript复制
const PlInput = designComponent({
    inheritPropsType: HTMLInputElement,
    inheritAttrs: false,
    props: {
        modelValue: {type: String},
        wrapperAttrs: {type: Object}
    },
    setup({props, attrs}) {
        return () => (
            /*如果开发者需要设置根节点属性,通过wrapperAttrs这个属性对象设置即可*/
            <div {...props.wrapperAttrs}>
                {/*手动将attrs传递给input节点*/}
                <input type="text" {...attrs}/>
            </div>
        )
    },
})

export const App = () => <>
    {/*div没有type属性,这里会有ts编译报错提示*/}
    {/*<div type="submit"/>*/}

    {/*PlInput继承的是HTMLInputElement属性类型,所以支持接收type属性;因为设置了inheritAttrs:false,所以虽然type没有定义在props中,但是不会传递给根节点div,而是手动通过attrs传递给了input节点*/}
    <PlInput wrapperAttrs={{class: 'class-on-div'}} class="class-on-input" type="submit"/>
</>
复制代码

绑定

  • plain-ui-composition的出现,是小编在开发组件库plain-ui的时候一步一步摸索出来的;
  • 目前plain-ui所有支持绑定的组件,都是非受控组件;关于受控组件与非受控组件的区别以及优缺点,网上有特别多的文章做了详细说明,这里就不再赘述了。这里小编介绍一下plain-ui-composition中,用来快速实现非受控组件绑定值的一个组合函数——useModel

单值绑定:实现一个计数器组件,点击加号(减号)按钮可以使得绑定值计数加一(减一)

代码语言:javascript复制
const PlNumber = designComponent({
    props: {
        modelValue: {type: Number}
    },
    emits: {
        onUpdateModelValue: (val?: number) => true,
    },
    setup({props, event: {emit}}) {

        const model = useModel(() => props.modelValue, emit.onUpdateModelValue)

        return () => (
            <div>
                <button onClick={() => model.value = (model.value == null ? 0 : model.value - 1)}>-</button>
                <button>{model.value == null ? 'N' : model.value}</button>
                <button onClick={() => model.value = (model.value == null ? 0 : model.value   1)}> </button>
            </div>
        )
    },
})

export const App = designComponent({
    setup() {
        const state = reactive({
            count: undefined
        })
        return () => <>
            <PlNumber v-model={state.count}/>
            <PlNumber v-model={state.count}/>
            {state.count}
        </>
    },
})
复制代码

多值绑定:

  • 实现一个编辑数字的组件:PlNumber;
  • 定义一个range属性,没有设置range为true时,编辑单值,绑定也是单值;
  • range为true时,编辑多值,绑定也是多值;
代码语言:javascript复制
const PlNumber = designComponent({
    props: {
        modelValue: {type: [Number, String]},
        range: {type: Boolean},
        start: {type: [Number, String]},
        end: {type: [Number, String]},
    },
    emits: {
        onUpdateModelValue: (val?: number | string) => true,
        onUpdateStart: (val?: number | string) => true,
        onUpdateEnd: (val?: number | string) => true,
    },
    setup({props, event: {emit}}) {

        const model = useModel(() => props.modelValue, emit.onUpdateModelValue)
        const startModel = useModel(() => props.start, emit.onUpdateStart)
        const endModel = useModel(() => props.end, emit.onUpdateEnd)

        return () => (
            <div>
                {
                    !props.range ? <>
                        <button onClick={() => model.value = (model.value == null ? 0 : Number(model.value) - 1)}>-</button>
                        <button>{model.value == null ? 'N' : model.value}</button>
                        <button onClick={() => model.value = (model.value == null ? 0 : Number(model.value)   1)}> </button>
                    </> : (<>
                        <input type="text" v-model={startModel.value}/>
                        至
                        <input type="text" v-model={endModel.value}/>
                    </>)
                }
            </div>
        )
    },
})

export const App = designComponent({
    setup() {
        const state = reactive({
            formData: {
                value: undefined,
                startValue: undefined,
                endValue: undefined,
            },
        })
        return () => <>
            {/*单独使用*/}
            <PlNumber v-model={state.formData.value}/>
            {/*输入数字范围*/}
            <PlNumber range v-models={[
                [state.formData.startValue, 'start'],
                [state.formData.endValue, 'end'],
            ]}/>
            {JSON.stringify(state.formData)}
        </>
    },
})
复制代码

在template中绑定多值的时候,写法为:

代码语言:javascript复制
<template>
    <pl-number v-model:start="state.formData.startValue" v-model:end="state.formData.endValue"/>
</template>
复制代码

结语

  • 附上小编录制的一个视频,对本文的一些例子的一些说明,视频地址:www.bilibili.com/video/BV1Q4…;
  • plain-ui是一个Vue3.0组件库,是以plain-ui-composition为基础开发的,目前所有的组件都是使用jsx typescript composition api开发的,有需要的同学可以参考一下部分组件的源码;目前组件库的默认主题色是绿色,在线文档地址:plain-pot.gitee.io/plain-ui-do…;有时间小编会专门写一篇文章介绍一下这个组件库中的个人研发的比较有意思的内容;
  • plain-ui-composition有一个孪生兄弟——plain-design-composition,这个库是帮助开发者通过几行代码配置就能够在已有的React应用程序中使用VueCompositionApi以及双向绑定功能的工具库,其类型提示要比plain-ui-composition更为强大以及准确;而且API一模一样。在线文档地址:plain-pot.gitee.io/plain-desig…;有时间小编也会专门写一篇文档介绍这个库;
  • plain-design是基于plain-design-composition开发的一套React组件库,目前默认主题色为深蓝色,在线文档地址:plain-pot.gitee.io/plain-desig…;这个组件库可以直接用于现有的React应用中,与其他已有的React组件库共存;有意思的是,里边组件的源码与plain-ui极度相似,这个与前几天Semi Design所提出的fundation/adapter架构不谋而合;区别是目前plain仅实现了Vue3.0以及React;fundation部分由plain-ui-compositionplain-design-composition实现,adapter由开发者实现。adapter中的代码复用率高达99%,大部分情况下,去掉组件中的类型变成es6源码之后,很难分辨出来哪个是Vue组件,哪个是React组件;
  • 小编在Vue jsx typescript这条路的探索大概花了两年多的时间,以上是部分探索内容的总结。当然不同的人会有不同的看法,有人可能会认为基于defineComponent封装一个designComponent,就是“你随意篡改我的歌词就是画蛇添足”,也有部分人可能会从本文的一些例子中得到启发。不管怎样,首先小编并没有恶意,并不打算改变任何人的编程习惯,如果你用jsx typescript composition api从本文中得到了一些启发,那么恭喜你,又学到了一招。如果你更喜欢用template typescript,不用甚至反感jsx,那么一笑而过就好了。最后一点是,上述的所有开源库都不是KPI驱动(至少目前并没有从中得到任何的回报或者工作上的晋升),并不是为了实现而实现,而是日常生活中有了这个想法,才去实现。

0 人点赞