likes
comments
collection
share

万字长文详解 Vue JSX,带你全面拥抱 JSX 特性!

作者站长头像
站长
· 阅读数 14

为什么要使用 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"></Children>
</template>

子组件 children.vue 递归调用自身:

// children.vue 子组件
<template>
  <span>{{ subData.name }}</span>
    // 递归调用
  <template v-if="subData.children">
    <Children v-for="item in subData" :subData="item"></Children>
  </template>
</template>

JSX 写法:

而使用 JSX 则可以灵活地使用一个文件实现递归的逻辑:

// name.jsx
const renderChildren = (data) => (
   return data.map(item => (
       item.children && renderChildren(item.children)
       return (
           <span>{ item.name }</span>
       )
   ))
)
const render = () => (
  <>
      <div>我是父组件</div>
    { renderChildren(data) }
  </>
)

动态生成标签名称

这是一个来自 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 中使用 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样式)、keyslot  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 (虽然但是,不会真的有人这么写吧?😹)

如果你不喜欢这种写法,还可以通过打开 babeltransformOn 配置,然后通过属性 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 函数接收两个参数:

  1. 第一个是我们的回调函数;
  2. 第二个参数是修饰符组成的数组。
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 你有任何疑惑,都欢迎在评论区讨论😆。