在 Vue 中使用 JSX/TSX 语法有利有弊,我们在这里不详细讨论,是否使用 JSX/TSX 取决于你的项目需求和个人偏好。本文主要介绍如何使用它,因为与 SFC(单文件组件)相比有一些不同,相信你看完后能够快速上手。
Vite 开始
首先要安装插件@vitejs/plugin-vue-jsx
:
shell
npm install -D @vitejs/plugin-vue-jsx
然后在vite.config.ts
文件中进行配置:
ts
import { defineConfig } from 'vite'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [vueJsx()],
})
接下来就可以开始编写代码了,推荐使用.tsx
后缀来创建组件,例如App.tsx
:
jsx
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
return () => <div>Hello Vue!</div>
},
})
这里的defineComponent
只是起到类型推导的作用,你可以省略它。当然,你也可以在.vue
后缀的组件中的<script>
标签中进行编写,但是这种写法仍然需要在vite.config.ts
中配置插件@vitejs/plugin-vue
。当引入.tsx
后缀的组件时,可以省略后缀名,例如:
ts
import { createApp } from 'vue'
import App from './App'
createApp(App).mount('#app')
CSS Modules
由于不是 SFC 组件,因此我们使用 CSS Modules 的方式来编写作用域样式。在使用 CSS Modules 时,我们只需要将文件名后缀改为.module.css
,Vite 已经内置了对这类文件的处理。例子如下:
css
.app {
color: red;
}
引入:
jsx
import { defineComponent } from 'vue'
import styles from './app.module.css'
export default defineComponent({
setup() {
return () => <div class={styles.app}>Hello Vue!</div>
},
})
插值
在 SFC 中使用双花括号{{}}
来进行插值,而在 JSX 中使用单花括号{}
,而且属性外层也不再需要引号。
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
。获取节点也会有所变化,需要进行修改:
jsx
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
,而是使用三元运算符或 &&
运算符进行替代。
jsx
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const show = ref(true)
return () => (show.value ? <div>是</div> : <div>否</div>)
},
})
如果不需要不同条件返回值,可以使用 &&
运算符:
jsx
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const show = ref(true)
return () => show.value && <div>是</div>
},
})
列表循环(v-for)
列表循环同样不能使用,而应该用map
替代。
jsx
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,就像下面这样:
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
:
jsx
<input type="text" v-model_trim="{current.value}" />
在 Vue 3 中,可以通过指定一个参数来将其作为prop
传递给组件,例如v-model:title
,同样这个参数也可以使用下划线表示。
jsx
<input type="text" v-model_title="{current.value}" />
另外,也可以使用一个数组表示传入的值和名称。
jsx
<input type="text" v-model={[current.value, 'title']} />
事件绑定
事件绑定使用on[事件名]
的格式,如input
事件使用onInput
。
jsx
<input
type="text"
onInput={() => {
console.log('input')
}}
/>
自定义事件value-input
:
jsx
<Input
onValue-input={(v) => {
console.log('input', v)
}}
/>
事件修饰符同样不能用.
,可以改为下划线或者驼峰式。
jsx
<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
:
jsx
import { defineComponent } from 'vue'
export default defineComponent({
props: {
title: String,
},
setup(props) {
return () => <h1>{props.title}</h1>
},
})
插槽
插槽的定义不再使用<slot>
标签,而是使用setup
函数中的第二个参数slots
。例如,定义默认插槽可以如下所示:
jsx
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Content',
setup(props, { slots }) {
return () => <div>{slots.default?.()}</div>
},
})
在父组件中使用时,可以像这样调用:
jsx
<Content>
<div>内容</div>
</Content>
定义具名插槽的方式也相同,例如命名为title
的具名插槽可以这样写:
jsx
<h2>{slots.title?.()}</h2>
使用具名插槽时,需要在父组件传入一个对象,其中 key 为插槽的名称,例如:
jsx
<Content>
{{
title: () => <h2>标题</h2>,
default: () => <div>内容</div>,
}}
</Content>
然而这种写法可能存在一些问题,花括号的外面不能添加其他内容,如果是空格还会报错。因此,推荐使用下面这种v-slots
的写法:
jsx
<Content
v-slots={{
title: () => <h2>标题</h2>,
}}
>
<div>内容</div>
</Content>