基础入门:Vue3 + TSX语法最佳实践

555 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

从Vue3发布到现在已经很久了,Vue相关的生态已逐渐形成。自从使用Vue3后写最多的就是SFC单文件组件。之前也总结过一篇关于SFC单文件组件的语法实践:Vite+Vue3.2+TS 相关API归纳总结

但是最近也是看了开源社区中许多组件库在搭建时使用的 Vue3 + TSX 这样一套语法去开发,这就让我对 TSX 的写法好奇起来。虽然再次之前很久以前使用过一段时间的 React 但写 JSX/TSX 语法依然还是有点不知所措。

所以也同样稍微体验了一下并且总结才输出着一篇文章,希望有帮助。

基本使用

JSX 是 JavaScript 的一个类似 XML 的扩展

通过Vite搭建的项目,在Vue中使用JSX语法,我们需要使用到一个插件来进行语法支持:@vitejs/plugin-vue-jsx

  1. 安装:pnpm install @vitejs/plugin-vue-jsx -D

  2. vite.config.ts 中配置文件

import { defineConfig } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";

export default defineConfig({
  plugins: [vueJsx()],
});
  1. 在App.tsx中编写一个简单函数式组件
export default () => <div>Hello World</div>

使用 defineComponent 定义组件

使用defineComponent创建组件的好处在于:

  • 在定义 Vue 组件时会提供类型提示的帮助函数
  • 使用defineComponent在插件@vitejs/plugin-vue-jsx则会提供HMR支持
import { defineComponent } from "vue"
export default defineComponent({
  setup() {
    // 需要在setup函数当中return一个渲染函数
    return () => <div>使用defineComponent来定义组件</div>
  }
})

语法概述

文本插值

JSX/TSX 中使用 {} 单括号进行包裹

export default defineComponent({
  setup() {
    const userName = ref("Vue3-TSX")
    return () => (
      <>
        {/* 文本插值:在JSX中使用单括号 */}
        <h3>{userName.value}</h3>
      </>
    )
  }
})

条件渲染

可以使用 if/else 和 三目运算符来进行条件渲染

export default defineComponent({
  setup() {
    const randomNumber = Math.floor(Math.random() * 10)
    return () => (
      <>
        {/* 条件渲染 */}
        <h3>随机数{randomNumber}:{randomNumber > 6 ? '大于6' : '小于等于6'}</h3>
      </>
    )
  }
})

列表渲染

列表渲染则是使用 array.map 方法

export default defineComponent({
  setup() {
    interface ListData {
      id: string
      name: string
      age: number
    }
    const listData = ref<ListData[]>([{
      id: '1',
      name: '小红',
      age: 20
    }, {
      id: '2',
      name: '小王',
      age: 22
    }, {
      id: '3',
      name: '小华',
      age: 25
    }])
    return () => (
      <>
        {/* 列表渲染 */}
        {listData.value.map(item => {
          return (
            <div style="border: 1px solid #eeeeee; margin-bottom: 5px;">
              <p>ID:{item.id}</p>
              <p>用户名:{item.name}</p>
              <p>年龄:{item.age}</p>
            </div>
          )
        })}
      </>
    )
  }
})

标签属性绑定

当我们给标签绑定属性,使用 {} 即可不需要给标签名添加 :

export default defineComponent({
  setup() {
    const githubLink = 'https://github.com/flingyp'
    return () => (
      <>
        {/* 标签属性绑定 */}
        <a href={githubLink} target="__blank">GitHub</a>
      </>
    )
  }
})

事件绑定

绑定事件 onMethodName={}

export default defineComponent({
  setup() {
    const submitContent = (info: string) => {
      console.log("提交的信息:", info);
    }
    return () => (
      <>
        {/* 事件绑定 */}
        <button class={['custom-button']} onClick={() => submitContent('添加')}>时间绑定</button>
      </>
    )
  }
})

v-model 数据双向绑定

第一种写法默认值 modelValue

当我们需要这样使用组件:<CustomInput v-model={inputContent.value} /> 则需要使用这种方法

