vue3 jsx

142 阅读5分钟

使用jsx的前置条件

如果使用vite创建的项目

  1. 安装依赖:
npm install @vitejs/plugin-vue-jsx --save-dev
  1. vite.config.ts配置插件
// vite.config.js
import vueJsx from '@vitejs/plugin-vue-jsx'

export default defineConfig({
  plugins: [
    vueJsx({
      // options are passed on to @vue/babel-plugin-jsx
    }),
  ],
})

jsx语法

v-for

不能使用v-for指令,在jsx中map来代替

// 组件JsxComp.jsx
import { defineComponent } from "vue"
import styles from './jsxcomp.module.less'

export default defineComponent({
    setup(props, { emit, slots }) {
        const buttons = [{
          name: '收入',
          id: 1
        }, {
          name: '支出',
          id: 2
        }, {
          name: '结余',
          id: 3
        }]
        
        return () => (
            <div style="border: 1px solid #999;">
                <h2>按钮区</h2>
                {buttons.map((btn, index) => (
                  <button key={btn.id} class={[styles['default-btn'], btn.id === activeBtn.value && styles['active-btn']]} onClick={() => clickBtn(btn.id)}>{btn.name}</button>
                ))}
            </div>
        
        )
    }
})

v-slots

为节省空间,组件JsxComp.jsx中的其余代码不展示,只展示在setup中返回的与标题相关的部分模板

// 子组件JsxComp.jsx
<div>
  <h2>插槽区域:</h2>
  <div class={styles['default-slot-area']}>
    默认插槽区:
    <div>{ slots?.default?.() }</div>
  </div>

  <div style="margin-top: 10px; border-radius: 5px; padding: 5px; border: 1px solid #999;">
    具名插槽区main:
   <!-- 给slot传参,在模板语法中为作用域插槽 >
    <div>{ slots?.main?.({age: 1}) }</div>
  </div>
</div>

父组件为App.jsx

// 父组件App.jsx
// 方式一:
const slots = {
  main: ({age}) => (
    <>
      <div style="color: #1E90FF;">节点1,从子组件传过来的值age: {age}</div>
      <div style="color: #87CEEB;">节点2</div>

    </>
  )
}
<JsxComp v-slots={slots}>
  <div>
    <h1>默认插槽内容</h1>
    <div style="color: #fdc;">内容区</div>
  </div>
</JsxComp>

// 方式二:
const slots = {
  default: () => (
    <div>
      <h1>默认插槽内容</h1>
      <div style="color: #fdc;">内容区</div>
    </div>
  ),
  main: () => (
    <>
      <div style="color: #1E90FF;">节点1</div>
      <div style="color: #87CEEB;">节点2</div>

    </>
  )
}
<JsxComp>
  { slots }
</JsxComp>

// 方式三:
const slots = {
  default: () => (
    <div>
      <h1>默认插槽内容</h1>
      <div style="color: #fdc;">内容区</div>
    </div>
  ),
  main: () => (
    <>
      <div style="color: #1E90FF;">节点1</div>
      <div style="color: #87CEEB;">节点2</div>

    </>
  )
}
<JsxComp v-slots={slots}>

</JsxComp>

父组件为App.vue

<JsxComp>
  <template #default>
    <div>默认slot</div>
  </template>
  <template #main="{ age }">
    <div>主要内容1,从子组件传过来的数据:{{ age }}</div>
    <div>主要内容2</div>
  </template>
</JsxComp>

v-if

const isVisible = ref(false)
const changeVisible = () => {
  isVisible.value = !isVisible.value
}
// setup返回的渲染模板
<div>
  <h2>v-if演示</h2>
  <button onClick={changeVisible}>changeVisible</button>
  <div style="width: 100%; height: 200px; background: #D4F2E7;line-height: 200px;text-align: center;">
    {
      isVisible.value && <span>能显示隐藏的区域1</span>
    }
    {
      isVisible.value ? <span>能显示隐藏的区域2</span> : ''
    }
  </div>
