再聊Vue的TSX函数式组件

5,324 阅读4分钟

前几天发了一篇《DevUI中VUE的TSX函数式组件实践》,主要讲如何使用tsx,在Vue3中实现函数式组件的写法。

这里再追述一些内容。

使用TSX的原因

代码质量约束

使用ts,可以提高开发侧代码的质量……

当然,我清楚这是通过强类型倒逼开发者的方式实现,逼死的情况也可能存在,但也请看到正面的积极的作用:强化抽象思维,强化整体观,代码静态纠错,等等。

如果开发者能越过这道门槛,相信能跃迁到另一个层级。

编译期间自动生成d.ts文件

假设你项目里装了typescript包,运行下面的脚本:

npx tsc src/index.ts --outDir public -d

你会得到一个和src结构一致的目录,如下图:

image.png

(无视截图的css文件哈)

这里,tsc后面

  • 第一个参数是入口文件
  • --outDir是输出目录
  • -d表示生成声明文件

请注意:tsc是一个独立的、功能完整的ts编译工具,它会根据入口文件和引用依赖,编译整个项目,且支持tsx。 至于打包、压缩,是另一个事。

如果带着d.ts文件发布为npm包,那么在引用这个包的时候,VSCode会根据入口文件对应的d.ts,递归出所有的输出类型。

很多早期发布的包,没有带d.ts,或者不是基于ts开发的,都是通过@types/xxx额外的npm包来弥补,比如@types/node

其实最爽的,是在生成d.ts文件时,不仅会提取输出模块的类型,而且会将JSDoc格式的注释也提取出来。

同时,JSDoc支持的markdown写法,也就是说,注释提示可以带格式。

type TProps = {
    /**
     * ### 图片地址URL
     */
    src?: string
    /**
     * ### 指定宽度
     */
    width?: number
    /**
     * ### 指定高度
     */
    height?: number
    /**
     * ### 填充模式
     * - `stretch` 拉伸充满
     * - `cover` 裁切充满,保持图片比例
     * - `contain` 全部显示,留白,保持图片比例
     */
    mode?: 'stretch' | 'cover' | 'contain'
}

image.png

就很专业了。

Vue文件的代码提示较弱

根据在DevUI项目中对vue使用经历,感觉.vue文件,以及Vue本身的类型推断,有些局限,而且IDE支持也没有那么好。

比如上面的Image组件的mode属性,用vue的话props这么写:

export default defineComponent({
    props: {
        mode: { type: String }
    }
})

因为vue背后是做了类型推断的,如果说在js范畴,这种props写法也能够提示出属性的名称、划线属性类型错误,其实也非常nb了。但到了ts范畴,就相对弱了很多。

tsx这么写

export default (props: { mode: 'stretch' | 'cover' | 'contain' }) => <img ...>

在vue的写法中,只能约束到string这种大的类型,但用了ts可以更准确到枚举值验证,而且IDE还能把枚举值列出来。

image.png

你说气不气人。

为什么是函数式组件

借鉴React成熟做法

在写React的时候,如果写一个类组件,大概是这个样子

import React from 'react'
export class extends React.Component<{ mode?: string }> {
    //...
}

能看到组件对react包是有实际依赖的。

但函数式组件就不同了

import React from 'react'
export default (props: { mode?: string }) => <img mode={props.mode} />

这里对react的引用,实际是一个历史遗留问题,本身函数组件对react并没有任何依赖。

但也正是这个遗留问题,造成babel-loader在解析jsx的时候,如果发现没有react引用会报错。

上一篇提到的@vue/babel-plugin-jsx插件就是解决这个问题的,这样,这个组件就可以很纯粹地变成:

export default (props: { mode?: string }) => <img mode={props.mode} />

IDE对函数式组件的良好支持

我们把这种函数式组件,可以抽象成一种类型

type TVueFuncComponent<TProps> = (props: TProps) => JSX.Element

重点来了:VSCode,对这种函数式组件类型,支持非常友好。

在现有的vue写法中,都是基于props来做类型推测,也未见官方发布函数式组件的正规写法。

Vue函数式组件的实现方法

在上一篇文章中,已经列举了了两种,这里汇总成3种组件:

无状态组件

export default (props: { mode?: string }) => <img mode={props.mode} />

这个就不多说了,熟悉jsx语法就好了。

有状态组件

高阶函数

import { reactive } from 'vue'

const factory = () => {
    const state = reactive({ count: 0 })
    return (props: TProps) => {
        return <button onClick={() => state.count += 1}>
            Click me {state.count} times
        </button>
    }
}
export default factory()

生命周期控制

高阶组件

import { onMounted, defineComponent } from 'vue'

const Comp = defineComponent({
    props: {
        mode: { type: String }
    },
    setup(props) {
        return () => <img mode={props.mode} />
    }
})

export default (props: TProps) => <Comp { ...props } />

经实际编译验证,由于这三种类型都可以输出成TVueFuncComponent<TProps>类型,IDE按照函数式JSX组件解析d.ts,可以完美实现代码提示。

jsx组件只能在jsx中引用时,才能有提示,在.vue文件中的template里面无法提示,template不是jsx。

以上。