Webpack迁到Vite第二节:JSX 到 Render 函数

1 阅读3分钟

Webpack迁到Vite第二节:JSX 到 Render 函数

本篇定位:项目中仅有的两处 JSX 写法在 Vite 下无法保留(无兼容 Vite 7 的 Vue2 JSX 插件),本篇分析 $createElement 替代方案与优缺点评估——是迁移中改动面最小的专题。

思维导图

JSX 到 Render 函数
├── 1.1 背景:什么是 JSX
│   ├── JSX 语法示例
│   ├── JSX 需要编译工具
│   ├── JSX 和 createElement 是标准吗?(都不是)
│   └── 项目中 JSX 的使用场景
├── 1.2 为什么不能保留 JSX
│   ├── @vitejs/plugin-vue2-jsx 与 Vite 7 不兼容
│   ├── 尝试过的方案
│   │   ├── @rollup/plugin-babel
│   │   ├── rollup-plugin-esbuild
│   │   └── 自定义 Vite 插件
│   └── 为什么都不行
├── 1.3 $createElement 替代方案
│   ├── 什么是 $createElement
│   ├── JSX 和 $createElement 的对应关系
│   │   ├── 标签 → 第一个参数
│   │   ├── 属性 → 第二个参数
│   │   └── 子元素 → 第三个参数
│   ├── 实战:loginMixin.js 改造
│   └── 实战:selectAccount.vue 改造
└── 1.4 优缺点分析
    ├── 优势:零依赖、永远兼容
    └── 劣势:可读性略差(仅 2 处,可接受)

1.1 背景:什么是 JSX

JSX 是 JavaScript 的语法扩展,让你可以在 JS 中写类似 HTML 的代码:

const element = (
  <label style="display: flex;">
    <input type="checkbox" id="check" />
    <span>30天内不再提示</span>
  </label>
)

这段代码不能直接在浏览器中运行——必须先被编译成普通的 JavaScript 函数调用。在 Vue 2 中,JSX 会被编译为 createElement 调用。

JSX 和 createElement 是标准吗?

都不是标准。JSX 是 Facebook 为 React 设计的语法扩展,不是 ECMAScript 或 W3C 规范。Babel 的 @babel/plugin-transform-react-jsx 是事实上的编译器,但编译目标可以配置为任意函数名。

JSX 语法      → 无标准,Facebook 发明
JSX 编译器    → Babel 插件是事实标准,esbuild/SWC/TypeScript 也都支持
编译产物      → 取决于配置,React.createElement / Vue h / Preact h 都可以

Vue 的 createElement($createElement) 是 Vue 框架自己定义的 API,用于创建 VNode(虚拟 DOM 节点)。VNode 本身也没有跨框架的标准——React 的 VNode 结构是 { type, props, key, ... },Vue 2 的是 { tag, data, children, elm, ... }。

整条链路,只有终点是标准:

JSX(无标准)
  → Babel 编译(约定)
  → createElement / h(框架定义)
  → VNode(框架定义)
  → diff + patch(框架实现)
  → DOM API(W3C/WHATWG 标准 ✅)

标准化委员会(TC39、W3C)只管浏览器能直接执行的东西——import/export(ESM)、document.createElement(DOM)、fetch(WHATWG)。框架内部的 VNode 怎么设计、JSX 怎么编译,不在标准化的范围内。

项目中 JSX 的使用场景

项目中只有 2 处 使用了 JSX,都在 $confirm 弹窗的 content 参数中:

  1. src/views/pc/user/loginMixin.js — 密码过期提醒弹窗
  2. src/views/pc/user/selectAccount.vue — 同上
// 原代码
this.$confirm({
  title: '为确保您安全使用大家签签署服务,建议您定期更改登录密码',
  content: (
    <label style="display: flex;align-items: center;">
      <input type="checkbox" id="check30DayNotHint" />
      <span style="margin-left: 5px">30天内不再提示</span>
    </label>
  ),
  // ...
})

1.2 为什么不能保留 JSX

核心问题:没有兼容的插件

@vitejs/plugin-vue2 不处理 JSX。它的职责仅限于 SFC 三件套:

@vitejs/plugin-vue2
  ├── 编译 <template>  → 调用 vue/compiler-sfc
  ├── 处理 <script>    → 交给 Vite 处理(esbuild 默认不处理 .vue 脚本块,且无兼容 Vite 7 的 Vue2 JSX 插件)
  └── 处理 <style>     → 交给 Less/Sass 编译器

JSX 需要单独的 @vitejs/plugin-vue2-jsx 插件处理。这是有意为之的职责分离——Vue 插件只管 SFC,JSX 是独立的语法扩展,需要独立插件。

版本冲突:

@vitejs/plugin-vue2-jsx@1.1.1
  peerDependency: vite@^2.9.13 || ^3.0.0 || ^4.0.0 || ^5.0.0