export default defineComponent({
    name: 'CustomInput',
    props: {
        modelValue: { // 默认Prop
            type: String,
            default: '默认内容'
        }
    },
    emits: ['update:modelValue'], // 默认Emit
    setup(props, { emit }) {
        const updateInputValue = (e: any) => {
            emit('update:modelValue', e.target.value)
        }
        return () => <input placeholder="请输入内容" value={props.modelValue} onInput={updateInputValue}></input>
    }
})

第二种写法自定义名称

而当我们需要使用这种方式使用组件:<CustomInput v-model:inputContent={inputContent.value} />

export default defineComponent({
  name: 'CustomInput',
  props: {
    inputContent: { // 自定义Props
      type: String,
      default: '默认内容'
    }
  },
  // 对应的触发事件
  emits: ['update:inputContent'],
  setup(props, { emit }) {
    const updateInputValue = (e: any) => {
      emit('update:inputContent', e.target.value)
    }
    return () => <input placeholder="请输入内容" value={props.inputContent} onInput={updateInputValue}></input>
  }
})

插槽

默认插槽

export default defineComponent({
    name: "CustomSlot",
    setup(props, { slots }) {
        return () => (
            <>
                {/* 第一种写法 */}
                <button>{slots.default?.() || '默认内容'}</button> 
                {/* 第一种写法 */}
                <button>{renderSlot(slots, 'default')}</button>
            </>
        )
    }
})

具名插槽

但我们使用具名插槽,我们的父组件可以由两种写法。

父组件的定义如下:

第一种

export default defineComponent({
  name: 'App',
  setup() {
    return () => (
      <>
        <CustomSlot v-slots={{
            default: () => '默认按钮',
            prefix: () => '前缀',
            suffix: () => '后缀'
          }}>
        </CustomSlot>
      </>
    )
  }
})

第二种

export default defineComponent({
  name: 'App',
  setup() {
    return () => (
      <>
        <CustomSlot>
          {{
            default: () => '默认按钮',
            prefix: () => '前缀',
            suffix: () => '后缀'
          }}
        </CustomSlot>
      </>
    )
  }
})

子组件定义也有两种方式:

export default defineComponent({
    name: "CustomSlot",
    setup(props, { slots }) {
        return () => (
            <>
                <button>{slots.prefix && slots.prefix()}-{slots.default && slots.default()}-{slots.suffix && slots.suffix()}</button>
            </>
        )
    }
})

第二种写法使用 renderSlot(slot, 'slotName')

export default defineComponent({
    name: "CustomSlot",
    setup(props, { slots }) {
        return () => (
            <button>{renderSlot(slots, 'prefix')}{renderSlot(slots, 'default')}{renderSlot(slots, 'suffix')}</button>
        )
    }
})

作用域插槽

作用域插槽就是有时候可能需要通过子组件向父组件传递数据,则是我们就需要使用到我们的作用域插槽。其实也就是在具名插槽的基本上添加一个Prop的参数。具体使用方式如下:

// 第一种方式
export default defineComponent({
  name: "CustomSlot",
  setup(props, { slots }) {
    return () => (
      <>
        <button>{slots.prefix && slots.prefix({ name: '默认前缀' })} - {slots.default && slots.default()} - {slots.suffix && slots.suffix({ name: '默认前缀' })}</button>
      </>
    )
  }
})
// 第二种方式
export default defineComponent({
  name: "CustomSlot",
  setup(props, { slots }) {
    return () => (
      <>
      	<button>{renderSlot(slots, 'prefix', { name: '默认前缀' })}-{renderSlot(slots, 'default')}-{renderSlot(slots, 'suffix', { name: '默认后缀' })}</button>
      </>
    )
  }
})

父组件

export default defineComponent({
  name: 'App',
  setup() {
    return () => (
      	<CustomSlot 
            v-slots={{
                default: () => '默认按钮',
                prefix: (props: { name: string }) => <lablel>{props.name}</lablel>,
                suffix: (props: { name: string }) => <lablel>{props.name}</lablel>,
            }}>
      	</CustomSlot>
    )
  }
})

简单总结

从SFC单文件组件的写法再到JSX/TSX的写法需要一段时间的适应。使用 JSX/TSX 写法则更加倾向于编写原生JS,在操作DOM和渲染会变得更加灵活,使用门槛相对较高。

而SFC单文件组件使用起来则相对简单但可能才操作DOM和渲染方面没有其灵活性。