Vue3 用了这么久还没体验过 JSX或TSX?来封装个业务弹窗玩玩

2024-09-09
21088 分钟
...

平时开发 Vue 项目时,用的最多的就是模板组件,一些模板组件实现起来稍微麻烦的需求,也会用到渲染函数,对于 JSX/TSX 的应用相比之下就少了很多,不知大家是否也是如此呢?不过没关系,今天我们来详细的梳理下如何在 Vue 项目中使用 JSX。建议跟着文章敲起来,为自己的技术道路添砖加瓦😄。

阅读本文您将收获:

  1. 了解 Vue3 如何使用 JSX/TSX
  2. 封装个业务弹窗,体验下 JSX/TSX 在实际开发中的应用。
  3. 等等...

JSX 是一种类似 XMLJavaScript 扩展,本身没有特定的语义。 JavaScript 引擎和浏览器没有直接实现它,也没有纳入 ECMAScript 规范,核心作用就是被各种编译器(如 Babel)解析,转换成标准的 JavaScript 代码。

简单来说,JSX 被设计为一个中间格式,用于在 JavaScript 中方便地编写类似 HTML 的结构,最终通过工具(如 Babel)转换成标准的 JavaScript 代码,以在浏览器或 JavaScript 引擎中运行。

JSX 代码示例:

const vnode = <div>hello</div>

启用 JSX

脚手架

目前 create-vue 和 Vue CLI 脚手架工具创建的模板工程,都提供了预置的 JSX 支持选项。

手动配置

若要手动配置,可使用 @vue/babel-plugin-jsx 插件。

安装依赖

终端输入以下命令,回车执行:

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

配置 babel

babel 配置文件,如 babel.config.js 中添加如下配置:

{
  "plugins": ["@vue/babel-plugin-jsx"]
}

Vite 配置

若项目使用 Vite 构建,需配置 ViteJSX 插件 - @vitejs/plugin-vue-jsx,否则无法识别 JSX 语法,会报解析错误。

安装插件

终端输入以下命令,回车执行:

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

配置插件

Vite 配置文件,vite.config.ts 添加如下配置:

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


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

TSX

TS 配置

若项目使用 TypeScriptVue 也为 TSX 提供了类型推断。

tsconfig.json compilerOptions 选项中添加如下配置:

{
    "jsx": "preserve"
}

这样 TypeScript 会保留 JSX 语法,以便 VueJSX 转换器 将其转换为 Vue 渲染函数。

类型

  • Vue3.4 之前的版本,Vue 会自动注册全局 JSX 命名空间,处理 JSX 相关的类型定义,无需手动处理。

  • Vue3.4 版本之后,为减少全局污染,不再自动注册全局 JSX 命名空间,需要手动配置 TypeScript 来支持 JSX

    tsconfig.json compilerOptions 选项中添加如下配置:

    {
        "jsx": "preserve",
        "jsxImportSource": "vue",
    }
    

OK、JSX 的环境准备工作到此结束。

常见用法

列举 Vue 相关语法的 JSX 版本。

指令

v-if

JSX 中使用三元表达式实现 v-if 执行效果:

<div>{ok.value ? <div>yes</div> : <span>no</span>}</div>

v-show

可以在 JSX 结构中直接使用:

<input v-show={this.visible} />;

v-for

JSX 中使用 Array.map 实现 v-for 指令遍历效果:

<ul>
  {items.value.map(({ id, text }) => {
    return <li key={id}>{text}</li>
  })}
</ul>

v-model

Vue 模板组件中的 v-model 指令有以下几种用法:

  • 直接使用v-model="value"
  • 带参v-model:title="title"
  • 带修饰符v-model.trim="input"
  • 带参 + 修饰符v-model:title.trim="title"

针对以上情况,我们分别来看下 JSX 如何实现:

  1. 直接使用
<input v-model={val} />
  1. 带参
<input v-model:title={title} />
  1. 带修饰符
<input v-model_trim={input} />

<input v-model={[input, ['trim']]} />
  1. 带参 + 修饰符
<input v-model:title_trim={title} />

<input v-model={[title, 'title', ['trim']]} />

事件 v-on

JSX 中以 on + 大写字母开头的属性名作为事件监听器。如 onClick 相当于模板中的 @click。

<button
  onClick={(event) => {
    
  }}
>
  Click Me
</button>

修饰符

JSX 处理方式划分,可把事件修饰符分为两类:

  • .passive.capture.once

    这三个修饰符可以用驼峰命名法连接到事件名称后,作为事件监听器。

    <input
      onClickCapture={() => {}}
      onKeyupOnce={() => {}}
      onMouseoverOnceCapture={() => {}}
    />
    
  • 除以上三个修饰符之外的,如 .self

    使用 withmodifiers 帮助函数处理。

    <div onClick={withModifiers(() => {}, ['self'])} />
    

插槽

渲染插槽