</div>

v-model

实现父子组件的双向绑定

// 子组件JsxComp.jsx
import { defineComponent} from "vue"

export default defineComponent({
  props: ['modelValue', 'title'],
  emits: ['update:modelValue', 'update:title'],
  setup(props, { emit, slots }) {
    return () => (
      <>
        <h1>这是jsx组件</h1>

        <div>
          <h2>测试v-model</h2>
          <div>
            <span>不添加参数的model -- props.modelValue: { props.modelValue } </span>
            <input value={props.modelValue} onInput={e => emit('update:modelValue', e.target.value)}/>
          </div>
          <div>
            <span>添加title参数的model -- props.title: { props.title }</span>
            <input value={props.title} onInput={e => emit('update:title', e.target.value)}/>
          </div>
        </div>
      </>
    )
  }
})

// 父组件App.jsx
import { defineComponent, ref } from "vue";
import JsxComp from '../src/components/JsxComp'

export default defineComponent({
  setup() {

    const count = ref(0)
    const title = ref('placehold title')

    return () => (
      <>
        <div>双向绑定的count:{ count.value }</div>
        <div>双向绑定的title:{ title.value }</div>

        <JsxComp
          v-model={count.value}
          v-model:title={title.value}
        >
        </JsxComp>
      </>
    )
  }
})
<!-- 父组件App.vue >
<div>
  <span>双向绑定count的值:{{ count }}</span>
  <br/>
  <span>双向绑定title的值:{{ title }}</span>
</div>
<JsxComp
  v-model="count"
  v-model:title="title"
>
</JsxComp>
// 父组件App.vue
<script setup lang="ts">
import { ref } from 'vue'
import JsxComp from './components/JsxComp.jsx'

const count = ref(0)
const title = ref('title')

组件内部的双向绑定

let count = ref(0)
const addNum = () => {
  count.value++
}

<input type="number" v-model={count.value} />
<span>{count.value}</span>
<button onClick={addNum}>+</button>

绑定样式

// 直接定义style
<div style="border: 1px solid #999;"></div>

<div style={{color: 'red'}}>红颜色的字</div>


// 绑定定义的变量的样式
const innerStyle = {
  buttonStyle: {
    padding: '5px',
    border: '1px solid #999',
    'border-radius': '5px',
    'background-color': '#0000ff3d'
  }
}

<button style={innerStyle.buttonStyle}>blueColor</button>

// 带有变量的样式
const color = ref('#ADD8E6')
const colors = ['#ADD8E6', '#F0FFFF', '#2F4F4F', '#7FFFAA']
let preIndex = 0
const changeColor = () => {
  const randomIndex = Math.floor(Math.random() * colors.length)

  if (preIndex === randomIndex) {
    changeColor()
    return;
  }
  color.value = colors[randomIndex]
  preIndex = randomIndex
}
<button onClick={changeColor}>修改以下背景色</button>

<div style={{'background': color.value, transition: 'all 1s ease'}}></div>

绑定class

如果绑定的class只在本组件中使用,需要在文件名后添加.module,例如,有个文件名为test.css,如果只希望定义的class在组件内部使用,则文件名需为:test.module.css。编译后,模块内的class会在原有class基础上添加一些特殊字符,例如,在文件中定义了一个class名为blue_bg,经过编译后编译成了_blue-bg_10cyo_6

// jsxcomp.module.less
.default-slot-area {
  padding: 5px;
  border: 1px solid #999;
  border-radius: 5px;
}

.blue-bg {
  background-color: rgb(111, 111, 236);
}

.border {
  border: 1px solid #d30677;
}

@activeColor: #d30677;
.default-btn {
  margin-right: 5px;
  cursor: pointer;
  border: 1px solid #999;
  background-color: 	#4169E1;
  &:hover {
    background-color: @activeColor;
  }
}

.active-btn {
  background-color: @activeColor;
}


import styles from './jsxcomp.module.less'

// 绑定单个class
<div class={styles['default-slot-area']}></div>

