Vue 3 的 Composition API 极大地提升了逻辑复用的能力,而当我们将其与 JSX 或 TSX 结合时,能够进一步释放 JavaScript 原生的渲染能力。虽然单文件组件(SFC)在结构分离上表现优异,但在处理高度动态化的渲染逻辑、复杂的条件分支或高阶组件时,模板语法的指令往往会显得冗长且难以维护。通过引入 JSX,我们可以直接使用 JavaScript 的控制流来组织 UI 结构,配合 TypeScript 的强类型约束,让组件的开发体验更加接近 React,同时保留 Vue 响应式系统的全部优势。

为什么在 Vue 3 中选择 JSX/TSX
Vue 3 的模板编译器虽然做了大量静态优化,比如静态提升和补丁标记,但在某些复杂场景下,模板语法受限于其自身的表达式解析能力。例如,当需要根据复杂的映射关系动态生成不同类型的组件时,模板中的动态组件写法可能会变得难以阅读。JSX 本质上是 JavaScript 语法的扩展,它允许我们直接在函数中编写 UI 逻辑,这意味着你可以随意使用变量、循环、三元运算符等原生 JS 特性来组织渲染输出,无需受限于特定的指令语法。
引入 TSX 的另一个核心优势在于类型推导的完整性。在 SFC 中,虽然可以通过 defineProps 的泛型定义属性类型,但在模板内部使用这些属性时,IDE 的类型提示往往不够精确,甚至存在盲区。而在 TSX 中,整个组件就是一个返回虚拟 DOM 的函数,所有的 Props、Emits 和 Slots 都有明确的 TypeScript 接口定义。这种端到端的类型安全不仅能有效减少运行时错误,还能极大提升重构时的信心,特别是在开发底层基础组件库时,TSX 的类型体验是模板难以比拟的。
当然,选择 JSX 并不意味着要完全抛弃 SFC。两者在 Vue 3 中可以无缝共存。通常情况下,业务层面的页面开发依然推荐使用 SFC 以保持清晰的视图层分离;而在开发无状态的高阶组件、复杂的渲染函数组件或需要大量动态逻辑的内部工具组件时,切换到 JSX/TSX 是更明智的选择。理解两者的边界,才能在工程化实践中发挥最大的效益。
Vite 环境下的完整配置指南
要在 Vite 项目中启用 JSX 或 TSX 支持,首先需要安装官方提供的插件 @vitejs/plugin-vue-jsx。这个插件底层依赖于 Babel,能够自动处理 Vue 3 所需的 JSX 转换规则,包括自动导入 createVNode 等运行时辅助函数。通过包管理器执行安装命令后,我们需要在 Vite 的配置文件中显式引入并应用该插件。
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vue(),
vueJsx({
// 可以在此处传入 Babel JSX 插件配置
optimize: true,
})
]
})
配置完成后,Vite 就能正确解析以 .jsx 或 .tsx 结尾的文件了。但为了让 TypeScript 编译器在类型检查时不报错,还需要在 tsconfig.json 中进行相应配置。需要在 compilerOptions 的 jsx 属性中设置为 preserve,这样 TS 编译器会将 JSX 语法原样保留,交由 Vite 插件去处理。同时,确保 include 字段包含了对应的文件后缀。
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"jsx": "preserve",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
除了基础配置,有时我们还需要调整 Babel 的解析行为,比如处理自定义的 JSX 片段。@vitejs/plugin-vue-jsx 提供了配置选项,允许传入 jsxOptions 参数。通过这个参数,可以精细控制 optimize 选项,决定是否开启 Vue 3 特有的 JSX 优化,例如将事件监听器缓存化。在大多数默认场景下,插件的零配置即可满足需求,但了解其底层可定制性有助于解决特殊兼容性问题。
JSX/TSX 组件开发核心实践与对比
在 TSX 中编写 Vue 组件,最直观的变化是组件不再由 template、script、style 三部分组成,而是直接导出一个函数。通常我们会使用 defineComponent 来包裹函数,以获得完善的类型推导。在函数内部,通过 setup 方法返回一个渲染函数。与 SFC 不同,TSX 中不需要使用 ref 解包,因为在 JSX 表达式中,Vue 会自动处理响应式变量的读取。
import { defineComponent, ref } from 'vue'
export default defineComponent({
props: {
title: {
type: String,
required: true
}
},
setup(props) {
const count = ref(0)
const increment = () => count.value++
return () => (
<div class="my-component">
<h2>{props.title}</h2>
<p>Count: {count.value}</p>
<button onClick={increment}>Increment</button>
</div>
)
}
})
插槽的处理是 JSX 开发中的一个重点。在 SFC 中,我们习惯使用 <slot> 组件和 v-slot 指令。而在 TSX 中,插槽被映射为组件 Props 上的一个特殊对象。默认插槽通过 slots.default 调用,具名插槽则通过对应的属性名调用,例如 slots.header。如果需要传递插槽属性,只需在调用时传入参数即可。这种基于函数调用的插槽机制,使得插槽的类型校验变得异常清晰。
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, { slots }) {
return () => (
<div class="layout">
<header>
{/* 调用具名插槽 */}
{slots.header ? slots.header() : <h2>Default Header</h2>}
</header>
<main>
{/* 调用默认插槽 */}
{slots.default ? slots.default() : <p>No content</p>}
</main>
</div>
)
}
})
自定义指令和 ref 的绑定在 JSX 中也有所不同。指令无法直接像 v-model 那样使用,需要改写为 v-model={variable} 或者使用 v-models 语法(针对多参数)。对于自定义指令,需要使用带有 v 前缀的字符串对象写法,例如 v-focus={条件}。而模板引用 ref 则直接通过 ref 属性绑定到一个响应式变量上。掌握这些语法映射规则,是从 SFC 平滑过渡到 TSX 开发的关键所在。