相信写过 JSX 的人再去写 Vue 中的 <template>
,很多人会觉得束缚太多了,语法不够灵活。我也深有此感,所以琢磨了在 <template>
中使用 JSX 的一些方式,特此记录与大家分享。
两种写法的对比
先来看下常规的 <template>
的写法:
html
<script setup lang="tsx">
import { reactive } from 'vue'
const state = reactive({
status: null as '成功' | '失败' | '未知' | null,
message: null as null | string,
})
MockAPI().then((data) => { /** 调用接口,填充 state 数据 */ })
</script>
<template>
<div role="demo">
<div v-if="!state.status">状态获取中...</div>
<div
v-else
:class="{
'text-success': state.status === '成功',
'text-error': state.status === '失败',
'text-warning': state.status === '未知',
}"
>
<div role="content" v-if="!state.message">
<span>状态:{{ state.status }}</span>
<a-icon v-if="['成功', '失败'].includes(state.status)" :type="state.status === '成功' ? 'check' : 'close'" />
</div>
<!-- 失败且有失败原因时,展示失败原因 -->
<a-tooltip v-else :title="state.message">
<div role="content">
<span>状态:{{ state.status }}</span>
<a-icon v-if="['成功', '失败'].includes(state.status)" :type="state.status === '成功' ? 'check' : 'close'" />
</div>
</a-tooltip>
</div>
</div>
</template>
这是常见的在 <template>
中写所有的元素,可以对比下使用 <template>
+ JSX 的方式:
html
<script setup lang="tsx">
// ...
const Node = {
render() {
if (!state.status) return <div v-if="!state.status">状态获取中...</div>
const clsMap = {
成功: 'text-success',
失败: 'text-error',
未知: 'text-warning',
}
const showIcon = ['成功', '失败'].includes(state.status)
const content = (
<div role="content">
<span>状态:{state.status}</span>
{showIcon && <a-icon type={state.status === '成功' ? 'check' : 'close'} />}
</div>
)
return (
<div class={clsMap[state.status]}>
{/* 使用 JSX,content 可以重用 */}
{!state.message ? content : <a-tooltip title={state.message}>{content}</a-tooltip>}
</div>
)
},
}
</script>
<template>
<div role="demo">
<component :is="Node" />
</div>
</template>
通过对比,可以看到使用 <template>
+ JSX 的方式有如下优点:
- 类似 hooks,将高度相关的函数、变量与元素耦合在一起,代码更简洁更好维护
- 使用 JSX 实现更灵活的逻辑判断
- 为数据命名有意义的变量名,如上面的
showIcon
和clsMap
,增加代码可读性 - 借助 JS 编程减少重复代码,如上面提取的
div[role="content"]
元素
上面这种实现方式的原理很简单, <componet>
是 Vue 内置的组件, is
属性接收一个组件的定义, const Node = { render() {} }
其实就是一个组件内定义的选项式组件,使用 const Node = () => {}
函数式组件也是可以的。
自定义 组件实现
除此之外,自定义一个全局的 <jsx>
组件也是可以实现的:
tsx
// Jsx.jsx
const Jsx = defineComponent({
name: 'Jsx',
props: {
node: {
// 这里的 Object 实际上特指 VNode 类型
type: [Object, String, Number],
},
// Vue2 中的组件必须用实际的标签包裹
tag: {
type: String,
default: 'div',
},
},
setup(props) {
// 注意:props.node 允许为空(null、undefined),这时还是会渲染出一个内容为空的节点
return () => h(props.tag, [props.node as any])
},
})
// vue 项目入口文件 main.ts
import Vue from 'vue'
Vue.component('Jsx', Jsx)
使用方式如下:
html
<script setup lang="tsx">
function renderNode() {
return <div>JSX 内容...</div>
}
</script>
<template>
<jsx :node="renderNode()" />
</template>
不适用的场景
当然了,<template>
+ JSX 也并不适用于所有的场景。比如下面这几个场景
1. 逻辑简单
逻辑简单时,使用 <template>
即可,没必要为了用 JSX 而用 JSX。
2. 逻辑过于复杂
当逻辑过于复杂时,建议提取为一个单独的组件。
3. 想要更好的 Vue 编译优化、热更新
由于 <template>
是静态的,而 JSX 是动态的,所以 Vue 对前者的编译优化会更好些。此外经过我的实践, JSX 在很多时候热更新会状态丢失,稍稍影响到了开发体验。