多多读书
1450 字
7 分钟
全面分析Vue 3中使用JSX/TSX
2023-10-19

在 Vue 中使用 JSX/TSX 语法有利有弊,我们在这里不详细讨论,是否使用 JSX/TSX 取决于你的项目需求和个人偏好。本文主要介绍如何使用它,因为与 SFC(单文件组件)相比有一些不同,相信你看完后能够快速上手。

Vite 开始#

首先要安装插件@vitejs/plugin-vue-jsx

npm install -D @vitejs/plugin-vue-jsx

然后在vite.config.ts文件中进行配置:

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

export default defineConfig({
  plugins: [vueJsx()],
})

接下来就可以开始编写代码了,推荐使用.tsx后缀来创建组件,例如App.tsx

import { defineComponent } from 'vue'

export default defineComponent({
  setup() {
    return () => <div>Hello Vue!</div>
  },
})

这里的defineComponent只是起到类型推导的作用,你可以省略它。当然,你也可以在.vue后缀的组件中的<script>标签中进行编写,但是这种写法仍然需要在vite.config.ts中配置插件@vitejs/plugin-vue。当引入.tsx后缀的组件时,可以省略后缀名,例如:

import { createApp } from 'vue'
import App from './App'

createApp(App).mount('#app')

CSS Modules#

由于不是 SFC 组件,因此我们使用 CSS Modules 的方式来编写作用域样式。在使用 CSS Modules 时,我们只需要将文件名后缀改为.module.css,Vite 已经内置了对这类文件的处理。例子如下:

.app {
  color: red;
}

引入:

import { defineComponent } from 'vue'
import styles from './app.module.css'

export default defineComponent({
  setup() {
    return () => <div class={styles.app}>Hello Vue!</div>
  },
})

插值#

在 SFC 中使用双花括号{{}}来进行插值,而在 JSX 中使用单花括号{},而且属性外层也不再需要引号。

import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const text = ref('Hello Vue!')
    return () => <div style={{ width: '100px' }}>{text.value}</div>
  },
})

在这里,与 SFC 中不同的是,使用ref变量时需要添加.value。获取节点也会有所变化,需要进行修改:

import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const text = ref('Hello Vue!')
    const el = ref()

    return () => (
      <div ref={el} style={{ width: '100px' }}>
        {text.value}
      </div>
    )
  },
})

需要注意的是,在这里使用ref来获取节点时不需要再加上.value,这与插值又是不一样的。

条件渲染(v-if)#

JSX 中不能使用 v-ifv-else,而是使用三元运算符或 && 运算符进行替代。

import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const show = ref(true)

    return () => (show.value ? <div>是</div> : <div>否</div>)
  },
})

如果不需要不同条件返回值,可以使用 && 运算符:

import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const show = ref(true)

    return () => show.value && <div>是</div>
  },
})

列表循环(v-for)#

列表循环同样不能使用,而应该用map替代。

import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const list = ref([1, 2, 3, 4])

    return () => (
      <>
        {list.value.map((item) => (
          <div>{item}</div>
        ))}
      </>
    )
  },
})

需要注意的是,JSX 必须要有一个根节点来包含所有的节点。当然,你可以使用空标签<>作为占位符,实际上不会生成该标签。为什么要使用map函数呢?因为它可以改变数组,并生成相应的 JSX。当然,你也可以使用forEach方法并定义一个函数来返回 JSX,就像下面这样:

import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const list = ref([1, 2, 3, 4])

    const renderList = () => {
      const jsxElements: JSX.Element[] = []
      list.value.forEach((item) => {
        jsxElements.push(<div>{item}</div>)
      })
      return jsxElements
    }

    return () => <>{renderList()}</>
  },
})

相比,这种写法变得复杂了。不过,使用函数返回 JSX 的方式仍然非常有用,特别是在编写递归组件时。你只需要定义一个函数就可以了,比 SFC 更加直观。

v-model#

v-model在 SFC 中的写法与正常情况下差不多,可以直接使用v-model来绑定数据。不过修饰符和指定参数的写法略有不同,在 JSX 中不能使用.:。例如,修饰符.trim可以改成_trim

<input type="text" v-model_trim="{current.value}" />

在 Vue 3 中,可以通过指定一个参数来将其作为prop传递给组件,例如v-model:title,同样这个参数也可以使用下划线表示。

<input type="text" v-model_title="{current.value}" />

另外,也可以使用一个数组表示传入的值和名称。

<input type="text" v-model={[current.value, 'title']} />

事件绑定#

事件绑定使用on[事件名]的格式,如input事件使用onInput

<input
  type="text"
  onInput={() => {
    console.log('input')
  }}
/>

自定义事件value-input

<Input
  onValue-input={(v) => {
    console.log('input', v)
  }}
/>

事件修饰符同样不能用.,可以改为下划线或者驼峰式。

<button onClick_stop={() => { console.log('click') }}>点击</button>
<button onClickStop={() => { console.log('click') }}>点击</button>

props#

setup函数包含两个参数,第一个是props,用于接收父组件传递的数据。第二个参数是ctx对象,它包含了emitslotsattrs属性。JSX 中不能使用 SFC 的definePropsdefineEmits等函数,而是直接使用setup函数的参数。

下面是一个例子,展示了如何使用props

import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    title: String,
  },
  setup(props) {
    return () => <h1>{props.title}</h1>
  },
})

插槽#

插槽的定义不再使用<slot>标签,而是使用setup函数中的第二个参数slots。例如,定义默认插槽可以如下所示:

import { defineComponent } from 'vue'

export default defineComponent({
  name: 'Content',
  setup(props, { slots }) {
    return () => <div>{slots.default?.()}</div>
  },
})

在父组件中使用时,可以像这样调用:

<Content>
  <div>内容</div>
</Content>

定义具名插槽的方式也相同,例如命名为title的具名插槽可以这样写:

<h2>{slots.title?.()}</h2>

使用具名插槽时,需要在父组件传入一个对象,其中 key 为插槽的名称,例如:

<Content>
  {{
    title: () => <h2>标题</h2>,
    default: () => <div>内容</div>,
  }}
</Content>

然而这种写法可能存在一些问题,花括号的外面不能添加其他内容,如果是空格还会报错。因此,推荐使用下面这种v-slots的写法:

<Content
  v-slots={{
    title: () => <h2>标题</h2>,
  }}
>
  <div>内容</div>
</Content>
全面分析Vue 3中使用JSX/TSX
https://fuwari.vercel.app/posts/20231019/
作者
我也困了
发布于
2023-10-19
许可协议
CC BY-NC-SA 4.0