为什么要使用 JSX
前阵子在 Vue3
项目中封装一个基础组件的时候用到了 JSX
语法,使用下来的感受就是 —— 某些场景下,JSX
的灵活性对我们编写代码还是能够带来一定的帮助的。
举两个常见的例子:
递归调用组件时
假设我们现在有如下数据,需要渲染其中的 name
字段:
const data = [
{
name: 'name1',
children: [{ name: 'name1-1' }]
},
{ name: 'name2' }
]
普通模板写法
如果使用普通模板写法,为了递归我们可能不得不编写两个组件:
父组件 parent.vue
:
// parent.vue 父组件
<template>
<div>我是父组件</div>
<Children v-for="item in data" :subData="item" :key="item.name"></Children>
</template>
子组件 children.vue
递归调用自身:
// children.vue 子组件
<template>
<span>{{ subData.name }}</span>
// 递归调用
<template v-if="subData.children">
<Children v-for="item in subData.children" :subData="item" :key="item.name"></Children>
</template>
</template>
JSX 写法:
而使用 JSX
则可以灵活地使用一个文件实现递归的逻辑:
// name.jsx
const renderChildren = (data: any) => {
return data.map((item: any) => {
if (item.children) {
return renderChildren(item.children)
} else {
return <span>{ item.name }</span>
}
})
}
const render = () => (
<>
<div>我是父组件</div>
{
data.map((item: any) => {
return (
<>
<span>{ item.name }</span>
{ item.children && renderChildren(item.children) }
</>
)
})
}
</>
)
动态生成标签名称
这是一个来自 vue 官网中的例子,如果你需要根据传入的 level
动态生成 <h1></h1>
到 <h6></h6>
之间的标签。
普通模板写法
你可能会这样写:
<template>
<h1 v-if="level === 1"></h1>
<h2 v-else-if="level === 2"></h2>
<h3 v-else-if="level === 3"></h3>
<h4 v-else-if="level === 4"></h4>
<h5 v-else-if="level === 5"></h5>
<h6 v-else-if="level === 6"></h6>
</template>
JSX 写法
但是如果学会了 JSX
的写法,就可以像这样:
const render = () => {
const level = props.level
const Tag = `h${level}`
return (
<Tag></Tag>
)
}
是不是瞬间简洁了许多!省下来的时间又可以用来愉快的摸鱼啦。
那么接下来我们就来一起看看,如何在 Vue
中使用 JSX
吧!
开启 JSX 特性
vue-cli 搭建
如果是使用 vue-cli
搭建的项目,默认就是支持 JSX
语法的,直接使用就可以。
webpack
如果不是 vue-cli
搭建的 webpack
项目,需要按照如下步骤开启:
下载 babel
插件
npm install @vue/babel-plugin-jsx -D
添加配置
在 babel
的配置文件中添加:
{
"plugins": ["@vue/babel-plugin-jsx"]
}
根据版本的不同,
babel
配置文件可能是.babelrc
或者babel.config.js
,注意区分。
vite
使用 vite
的项目,同样需要先安装插件:
npm install @vitejs/plugin-vue-jsx -D
然后在 vite.config.js
文件中添加以下配置:
// vite.config.js
import vueJsx from '@vitejs/plugin-vue-jsx'
export default {
plugins: [
vueJsx({
// options are passed on to @vue/babel-plugin-jsx
}),
],
}
更多的配置请参考 babel-plugin-jsx。
JSX 语法
使用 JS 表达式
在 JSX
语法中,可以通过一对大括号 {}
来使用 JS 表达式:
const name = 'zhangsan'
// 通过一对大括号 {} 来包裹 JS 表达式内容
const list1 = <div>{name}</div>
// 同样可以通过大括号 {} 来给标签传递动态属性
const id = 1
const list2 = <div id={id}>{name}</div>
或许你还看到过双大括号 {{}}
这种令人迷惑的写法,其实它表示 绑定的是个 JS 对象:
const name = 'zhangsan'
// 双大括号 {{}} 表示的是绑定的是个 JS 对象
// 可以拆分成 {} 和 { width: '100px' } 来理解
const list1 = <div style={{width:'100px'}}>{name}</div>
Fragment
Vue3 新增了新特性 Fragment
,使得我们在模板语法中能够返回多个根节点:
<template>
<div>Fragment</div>
<span>yes</span>
</template>
Vue 的编译器在编译时,会把这种包含多个根节点的模板被表示为一个片段(Fragment
)。
但是在 JSX
中,一组元素必须被包裹在一个闭合标签中返回;因此下面这种写法是不允许的:
// 错误写法 ❌
const render = () => (
<div>Fragment</div>
<span>yes</span>
)
正确做法是用一对闭合标签包裹:
// 正确写法 ✅
const render = () => (
<div>
<div>Fragment</div>
<span>yes</span>
</div>
)
那如果我们不想引入额外的标签该怎么办呢?可以用 <></>
来包裹我们想要返回的内容,如下:
const render = () => (
<>
<div>Fragment</div>
<span>yes</span>
</>
)
乍一看,你是不是觉得 <></>
和我们在使用模板写法时的 <template></template>
作用很相似?
但是实际上,JSX 中被 <></>
标签包裹的内容,会被当做 Fragment
来处理;并且针对 Fragment
Vue 在编译和渲染时会有特定的优化策略。
而对于 <template></template>
,Vue 只会将其作为一个普通的元素渲染;所以要注意别搞混咯。
Vue2 JSX 传递属性
在 Vue2 的时代,使用 JSX
时传递属性还是比较麻烦的。
因为 Vue2 中将属性又细分成了 组件属性、HTML Attribute
以及 DOM Property
等等,不同的属性写法也大相径庭,如下:
const render = () => {
return (
<div
// 传递一个 HTML Attribute,属性名称是 id 属性值是 'foo'
id="foo"
// 传递 DOM Property 需要使用前缀 `domProps` 来表示,这里表示传递给 innerHTML 这个 DOM 属性的值为 ‘bar’
domPropsInnerHTML="bar"
// 绑定原生事件需要以 `on` 或者 `nativeOn` 为前缀,相当于 @click.native
onClick={this.clickHandler}
nativeOnClick={this.nativeClickHandler}
// 绑定自定义事件需要用 `props` + 事件名 的方式
propsOnCustomEvent={this.customEventHandler}
// class(类名)、style(样式)、key、slot 和 ref 这些特殊属性写法
class={{ foo: true, bar: false }}
style={{ color: 'red', fontSize: '14px' }}
slot="slot"
key="key"
ref="ref"
// 如果是循环生成的 ref(相当于 v-for),那么需要添加 refInFor 这个标识
// 用来告诉 Vue 将 ref 生成一个数组,否则只能获取到最后一个
refInFor>
</div>
)
}
而在 Vue3 的
JSX
中传递各种属性的方式已经简化了许多,下面会拆开细讲,各位小伙伴们请接着往下看~
Vue3 JSX 传递属性
DOM Property
传递 DOM Property
时去掉了 domProps
前缀,可以直接书写:
const render = () => {
return (
<div innerHTML="bar"></div>
)
}
HTML Attribute
Vue3 JSX
传递 HTML Attribute
与 Vue2 JSX
相同,直接书写就行:
const render = () => {
return (
<div id="foo" type="email"></div>
)
}
如果需要动态绑定:
const placeholderText = 'email';
const render = () => {
return (
<input
type="email"
placeholder={placeholderText}
/>
)
};
class 与 style
Vue3 JSX
传递类名与样式的方法,与 Vue2 JSX
相同:
const render = () => {
return (
<div
class={{ foo: true, bar: false }}
style={{ color: 'red', fontSize: '14px' }}
>
</div>
)
}
ref
定义好 ref
后用 JS 表达式绑定就行:
const divRef = ref()
const render = () => {
return (
<div ref={divRef}></div>
)
}
绑定事件
在 Vue 模板写法中,绑定事件时我们使用 v-on
或者 @
符号:
<template>
<div @click="handleClick">事件绑定</div>
</template>
而在 Vue3 的 JSX
中,会把以 on
开头,并紧跟着大写字母的属性当作事件监听器来解析;
上面的模板写法换成 JSX
就是:
const render = (
<div onClick={handleClick}>事件绑定</div>
)
注意:这里一定要以
on
开头,并且紧跟着大写字母。错误写法 ❌:onclick、click;
正确写法 ✅:onClick、onClickChange、onClick-change (虽然但是,不会真的有人这么写吧?😹)
如果你不喜欢这种写法,还可以通过打开 babel
的 transformOn
配置,然后通过属性 on
绑定一个对象,一次性传递多个事件:
babel
配置:
// babel 配置
{
"plugins": [
[
"@vue/babel-plugin-jsx",
{
"transformOn": true
}
]
]
}
JSX
写法:
// 通过属性 on 绑定对象 批量传递多个事件
const render = () => (
<div on={{ click: handleClick, input: handleInput }}> 事件绑定 </div>
)
事件修饰符
对于 .passive
、.capture
和 .once
事件修饰符,可以使用驼峰写法将他们拼接在事件名称后面。
比如 onClick
+ Once
,就代表监听 click
事件触发,且只触发一次:
const render = () => (
<input
onClickCapture={() => {}}
onKeyupOnce={() => {}}
onMouseoverOnceCapture={() => {}}
/>
)
而像其余的 .self
、.prevent
等事件和按键修饰符,则需要使用 withModifiers
函数。
withModifiers 函数
withModifiers
函数接收两个参数:
- 第一个是我们的回调函数;
- 第二个参数是修饰符组成的数组。
import { withModifiers } from 'vue'
const count = ref(0)
const render = () => (
<input
onClick={
withModifiers(
// 第一个参数是回调函数
() => count.value++,
// 第二个参数是修饰符组成的数组
['self', 'prevent']
)
}
/>
)
上面的写法就相当于我们在 Vue 模板中这样写:
<template>
<input @click.stop.prevent="() => count++" />
</template>
v-for
在 JSX
中是没有 v-for
这个自定义指令的,我们需要用 map
方法来替代:
const render = () => (
<ul>
{
items.value.map(({ id, text }) => {
return (
<li key={id}>{text}</li>
)
})
}
</ul>
)
上面的写法,其实就相当于我们在模板中这样写:
<ul>
<li v-for="{ id, text } in items" :key="id">
{{ text }}
</li>
</ul>
v-if
同样,在 JSX
中也是没有 v-if
这个指令的~
但是细想一下,其实 v-if
的功能就是做判断嘛,比如我们的模板长这样:
<div>
<div v-if="ok">yes</div>
<span v-else>no</span>
</div>
那么换成用 JSX
就可以使用 三元表达式 或者 &&
连接符 来实现这个功能,我们可以这样写:
// 使用三元表达式
const render = () => (
<div>
{ ok.value ? <div>yes</div> : <span>no</span> }
</div>
)
// 使用 && 连接符
const render = () => (
<div>
{ ok.value && <div>yes</div> }
{ !ok.value && <span>no</span> }
</div>
)
v-show
可以直接使用v-show
指令,也可以写成 vShow
这种形式:
const show = ref(false)
// v-show
const render = () => (
<div v-show={show}></div>
)
// 或者 vShow
const render = () => (
<div vShow={show}></div>
)
v-model
正常情况下与我们在模板中使用 v-model
无异:
const value = ref('')
const render = () => (
<input v-model={value} />
)
const value = ref('')
// 将默认的 arg 从 modelValue 修改为 childrenProp
const render = () => (
<input v-model:childrenProp={value} />
)
修饰符
但如果你需要在 JSX
中使用 v-model
的内置修饰符,如.lazy
、.trim
、.number
,那么你需要传递一个数组:
const render = () => (
<input v-model={[value, ['trim']]} />
)
如果你想同时修改默认 arg
,并且使用修饰符;那么传递的数组的第二个参数需要定义为你设置的 arg
,且是个字符串:
const render = () => (
<input v-model={[value, 'childrenProp', ['trim']]} />
)
上面的写法相当于模板中:
<input v-model:childrenProp.trim="value"></input>
自定义指令
JSX
中自定义指令的使用方法和 v-model
十分相似,只需要把 v-model
替换成你对应的自定义指令就可以啦:
const App = {
directives: { custom: customDirective },
setup() {
const value = ref()
return () => <children v-custom:childrenProp={value} />;
},
};
修饰符
带修饰符的自定义指令,写法如下:
const App = {
directives: { custom: customDirective },
setup() {
const value = ref()
return () => (
<children v-custom={[value, 'childrenProp', ['a', 'b']]} />;
)
},
};
插槽
在 Vue3 的 jsx
中,使用插槽同样分为两步走:
预留插槽
首先,在接收插槽的组件中,给插槽留个“座位”;我们可以从 setup
函数的第二个参数解构出 slots
,拿到外部传入的所有插槽:
// 自定义组件 customComp.jsx
export default {
name: 'CustomComp',
props: ['message'],
// 外部传入的插槽信息都在 slots 中
setup(props, { slots }) {
return () => (
<>
// 传入的 default 默认插槽会被展示这里,如果没有传入默认插槽,则展示文本 foo
<h1>{slots.default ? slots.default() : 'foo'}</h1>
// 传入的具名插槽 bar 会被展示在这里
<h2>{slots.bar?.()}</h2>
// 作用域插槽 footer,外部可以通过作用域拿到 text 的值
<h2>{slots.footer?.({ text: props.message })}</h2>
</>
)
}
}
传入插槽
在 Vue3 的 jsx
中传入插槽时,需要使用 v-slots
代替 v-slot
:
// 定义好我们需要的插槽
const slots = {
// 这部分内容会传到具名插槽 bar 中
bar: () => <span>B</span>,
// 这部分内容会传到作用域插槽 footer 中
footer: ({ text }) => <span>{text}</span>
};
const render = () => (
// 使用 v-slots 将定义好的插槽 slots 传入自定义组件 CustomComp
<CustomComp v-slots={slots}>
// 这部分内容,会传入组件 CustomComp 的默认插槽中
<div>A</div>
</CustomComp>
);
或者还可以直接用一个对象同时定义好默认插槽和具名插槽:
export default {
setup() {
// 将默认插槽和具名插槽用对象的形式定义好:
const slots = {
default: () => <div>A</div>,
bar: () => <span>B</span>,
// 作用域插槽,能够拿到对应的信息 text
footer: ({ text }) => <span>{text}</span>
};
// 直接使用 v-slots 将整个插槽对象传递给自定义组件 CustomComp
return () => <CustomComp v-slots={slots} />
}
}
另外,如果 babel
的配置项 enableObjectSlots
不为 false
时,传入多个插槽还可以写成对象的形式,对象的 key
为插槽名称, value
为一个函数,函数的返回就是插槽的默认占位:
修改 babel
配置:
{
"plugins": [
[
"@vue/babel-plugin-jsx",
{
"enableObjectSlots": true
}
]
]
}
jsx
具体写法:
const render = () => (
<>
<CustomComp>
{{
// 给自定义组件 CustomComp 传入一个默认插槽,内容是 <div>A</div>
default: () => <div>A</div>,
// 给自定义组件 CustomComp 传入一个具名插槽 bar,内容是 <span>B</span>
bar: () => <span>B</span>,
// 给自定义组件 CustomComp 传入一个具名插槽 footer,内容是 <span>B</span>
footer: ({ text }) => <span>{ text }</span>,
}}
</CustomComp>
// 相当于: {{ default: () => 'foo' }},给自定义组件 CustomComp 传入了一个默认插槽
<CustomComp>{() => 'foo'}</CustomComp>
</>
)
以函数的形式传递插槽,子组件就可以懒调用这些插槽了。
在 Vue3 中使用 JSX
在函数式组件中使用
在函数式组件中使用 JSX
十分简单,直接返回相关内容就行:
const App = () => <div></div>;
在 SFC 中使用
我们知道 Vue3 中的 setup
函数如果 返回的是个函数,那么这个函数的返回值会被作为模板内容渲染,并且会忽略 template
的内容。
因此在普通的单文件组件中,我们可以直接在 setup
函数中返回我们的 JSX
内容:
import { defineComponent } from 'vue';
const App = defineComponent({
setup() {
const count = ref(0);
return () => (
<div>{count.value}</div>
);
},
});
以上就是 Vue3 中使用 JSX
的全部内容啦!如果文章对你有帮助的话,还希望各位小伙伴不要吝啬你的赞🙏~
另外,如果文章有纰漏 or 你有任何疑惑,都欢迎在评论区讨论😆。