创建新项目
在使用 npm create vue 命令创建新项目时,可以通过配置选项启用 JSX 支持。
已有项目(基于 Vite)
对于已有项目,若使用 Vite 作为构建工具,可以在 vite.config.js 文件中添加以下插件:
javascript
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [
// 其他插件...
vueJsx(),
],
});
同时,在 jsconfig.json 或 tsconfig.json 中增加如下配置项以支持 JSX:
json
{
"compilerOptions": {
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}
编写 JSX 组件
下面是一个简单的组件示例,展示如何在 Vue 中使用 JSX:
jsx
// App.jsx
import { defineComponent, ref } from 'vue';
const Comp = defineComponent({
props: ['onClick'],
setup(props, { slots }) {
return () => (
<button onClick={props.onClick}>{slots.default ?? 'a'}</button>
);
},
});
const App = defineComponent({
setup() {
const num = ref(0);
return () => <Comp onClick={() => num.value++}>{num.value}</Comp>;
},
});
export default App;
插槽的使用方式
插槽对象具有如下类型定义:
typescript
{
default?: () => VNode,
[otherSlotName]?: () => VNode
}
可以直接将插槽对象传递给子组件:
jsx
<Children>{slots}</Children>
也可以对插槽进行修改:
jsx
<Children>
{{
...slots,
default: slots.default ?? 'aaa',
}}
</Children>
如果直接传入一个 VNode 或者渲染函数,则被视为默认插槽:
jsx
<Children>{slots.default ?? 'aaa'}</Children>
使用说明
- Setup 函数返回值
setup 函数的返回值是一个渲染函数 () => JSX,而不是普通的 JSX。可以将其理解为 React 类组件中的 render 函数。 - Setup 函数签名
setup 函数的签名为 setup(props, { slots, emit, attrs, expose })。Vue 会根据提供的 props 对象构造组件的 props,其余参数会被自动归类到 attrs 中。 - 事件处理
形如 @click 的指令会被转化为 onClick,并且允许通过 emit 调用。attrs 和 slots 不可被解构赋值,因为它们是非响应式的,不能作为 watch 或 computed 的响应式数据源。emit 触发事件是没有返回值的,因此无法使用 await 获取事件处理的结果。 - Exposing 子组件 API
expose 等效于 defineExpose,用于暴露子组件的方法或属性。 - 样式和属性继承
传入的 onClick、class 等属性会被自动应用到子组件的最外层。如果需要阻止这种行为,需设置 inheritAttrs: false。 - Prop 类型定义
若希望某个属性(如 onClick)被识别为 props,则需要在 props 中显式声明,例如:
javascript
props: {
onClick: Function as PropType<() => void>,
}
在这种情况下,通常不需要设置 inheritAttrs: false。
TypeScript 支持
虽然 Vue + JSX 可以正常工作,但 Vue + TSX 的体验相对较差。在 TypeScript 中,传递 onClick 属性需要额外的类型声明:
typescript
import type { PropType } from 'vue';
defineComponent({
props: {
onClick: Function as PropType<() => void>,
},
setup(props, { slots }) {
// ...
},
});
尽管可以为 defineComponent 设置模板类型,但这仍然会导致 onClick 被自动应用到子组件最外层,并可能被视为 attrs。此外,插槽方面也存在一些问题,尤其是在复杂场景下。
总结与反思
这篇文章原本是介绍“如何在 Vue 中使用 JSX”,我对这种写法曾抱有很多期待。心理流程大致如下:
- 初体验:便捷的 ref
使用 ref 非常方便,无论是本体还是属性都可以直接传给 v-model。Hook 的限制较少,带来了一些爽快感。 - 遇到挑战:v-model 的局限性
在某些 UI 库(如 Element UI)中,v-model 的使用体验并不理想。例如,即使明确指定了 modelValue,组件仍可能自由切换状态,导致不受控的行为:
html
<el-tabs modelValue="0"> <el-tab-pane name="0"></el-tab-pane> <el-tab-pane name="1"></el-tab-pane> </el-tabs>
- 新思路:结合 ref 与 value-onChange 模式
Vue + JSX 提供了一种类似 React 的受控组件模式,能够将 ref 和 value-onChange 结合起来,避免繁琐的 :@# 写法,仿佛实现了 Vue 与 React 的“融合超进化”。 - 深入后的问题:代码变得复杂
然而,随着使用的深入,发现这种方式并没有想象中那么美好。虽然 ref 很方便,但在查找变量更新位置时缺乏清晰的状态管理机制(如 React 的 setState)。你需要查看变量的所有引用位置,无论是读取还是修改。 - 对比 React:Hook 与状态管理
在 React 中,虽然 Hook 也需要一定的学习成本,但配合 ESLint 插件可以有效避免常见错误。只要熟悉文档,也不容易犯错。而在 Vue 中,状态更新的逻辑更加分散,维护起来反而更困难。 - 性能与架构考量
就性能而言,现代浏览器已经足够强大,除非出现严重卡顿,否则无需过早优化。不过从架构角度看,Vue 项目中盘根错节的组件结构并不少见,其复杂度并不比 React 更低。 - 最终结论:谨慎选择
尽管 Vue + JSX 提供了另一种开发风格的选择,但从整体体验来看,它并未显著优于传统的 Vue 开发方式或 React 的 Hooks 模式。尤其在 TypeScript 支持、插槽处理等方面,仍存在不少痛点。因此,在实际项目中应根据团队技术栈和项目需求谨慎选择是否采用此方案。