问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

基于TSX的Vue3组件开发技能

创作时间:
作者:
@小白创作中心

基于TSX的Vue3组件开发技能

引用
CSDN
1.
https://blog.csdn.net/felix_alone2012/article/details/140350054

本文是一篇关于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>
      </>
    )
  }
})

页面效果:

技巧总结

  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 的混合开发实践

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号