// 绑定多个class
<div class={[styles['blue-bg'], styles.border]}>有多个class的div</div>

// class切换
<button class={[styles['default-btn'], btn.id === activeBtn.value ? styles['active-btn']: '']} }>btn</button>

<button class={[styles['default-btn'], btn.id === activeBtn.value && styles['active-btn']]} >name</button>

绑定事件

jsx组件内部绑定事件

// 不带参数
<button onClick={changeVisible}>changeVisible</button>

// 只带参数
<button onClick={() => clickBtn(btn.id)}>{btn.name}</button>

// 同时带事件e和参数
<button onClick={e => emitEvent(e, 'msg from child')}>向父组件抛事件</button>

子组件抛事件,父组件接收事件

// JsxComp.jsx
export default defineComponent({
    emits: ['custom-event'],
    setup(props, { emit, slots }) {
        // 使用defineEmits会报运行时警告,提示不能使用
        // const emits = defineEmits(['emit-event'])
        const emitEvent = (e, params) => {
          emit('custom-event', {
            e,
            params
          })
        }
        
        return () => <button onClick={e => emitEvent(e, 'msg from child')}>向父组件抛事件</button>

    }
})
// 父组件App.jsx
<JsxComp
  onCustomEvent={(params) => console.log(params)}
>
</JsxComp>
<!-- 父组件App.vue >
<JsxComp @custom-event="getMsg"/>

完整代码

// JsxComp.jsx
import { defineComponent, ref } from "vue"
import styles from './jsxcomp.module.less'

export default defineComponent({
  props: ['msg', 'modelValue', 'title', 'name', 'age'],
  emits: ['custom-event', 'update:modelValue', 'update:title'],
  setup(props, { emit, slots }) {
    const innerStyle = {
      buttonStyle: {
        padding: '5px',
        border: '1px solid #999',
        'border-radius': '5px',
        'background-color': '#0000ff3d'
      }
    }

    const buttons = [{
      name: '收入',
      id: 1
    }, {
      name: '支出',
      id: 2
    }, {
      name: '结余',
      id: 3
    }]

    const activeBtn = ref(buttons[0].id)
    const clickBtn = id => {
      activeBtn.value = id;
    }

    let count = ref(0)
    const addNum = () => {
      count.value++
    }

    // 使用defineEmits会报运行时警告,提示不能使用
    // const emits = defineEmits(['emit-event'])
    const emitEvent = (e, params) => {
      emit('custom-event', {
        e,
        params
      })
    }

    const color = ref('#ADD8E6')
    const colors = ['#ADD8E6', '#F0FFFF', '#2F4F4F', '#7FFFAA']
    let preIndex = 0
    const changeColor = () => {
      const randomIndex = Math.floor(Math.random() * colors.length)

      if (preIndex === randomIndex) {
        changeColor()
        return;
      }
      color.value = colors[randomIndex]
      preIndex = randomIndex
    }

    const isVisible = ref(false)
    const changeVisible = () => {
      isVisible.value = !isVisible.value
    }
    return () => (
      <>
        <h1>这是jsx组件</h1>
        <div>
          <span>通过obj传入的name: { props.name }</span>
          <br/>
          <span>通过obj传入的age: { props.age }</span>
        </div>
        <hr style="border-color: red;"/>
        <div>输入值props.msg: { props.msg } </div>
        <input type="number" v-model={count.value} />
        <span>{count.value}</span>
        <button onClick={addNum}>+</button>

        <button onClick={e => emitEvent(e, 'msg from child')}>向父组件抛事件</button>
        <div>
          <h2>插槽区域:</h2>
          <div class={styles['default-slot-area']}>
            默认插槽区:
            <div>{ slots?.default?.() }</div>
          </div>

          <div style="margin-top: 10px; border-radius: 5px; padding: 5px; border: 1px solid #999;">
            具名插槽区main:
            <div>{ slots?.main?.({age: 1}) }</div>
          </div>
        </div>

        <div class={[styles['blue-bg'], styles.border]}>有多个class的div</div>
        <button style={innerStyle.buttonStyle}>blueColor</button>

        <div style={{color: 'red'}}>红颜色的字</div>

        <div style="border: 1px solid #999;">
          <h2>按钮区</h2>
          {buttons.map((btn, index) => (
            // <button key={btn.id} class={[styles['default-btn'], btn.id === activeBtn.value ? styles['active-btn']: '']} onClick={() => clickBtn(btn.id)}>{btn.name}</button>

            <button key={btn.id} class={[styles['default-btn'], btn.id === activeBtn.value && styles['active-btn']]} onClick={() => clickBtn(btn.id)}>{btn.name}</button>
          ))}
        </div>

        <button onClick={changeColor}>修改以下背景色</button>
        <div style={{'background': color.value, transition: 'all 1s ease'}}>
          <h2>测试v-model</h2>
          <div>
            <span>不添加参数的model -- props.modelValue: { props.modelValue } </span>
            <input value={props.modelValue} onInput={e => emit('update:modelValue', e.target.value)}/>
          </div>
          <div>
            <span>添加title参数的model -- props.title: { props.title }</span>
            <input value={props.title} onInput={e => emit('update:title', e.target.value)}/>
          </div>
        </div>

        <div>
          <h2>v-if演示</h2>
          <button onClick={changeVisible}>changeVisible</button>
          <div style="width: 100%; height: 200px; background: #D4F2E7;line-height: 200px;text-align: center;">
            {
              isVisible.value && <span>能显示隐藏的区域1</span>
            }
            {
              isVisible.value ? <span>能显示隐藏的区域2</span> : ''
            }
          </div>
        </div>
      </>
    )
  }
})
// App.vue
<template>
  <div class="container">
    <div>从子组件获取到的msg: {{ msgFromChild }}</div>
    <div>
      <span>双向绑定count的值:{{ count }}</span>
      <br/>
      <span>双向绑定title的值:{{ title }}</span>
    </div>
    <JsxComp
      v-model="count"
      v-model:title="title"
      msg="first one"
      @custom-event="getMsg"
    >
      <template #default>
        <div>默认slot</div>
      </template>
      <template #main="{ age }">
        <div>主要内容1,从子组件传过来的数据:{{ age }}</div>
        <div>主要内容2</div>
      </template>
    </JsxComp>
  </div>

