基于TSX的Vue3组件开发技能

jsx的集成参考vue3最新开发环境篇相关小节。本教程介绍tsx在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>
		)
	}
})

注意jsxhtml模板语法中{ ... }用来绑定变量,而( ... )用于容纳标签元素。

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>
			</>
		)
	}
})

页面效果:
在这里插入图片描述

技巧总结

  1. 响应式变量的定义和操作使用
  2. jsx中事件的绑定方式
  3. jsx中空标签的使用

属性类型定义

现在对HelloWorld组件增加一个自定义接口类型IMsgmsg属性。

方式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

  1. lang设置为tsx
  2. defineProps语法糖定义属性
  3. 响应式变量的声明要放在外面,不能放在渲染函数内部
  4. <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 的混合开发实践

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java小卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值