Webpack迁到Vite第二节:JSX 到 Render 函数
本篇定位 :项目中仅有的两处 JSX 写法在 Vite 下无法保留(无兼容 Vite 7 的 Vue2 JSX 插件),本篇分析
$createElement替代方案与优缺点评估------是迁移中改动面最小的专题。
思维导图
bash
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 的代码:
jsx
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 参数中:
src/views/pc/user/loginMixin.js--- 密码过期提醒弹窗src/views/pc/user/selectAccount.vue--- 同上
javascript
// 原代码
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 三件套:
xml
@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 是独立的语法扩展,需要独立插件。
版本冲突:
less
@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 冲突 |
| 3 | rollup-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 的语法糖。
javascript
// JSX
<label style="display: flex;">Hello</label>
// 等价于
this.$createElement('label', { style: { display: 'flex' } }, 'Hello')
参数说明
$createElement(tagName, options, children)
| 参数 | 类型 | 说明 | 示例 |
|---|---|---|---|
tagName |
String |
HTML 标签名 | 'label', 'input', 'span' |
options |
Object |
属性/样式/事件等 | { style: {...}, attrs: {...} } |
children |
`String | Array` | 子节点 |
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):
jsx
<label style="display: flex;align-items: center;">
<input type="checkbox" id="check30DayNotHint" />
<span style="margin-left: 5px">30天内不再提示</span>
</label>
改造后($createElement):
javascript
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天内不再提示'),
])
逐行解释:
-
const h = this.$createElement:把$createElement赋值给短变量h,这是 Vue 社区的常见惯例(h代表hyperscript,即创建虚拟 DOM 的函数) -
外层
h('label', { style: ... }, [...]):- 第一个参数
'label':创建<label>元素 - 第二个参数
{ style: { display: 'flex', 'align-items': 'center' } }:style对象对应 JSX 的style="..."属性'align-items'加引号是因为包含连字符,不能直接写align-items(JS 变量名不能有连字符)
- 第三个参数
[...]:子元素数组
- 第一个参数
-
子元素
h('input', { attrs: { type: 'checkbox', id: 'check30DayNotHint' } }):type、id是 HTML 标签属性,必须放在attrs对象中- 这是 Vue 2 的规则:原生 HTML 属性走
attrs,Vue 特有的属性(如style、class、on)走顶层
-
子元素
h('span', { style: { 'margin-left': '5px' } }, '30天内不再提示'):- 第三个参数是纯文本,直接写字符串即可
验证效果 :构建不再报 JSX 语法错误;密码过期提醒弹窗正常弹出,复选框与"30天内不再提示"文案正常渲染,勾选状态正确写入 passwordGoOnIsChecked(弹窗确认/取消时按该状态决定是否调用 goOn30Days())。
1.4 优缺点分析
优势
- 零依赖 :不需要任何额外插件,
$createElement是 Vue 2 内置 API - 永远兼容:不依赖第三方插件的版本更新
- 改动量极小:项目只有 2 处 JSX,每处改动不到 10 行
劣势
- 可读性略差 :JSX 看起来像 HTML,更直观;
$createElement是纯 JS 函数调用 - 但在本项目场景下可接受:只有 2 处,且结构简单(一个 label + 一个 input + 一个 span)
为什么不用 vite-plugin-vue2-jsx 的 fork 版本
社区可能有 Vite 7 兼容的 fork 版本,但:
- 不是官方维护,长期可靠性不确定
- 增加依赖风险
- 2 处 JSX 不值得引入一个依赖