它只支持 Vite 2~5,而项目使用的是 Vite 7。API 不兼容导致构建时 Rollup 无法解析 JSX 语法。

尝试过的方案

方案工具结果
1@vitejs/plugin-vue2-jsx --force❌ Dev 可用,Build 报错
2@rollup/plugin-babel + @babel/plugin-transform-react-jsx❌ babelHelpers 冲突
3rollup-plugin-esbuild❌ 虚拟模块不处理
4自定义 Vite 插件 enforce: 'pre'❌ 拦截不到 SFC 内部
5自定义 Vite 插件 enforce: 'post'❌ 同上

根本原因:Vite 7 的插件 API 发生了变化,Vue 2 JSX 插件没有跟进更新。Vue 2 已经进入维护期,插件作者不太可能为 Vite 7 更新。


1.3 $createElement 替代方案

什么是 $createElement

$createElement 是 Vue 2 实例上的方法,用于编程式创建 VNode(虚拟 DOM 节点)。JSX 本质上就是 createElement 的语法糖。

// JSX
<label style="display: flex;">Hello</label>

// 等价于
this.$createElement('label', { style: { display: 'flex' } }, 'Hello')

参数说明

$createElement(tagName, options, children)

参数类型说明示例
tagNameStringHTML 标签名'label', 'input', 'span'
optionsObject属性/样式/事件等{ style: {...}, attrs: {...} }
childrenString | Array子节点'文本' 或 [h('span', ...)]

JSX → $createElement 对照表

JSX$createElement
<div>h('div')
<div class="x">h('div', { class: 'x' })
<div style="color:red">h('div', { style: { color: 'red' } })
<input type="checkbox">h('input', { attrs: { type: 'checkbox' } })
<div>text</div>h('div', {}, 'text')
<div><span>A</span></div>h('div', {}, [h('span', {}, 'A')])

关键规则:

  • HTML 属性(type, id, placeholder 等)放在 attrs 对象里
  • CSS 样式(style="...")放在 style 对象里,属性名用驼峰命名
  • 子元素用数组 [] 包裹

实战改造

项目中共 2 处 JSX,分别位于 src/views/pc/user/loginMixin.js(密码过期提醒弹窗)和 src/views/pc/user/selectAccount.vue(同上)。两处改造方式相同,以下示例两者通用。

原代码(JSX):

<label style="display: flex;align-items: center;">
  <input type="checkbox" id="check30DayNotHint" />
  <span style="margin-left: 5px">30天内不再提示</span>
</label>

改造后($createElement):

const h = this.$createElement

h('label', { style: { display: 'flex', 'align-items': 'center' } }, [
  h('input', { attrs: { type: 'checkbox', id: 'check30DayNotHint' } }),
  h('span', { style: { 'margin-left': '5px' } }, '30天内不再提示'),
])

逐行解释:

  1. const h = this.$createElement:把 $createElement 赋值给短变量 h,这是 Vue 社区的常见惯例(h 代表 hyperscript,即创建虚拟 DOM 的函数)

  2. 外层 h('label', { style: ... }, [...]):

    • 第一个参数 'label':创建 <label> 元素
    • 第二个参数 { style: { display: 'flex', 'align-items': 'center' } }:
      • style 对象对应 JSX 的 style="..." 属性
      • 'align-items' 加引号是因为包含连字符,不能直接写 align-items(JS 变量名不能有连字符)
    • 第三个参数 [...]:子元素数组
  3. 子元素 h('input', { attrs: { type: 'checkbox', id: 'check30DayNotHint' } }):

    • type、id 是 HTML 标签属性,必须放在 attrs 对象中
    • 这是 Vue 2 的规则:原生 HTML 属性走 attrs,Vue 特有的属性(如 style、class、on)走顶层
  4. 子元素 h('span', { style: { 'margin-left': '5px' } }, '30天内不再提示'):

    • 第三个参数是纯文本,直接写字符串即可

验证效果:构建不再报 JSX 语法错误;密码过期提醒弹窗正常弹出,复选框与"30天内不再提示"文案正常渲染,勾选状态正确写入 passwordGoOnIsChecked(弹窗确认/取消时按该状态决定是否调用 goOn30Days())。


1.4 优缺点分析

优势

  1. 零依赖:不需要任何额外插件,$createElement 是 Vue 2 内置 API
  2. 永远兼容:不依赖第三方插件的版本更新
  3. 改动量极小:项目只有 2 处 JSX,每处改动不到 10 行

劣势

  1. 可读性略差:JSX 看起来像 HTML,更直观;$createElement 是纯 JS 函数调用
  2. 但在本项目场景下可接受:只有 2 处,且结构简单(一个 label + 一个 input + 一个 span)

为什么不用 vite-plugin-vue2-jsx 的 fork 版本

社区可能有 Vite 7 兼容的 fork 版本,但:

  • 不是官方维护,长期可靠性不确定
  • 增加依赖风险
  • 2 处 JSX 不值得引入一个依赖