在 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-if 和 v-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对象,它包含了emit、slots和attrs属性。JSX 中不能使用 SFC 的defineProps、defineEmits等函数,而是直接使用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>