我们可以通过 setup 函数上下文获取 slots 对象,其包含要渲染的所有插槽。假设现有组件 A,需要渲染 footer 插槽,可以如下方式渲染:

export default {
  props: ['message'],
  setup(props, { slots }) {
    return <div>{slots.footer({ text: props.message })}</div>
  }
}

传递插槽

向组件传递插槽时,有以下几种方式:

  1. v-slots

注意:是 v-slots,不是 v-slot

将插槽对象传递给 v-slots 指令。

export default {
  setup(props, { slots }) {
    const slots = {
      footer: () => <span>B</span>,
    };
    return () => (
      <A v-slots={slots}></A>
    );
  }
}
  1. 对象插槽

将插槽对象作为组件的子内容传递。

export default {
  setup(props, { slots }) {
    const slots = {
      footer: () => <span>B</span>,
    };
    return () => (
      <A>{slots}</A>
    );
  }
}

接下来我们用 JSX 封装一个基于 ElementPlus Dialog 的业务弹窗组件 - zm-dialog,整合弹窗标题,主体内容和底部操作按钮等主要内容。

1. 创建组件

创建 zm-dialog.tsx 文件,新增以下初始内容:

import { ElDialog } from 'element-plus'

export const ZmDialog = defineComponent({
  name: 'ZmDialog',
  setup(props) {
    return {}
  },
  render() {
    return <ElDialog></ElDialog>
  }
})

最基础的组件框架,没有任何内容。

2. 属性&事件

思考下组件的属性设计,考虑到大家对应 ElementPlus 组件已经十分熟悉,从降低上手难度的角度来说,我们应尽量沿用 ElDialog 组件的属性设计,并在此基础上,添加支撑业务组件的扩展属性。

属性包括但不限于以下:

  • model-value/v-model:控制弹窗显隐。
  • title:弹窗标题。
  • width:弹窗宽度。
  • fullscreen:弹窗是否全屏。
  • top:弹窗距离顶部的 css 距离(margin-top)。
  • modal:是否需要遮罩。
  • append-to-body:是否插入至 body 元素。
  • ...
  • cancelButtonText: 取消按钮展示文本,默认“取消”。
  • confirmButtonText:确认按钮展示文本,默认“确定”。
  • ...

事件除了沿用 ElDialog 的事件外,额外新增 cancelconfirm 事件,点击对应按钮时触发。

import { dialogProps, ElDialog } from 'element-plus'

const zmDialogProps = {
  ...dialogProps,
  
  cancelButtonText: {
    type: String,
    default: '取消'
  },
  
  confirmButtonText: {
    type: String,
    default: '确定'
  }
}

const zmDialogEmits = ['confirm', 'cancel']

3. 结构设计

弹窗结构包括以下三部分:

  • 顶部标题栏

    包括标题关闭按钮

  • 主体内容

    包括弹窗需要展示的主体内容,根据业务需求定义。

  • 底部操作栏

    包括取消确认操作按钮。

本部分涉及到插槽的定义:

  • 标题插槽 header:对应 ElDialog 的 header 插槽。
  • 主体内容插槽 default:对应 ElDialog 的 default 插槽。
  • 底部栏插槽 footer:对应 ElDialog 的 footer 插槽,默认展示”取消“、”确认“按钮。
 * 弹窗组件
 * 支持 Dialog Drawer
 */
import { dialogProps, ElButton, ElDialog } from 'element-plus'
import type { ExtractPropTypes } from 'vue'

const zmDialogProps = {
  ...dialogProps,
  
  cancelButtonText: {
    type: String,
    default: '取消'
  },
  
  confirmButtonText: {
    type: String,
    default: '确定'
  }
}

const zmDialogEmits = ['confirm', 'cancel']

export const ZmDialog = defineComponent({
  name: 'ZmDialog',
  props: zmDialogProps,
  emits: zmDialogEmits,
  setup(props, { emit }) {
    const visible = computed({
      get() {
        return props.modelValue
      },
      set(val) {
        emit('update:modelValue', val)
      }
    })

    const elDialogProps = computed(() =>
      Object.keys(dialogProps).reduce((pre: any, cur: any) => {
        pre[cur] = props[cur]
        return pre
      }, {})
    )

    const onCancel = () => {
      visible.value = false
      emit('cancel')
    }

    const onConfirm = () => {
      emit('confirm')
    }

    return {
      visible,
      elDialogProps,
      onCancel,
      onConfirm
    }
  },
  render() {
    const { onCancel, onConfirm, cancelButtonText, confirmButtonText } = this
    const footer = this.$slots.footer
      ? this.$slots.footer
      : () => (
          <div>
            <ElButton onClick={onCancel}>{cancelButtonText}</ElButton>
            <ElButton type="primary" onClick={onConfirm}>
              {confirmButtonText}
            </ElButton>
          </div>
        )

    const slots = {
      header: this.$slots.title,
      default: this.$slots.default,
      footer
    }
    return (
      <ElDialog
        {...this.elDialogProps}
        v-model={this.visible}
        v-slots={slots}
      ></ElDialog>
    )
  }
})

