基于TSX的Vue3组件开发技能
基于TSX的Vue3组件开发技能
本文是一篇关于Vue3组件开发中使用TSX的教程。文章详细介绍了TSX在Vue组件开发中的使用技巧,包括TSX快速入门、属性类型定义、scoped样式、自定义指令等,并通过两个实战案例(扁平化Tree和树的折叠展开)来加深理解。文章内容丰富,结构清晰,适合有一定Vue基础的开发者阅读。
tsx快速入门
编写一个 HelloWorld.tsx
组件:
import { defineComponent } from 'vue'
// 通过defineComponent来实现setup语法和jsx风格的组件模板渲染
export default defineComponent({
// 组件名
name: 'HelloWorld',
// 组件属性定义
props: {
// 定义一个string类型非必需的msg属性
msg: {
type: String,
required: false,
default: 'Hello World!'
}
},
// 在setup方法种实现数据模型、事件定义以及模板渲染等逻辑
// 参数可接收组件属性定义对象、vue上下文中可注入的组件实例,如:slots、emit、expose等
setup(props, ctx) {
console.log(props, ctx)
// 解构属性
const { msg } = props
// jsx语法返回的模板渲染内容
return () => (
<div>{ msg }</div>
)
}
})
注意jsx的html模板语法中 { ... }
用来绑定变量,而 ( ... )
用于容纳标签元素。
在 App.tsx
中引入和使用:
import { defineComponent } from 'vue'
// 引入组件
import HelloWorld from './components/HelloWorld'
// 通过defineComponent来实现setup语法和jsx风格的组件模板渲染
export default defineComponent({
// 在setup方法种实现数据模型、事件定义以及模板渲染等逻辑
// 参数可接收组件属性定义对象、vue上下文中可注入的组件实例,如:slots、emit、expose等
setup(props, ctx) {
console.log(props, ctx)
// jsx语法返回的模板渲染内容
return () => (
<div>
{/* 使用组件 */}
<HelloWorld />
</div>
)
}
})
npm run dev,页面展示:
在这个示例基础上加上计数器功能:
import { ..., ref } from 'vue'
export default defineComponent({
...
setup(props, ctx) {
...
// 响应式变量声明
const count = ref(0)
// 定义click处理函数
const handleCount = () => {
// 操作响应式变量的值,自增
count.value++
}
return () => (
// 空标签类似于vue2中template标签的作用
<>
<h3>{ msg }</h3>
{/* jsx中事件固定命名onXxx,jsx模板中获取响应式变量的值要用value来获取!! */}
<button onClick={ handleCount } >count is { count.value }</button>
</>
)
}
})
页面效果:
技巧总结
- 响应式变量的定义和操作使用
- jsx中事件的绑定方式
- jsx中空标签的使用
属性类型定义
现在对 HelloWorld
组件增加一个自定义接口类型 IMsg
的 msg
属性。
方式1 - PropType泛型
import { ..., PropType } from 'vue'
// 自定义一个消息类型
interface IMsg {
info: string // 必选属性info
}
export default defineComponent({
...
props: {
// 定义一个IMsg类型且必需的msg属性
msg: {
type: Object as PropType<IMsg>,
required: true
}
},
setup(props, ctx) {
...
// 解构属性,此时msg为IMsg类型
const { msg } = props
...
return () => (
<>
{/* 因为msg为必需属性,因此访问其info属性是安全的 */}
<h3>{ msg.info }</h3>
...
</>
)
}
})
注意在 App.tsx
中使用 HelloWorld
组件方式的调整:
<HelloWorld msg={ { info: 'hello world!!' } } />。
jsx属性动态绑定的值放在 { ... }
中,这里为一个字面量形式的对象,注意 info
属性必须是 string
类型。
页面效果:
方式2 - defineProps
改造 HelloWorld.tsx
定义形式为 HelloWorld.vue
。
<script setup lang="tsx">
import { ref } from 'vue'
// 自定义一个消息类型
interface IMsg {
info: string // 必选属性info
}
const props = defineProps<{
msg: IMsg
}>()
// 响应式变量声明
const count = ref(0)
const render = () => {
const { msg } = props
// 定义click处理函数
const handleCount = () => {
// 操作响应式变量的值,自增
count.value++
}
return (
<>
{/* 因为msg为必需属性,因此访问其info属性是安全的 */}
<h3>{ msg.info }</h3>
{/* jsx中事件固定命名onXxx,jsx模板中获取响应式变量的值要用value来获取!! */}
<button onClick={ handleCount } >count is { count.value }</button>
</>
)
}
</script>
<template>
<component :is="render()"></component>
</template>
SFC(单文件组件)形式的tsx
- lang设置为tsx
defineProps
语法糖定义属性- 响应式变量的声明要放在外面,不能放在渲染函数内部
- 在
<template>
中使用动态组件渲染技术,调用包装好的渲染函数
scoped样式
SFC 版的 tsx 组件中 scoped 样式依然有效。
在 HelloWorld.vue
组件中增加:
<style scoped>
h3 {
color: red;
}
</style>
发现只有该组件内的 h3
元素的样式生效了。
自定义指令
jsx 的事件修饰符中并没有提供 enter
回车事件处理。为此我们可以通过自定义指令来封装这一通用功能。用法:
<input type="text" v-enterkey={ ($event: KeyboardEvent) => console.log('cpdd...', $event) }/>
封装方式:
// 自定义给input输入框用的v-enterkey指令
// 指令回调函数中完成元素的keyup事件绑定的初始化,这里采用了ts类型来明确参数类型
app.directive('enterkey', (el: HTMLElement, binding: DirectiveBinding<Function>) => {
// 事件处理函数内部会调用指令绑定的用户定义函数
const handler = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
// 这里明确是Function类型
const handlerFn = binding.value
// 因为是函数,所以可以直接调用
handlerFn(event)
}
}
el.addEventListener('keyup', handler)
})
通过 ts 泛型明确声明了 enterkey
指令绑定的值为一个 Function
类型。
页面效果,输入后按回车:
实战1 - 扁平化Tree
假设有这样一票数据,层级嵌套的树结构,要求你用 vue 把它渲染成一棵树,你会怎么做?
扁平化的思路
传统做法是对每一层级都进行组件的递归渲染,这种方式不利于后续的层级操作,避免不了一些递归处理。创新的做法是,将它转成扁平化结构进行处理就容器多了。
节点类型定义
按照之前的思路,树节点的类型会有两种:一种是原始嵌套结构的树节点,另一种是拍平后的扁平化树节点。创建一个 ts 文件: src/components/tree/types.ts
// 节点id定义,id可以是字符串也可以是数值类型
export type IdType = string | number
// 结构化节点
export interface ITreeNode {
id: IdType // 节点id
label: string // 节点名称
children?: ITreeNode[] // 子一代节点列表,可选,没有则说明是叶子节点
expanded?: boolean // 是否展开,可选,默认折叠,注意是父节点才有该属性
}
// 扩展的扁平化节点
export interface IFlatTreeNode extends ITreeNode {
parentId?: IdType // 关联父节点id,可选
level: number // 节点层级,从1开始
isLeaf: boolean // 是否是叶子节点
originalNode: ITreeNode // 关联原始结构化节点
}
树结构拍平
为了适配外部传入的树结构数据,可以允许一些属性用户自定义,为此我们定义一个配置属性:
// 用于树结构数据中节点名称和子节点列表属性命名的映射
export interface OptionProps {
labelName: string // 树节点名称
childrenName: string // 子节点列表的名称
}
核心树结构拍平处理函数, src/components/tree/utils.ts
import { IdType, IFlatTreeNode, ITreeNode, OptionProps } from './types'
/**
* 生成扁平化树结构的功能函数
* @param data 当前层级的树节点列表
* @param optionProps 节点选项属性定义
* @param level 当前层级
* @param pid 父节点id,可为空
* 返回转化后的flat节点列表
*/
export function generateFlatTree(data: ITreeNode[], optionProps: OptionProps, level = 0, pid: IdType | null = null): IFlatTreeNode[] {
level++ // 当前层级自增
// 对数组每一项进行拍平,处理结果放到tempArr
return data.reduce((tempArr, cur) => {
// 拷贝原始节点为扁平化节点
const flatNode = { ...cur } as IFlatTreeNode
// 绑定原始节点
flatNode.originalNode = cur
// 设置扁平化节点的层级
flatNode.level = level
// 如果当前层级大于1,说明一定有父节点,绑定父节点id
if (level > 1) {
// 注意这里的写法!表示断言pid一定不为空
flatNode.parentId = pid!
}
// 获取子节点列表的名称定义,注意用as来关联节点ts类型的属性名
const childrenName = optionProps.childrenName as 'children'
// 获取子节点列表
const children = flatNode[childrenName]
if (children) {
// 如果当前节点是父节点,则对其子节点列表进行递归拍平
const flatChildren = generateFlatTree(children, optionProps, level, flatNode.id)
// 删除扁平化节点的子节点列表属性,也就是取消其嵌套结构
delete flatNode[childrenName]
// 设置为非叶子节点
flatNode.isLeaf = false
// 将当前flat节点和后续flat节点列表添加到结果数组中
return tempArr.concat(flatNode, flatChildren)
} else { // 叶子节点的处理就相对简单
// 设置为叶子节点
flatNode.isLeaf = true
// 将flat节点添加到结果数组中
return tempArr.concat(flatNode)
}
}, [] as IFlatTreeNode[]) // 初始化tempArr为空数组
}
在 utils.ts
中增加测试脚本:
用 ts-node
工具测试下。如果没装,全局安装下: npm i -g ts-node
。并在 tsconfig.json
中加一项配置:
测试命令: ts-node .\src\components\tree\utils.ts
。将输出结果复制到浏览器控制台,输出如下,ok!
Tree组件属性定义
在 types.ts
中增加组件属性定义部分:
import { ExtractPropTypes, PropType } from 'vue'
// 导出tree组件的属性定义
export const props = {
// 实际提供给tree的属性是拍平后扁平化结构的数据
data: {
type: Object as PropType<Array<ITreeNode>>,
required: true
},
// 选项属性
optionProps: {
type: Object as PropType<OptionProps>,
// 设置选项属性的默认值
default() {
return {
label: 'label',
children: 'children'
}
}
}
} as const // 注意属性设置为只读的,外面不能修改,同时也避免传空的情况
组件模板实现
src/components/tree/index.tsx
import { defineComponent } from 'vue'
import { props, Props } from './types'
import { generateFlatTree } from './utils'
export default defineComponent({
name: 'FxTree',
props, // 属性定义
setup(props: Props) { // 实际属性对象
// 属性解构
const { data, optionProps } = props
const { labelName } = optionProps
// 将树拍平
const flatData = generateFlatTree(data, optionProps)
return () => {
return (
<div class='fx-tree'>
{/* v-for的tsx版本 */}
{flatData.map((node) => (
/* 注意遍历渲染的key不能少;通过一定层级的左留白实现树的嵌套效果 */
<div key={node.id} class='fx-tree-node' style={{ paddingLeft: `${24 * (node.level - 1)}px` }}>
{/* label属性需要动态取 */}
{node[labelName as 'label']}
</div>
))}
</div>
)
}
}
})
组件使用示例
src/App.vue
页面效果:
实战2 - 树的折叠展开
思路1 - 排除折叠节点的子节点
思路2 - v-show方式
待更新。。。
参考教程
稀土掘金 - RR9【Vue3干货👍】template setup 和 tsx 的混合开发实践