</template>

<script setup lang="ts">
import { ref } from 'vue'
import JsxComp from './components/JsxComp.jsx'
import CompC from './components/CompC.vue';


const count = ref(0)
const title = ref('title')
const changeCount = () => {
  count.value--
}

const msgFromChild = ref('')

const getMsg = obj => {
  console.log('obj:', obj)

  msgFromChild.value = obj.params
}



</script>


<style scoped lang='less'>
.container {
  width: 100%;
  height: 200px;
  background-color: v-bind(color);
}
</style>
// App.jsx
import { defineComponent, ref } from "vue";
import JsxComp from '../src/components/JsxComp'

export default defineComponent({
  setup() {
    const slots = {
      default: () => (
        <div>
          <h1>默认插槽内容</h1>
          <div style="color: #fdc;">内容区</div>
        </div>
      ),
      main: ({age}) => (
        <>
          <div style="color: #1E90FF;">节点1,从子组件传过来的值age: {age}</div>
          <div style="color: #87CEEB;">节点2</div>

        </>
      )
    }

    const count = ref(0)
    const title = ref('placehold title')

    const person = {
      name: 'jsx',
      age: 24
    }

    // {...person}类似于模板语法的v-bind="person"
    return () => (
      <>
        <div>双向绑定的count:{ count.value }</div>
        <div>双向绑定的title:{ title.value }</div>

        <JsxComp
          v-slots={slots}
          v-model={count.value}
          v-model:title={title.value}
          msg="msg from parent"
          onCustomEvent={(params) => console.log(params)}
          {...person}
        >
        </JsxComp>
      </>
    )
  }
})