4. 组件应用

zm-dialog 暂且封装到这,还有很多可以完善的地方,不过作为示例够用了,现在来测试一下。

新增测试页面,编写如下代码:

<template>
  <zm-dialog
    v-model="visible"
    title="测试弹窗"
    @opened="handleOpened"
    @cancel="handleCancel"
    @confirm="handleConfirm"
  >
    hello dialog
  </zm-dialog>
</template>

<script setup lang="ts">
const visible = defineModel({ type: Boolean, default: false })

const handleOpened = () => {
  console.log('Opened')
}

const handleCancel = () => {
  console.log('canceled')
}

const handleConfirm = () => {
  console.log('confirmed')
}
</script>

本文重点介绍了 Vue3 使用 JSX/TSX 的开发方式,比较详细的讲述了 Vue 常用语法对应的 JSX 写法,并动手实践,一起封装业务弹窗组件,旨在帮助同学们加深对于 JSXVue 项目中的应用理解。希望对您有所帮助!相关代码已上传至 GitHub

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
vue2025-10-22
shallowRef 与 ref 的区别、场景与坑位
结论先行: 会 深度 地把你放进去的对象转成响应式(递归代理),因此"对象内部的改动"也会触发视图更新。 只在 顶层 做依赖追踪: 更换 才触发更新 ;若你只是"在原对象上改属性",不会触发,除非手动 。 1\. 快速对比 维度 追踪粒度 深度(递归) 仅顶层 对象内部属性变更 会触发更新 不会 触发(除非 或重新赋值) 性能 有递归 & 依赖跟踪开销 更轻量,适合大对象/第三方实例 适用对象 普通标量、普通对象、数组 第三方实例(图表...
学习面试
vue2025-10-20
Vue 3 中的 setup执行时机与异步逻辑详解
Vue 3 中的 执行时机与异步逻辑详解 本文详细介绍 Vue 3 组合式 API 的核心函数 :它在组件生命周期中的执行时机、异步行为( 的影响)、常见用法和最佳实践。 一、 的执行时机 是 Vue 3 组件实例创建后、渲染前执行的函数。 它相当于 Vue 2 中的 和 的合体。 执行流程: 也就是说: 所有响应式数据( 、 、 )都在这里定义; 所有生命周期钩子( 、 等)都在这里注册; 模板渲染会等待 执行完成(或异步返回的 Pr...
学习面试
vue2025-10-08
vue中ref与reactive
Vue 3 的 与 :区别、实现,以及为什么更推荐 1\. 一句话结论 :适合 原始类型 或 独立、细粒度 的状态单元;类型推断明确、解构安全、追踪粒度更小, 默认首选 。 :适合管理 对象/集合 这类结构化状态;但 解构会丢失响应性 , 默认 非深度 ,需要正确的使用姿势配合。 2\. 用法与语义差异 2.1 基本示例 2.2 模板中的解包(unwrapping) 在 模板 里, 会 自动解包 ,可直接写 ,不必 。 在响应式对象中...
学习面试
vue2025-09-23
Vue3 中computed属性依赖总结
Vue3 属性依赖总结 1. 依赖收集机制 的 getter 内部 读取 到哪些响应式变量 ( / ),就会把这些变量收集为依赖。 当这些依赖发生变化时, 会重新计算。 2. 如果只返回常量 始终是 。 修改 或 不会触发 重新计算,因为 getter 内没有访问它们。 3. 如果访问了响应式变量但仍返回常量 初始化时会打印一次。 当 或 改变时,getter 会重新执行(再次打印),因为它们被收集为依赖。 但 依旧始终是 。 4. 总...
学习面试
vue2025-09-17
template标签为什么不可以使用v-show
前言 写 Vue 时,最容易踩的小坑之一,就是给 套一个 。编辑器不报错,页面却“消失”了。很多人以为这是 bug,其实它只是 Vue 的底层设计在默默提醒: 不是真节点,它只是个“草稿纸”。 一、为什么 拒绝 v show 的本质是切换元素的 CSS: 。浏览器要找到真实 DOM 节点,才能给它加样式。而 在运行时会被完全剥离,不会留下任何标签,Vue 找不到节点,也就无处下刀。 举个反面教材: 打开控制台,你会看到 还是生成了,但...
学习面试
vue2025-09-17
Vue 3中watchEffect自动追踪详解
Vue 3 自动追踪详解 一、是什么 是 Vue 3 Composition API 提供的一个响应式副作用(side effect)函数。 它会 立即执行一次回调函数 ,并且在回调函数中访问到的所有响应式数据(ref、reactive、computed 等)都会被 自动追踪 。 当这些依赖的响应式数据发生变化时,回调会重新执行。 简单理解: 能“自动发现”它用到的响应式变量,不需要手动指定依赖。 二、基本用法 特点: 第一次立即执行...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录