vue3下jsx教学,保证业务上手无问题!手敲代码,有知识点,附带和template对比

32 篇文章 2 订阅

 

前言

 

原因:闲的。还有一个原因吗?嘿嘿

然后之前就说过了,我拿vue3直接干公司项目了,是内部的孵化项目,客户不多,但是也是正规的生产项目。

技术栈说明:

公司项目:js,element-plus,vue3,jsx,常规template,按需加载(单独领出来是因为这个也会导致你的项目填坑变多)

个人组件库研究项目:TypeScript,vue3,jsx,常规template

主要讲公司项目吧,公司项目上线一个月。还算稳定,不过我需要给没能力填坑的大家看一个截图。

一、vue3生产注意点

1、不能使用getCurrentInstance的ctx

大家在获取app.config.globalProperties.$ELEMENT这里设置的$ELEMENT是要如下

这里的ctx不是steup里面提供的ctx,而是

const { ctx } = getCurrentInstance()

这里ctx在生产环境下是获取不到的,请各位没玩过生产的别误人子弟,这还是我专门给vue3提交issues问来的。

正确应该使用

const { proxy } = getCurrentInstance()

关于在ts中使用

在ts里面大家估计经常会遇到类型定义错误问题。

使用方式1

interface ProxyEx extends ComponentPublicInstance {
  $test: number
}

export default defineComponent({
  setup(props, ctx) {
    const self = getCurrentInstance() as ComponentInternalInstance
    const proxy = self.proxy as ProxyEx
    console.log(proxy?.$test)
    return {}
  },
})

使用方式2

上述方案把类型定义到了页面内部,也可以弄一个全局文件去调用。但是总是不方便的。还有就是我们去扩展@vue/runtime-core文件,

然后我们可以在main.ts文件下面增加这段,也不会产生全局错误了

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $test: number
  }
}

但是不能放在d.ts文件下面,否则会全局产生错误。我翻了好久的issues也没有找到方案。看官方后续优化解决吧。

二、jsx教学开始

关于jsx的官方教学地址:https://vue3js.cn/docs/zh/guide/render-function.html#jsx

git部分:https://github.com/vuejs/jsx-next/blob/dev/packages/babel-plugin-jsx/README-zh_CN.md

我这里就不放关于h函数的处理了,就直接用和react一样的编码方式来书写。主要是vue3的cli已经内置好了babel插件,也就是说大家不需要单独去引入babel插件。直接可以使用。

也就是大家可以这样写vue文件来书写jsx代码。这里我贴上简单示例

<script lang="tsx">
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'test',
  emits: [],
  directives: {},
  props: {},
  setup(props, ctx) {
    return () => (
      <span>简单示例</span>
    )
  },
})
</script>

Template对比

<template>
    <span>简单示例</span>
</template>
<script lang="tsx">
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'test',
  emits: [],
  directives: {},
  props: {},
  setup(props, ctx) {
    return {},
})
</script>

1、注意事项

1、html代码的js文件或者ts文件后缀名增加‘x’,原本是.js变成.jsx,ts变成.tsx

2、.vue文件的script标签增加 lang="tsx",这个呢是你里面存着jsx写法的就需要加。

比如这种情况

<template>
  <div class="experiment">
    <test-com>
      <template #default="de">{{ de }}</template>
    </test-com>
    <test-a></test-a>
  </div>
</template>

<script lang="tsx">
import { defineComponent } from 'vue'
import { TestCom } from './Test'

const TestA = () => <div>fdsafdsa</div>

export default defineComponent({
  components: { TestCom, TestA },
  setup(props, ctx) {
    return {}
  },
})
</script>

 

3、常规jsx语法下是无法配合scpoedCss使用的,解决方法等下就讲

2、在.vue文件下面让你的jsx代码也可以使用scopedCss

也就是大家经常在css部分加的这个<style lang="scss" scoped>

来源:https://github.com/vuejs/jsx-next/issues/51

原理:只要取到 __scopeId, 就可以在 setup/render 函数中

其中可以得到__scopeId的方式

方案1:

// main.js
import { createApp } from 'vue';
import App from './App.vue'

createApp(App(App)).mount('#app') // 这里将 App 传回组件

// ./App.vue
export default ({ __scopeId }) => {
   // 这里可以取到 __scopeId
})

方案2:

// ./App.vue
export default {
   render(ctx) {
       // ctx._.type.__scopeId
   }
}

方案3: setup 可以返回一个 render 方法, 用于 render jsx

// ./App.vue
export default {
   setup() {
        return (ctx) {
             // ctx._.type.__scopeId
        }
   }
}

上面的实现对于大家比较抽象,我这里提供一个自己的实现方式

<script lang="tsx">
import { defineComponent, withScopeId, getCurrentInstance } from 'vue'

export default defineComponent({
  setup(props, ctx) {
    const instance = getCurrentInstance()
    const scopeId = instance.type.__scopeId
    const withId = withScopeId(scopeId)

    return withId(() => <div class="ceshi">fdsafas</div>)
  },
})
</script>

然后附上template对比

<template>
    <span>简单示例</span>
</template>
<script lang="tsx">
import { defineComponent} from 'vue'

export default defineComponent({
  setup(props, ctx) {},
})
</script>
<style scoped lang="scss">
.ceshi {
  width: 100px;
  height: 100px;
  background: red;
}
</style>

关于像cssModule,css-in-js等方案大家就自行研究。

3、关于class和style样式

这里提醒一下,大家不太会用的直接拿react代码套进来也可以。也就是你可以直接搜索react使用方式,然后这里一样可以用。就是注意下比如className和class这种区别。语法上两者真的差异不大

class部分使用和template部分没有差别

<span class="tooltip" ref={tooltip}>

或者

动态方式就不太一样

<div class={{ 'test-red': classt }}></div>

style部分如果是没有动态的那么和普通的写法没差异

但是动态的需要是这样的

<div style={{ visibility: data.show }}></div>

4、在jsx里面使用ref,reactive等

import { defineComponent, ref, reactive } from 'vue'
const Testref = defineComponent({
  setup(props, ctx) {
    const dateone = ref('测试')
    const datetwo = reactive({
      test: '测试',
    })
    return () => (
      <div>
        <div>{dateone.value}</div>
        <div>{datetwo.test}</div>
      </div>
    )
  },
})

template对比

<template>
  <div class="experiment">
    <div>{{ dateone }}</div>
    <div>{{ test }}</div>
  </div>
</template>

<script lang="tsx">
import { defineComponent, reactive, ref, toRefs } from 'vue'
export default defineComponent({
  setup(props, ctx) {
    const dateone = ref('测试')
    const datetwo = reactive({
      test: '测试',
    })
    return {
      dateone,
      ...toRefs(datetwo),
    }
  },
})
</script>

5、使用插槽和具名插槽

官方:https://vue3js.cn/docs/zh/guide/component-slots.html#插槽内容

const testslot = defineComponent({
  setup(props, ctx) {
    return () => (
      <div>
        {/*默认插槽*/}
        <div>{ctx.slots.default && ctx.slots.default()}</div>
        {/*具名插槽*/}
        <div>{ctx.slots.name && ctx.slots.name()}</div>
      </div>
    )
  },
})

template对比

<template>
    <div>
      <slot></slot>
    </div>
    <div>
      <slot name="name"></slot>
    </div>
</template>
<script lang="tsx">
</script>

6、作用域插槽使用

官方地址:https://vue3js.cn/docs/zh/guide/component-slots.html#作用域插槽

传参

const TestA = defineComponent({
  setup(props, ctx) {
    return () => <div>{ctx.slots.default && ctx.slots.default('作用域传参')}</div>
  },
})

使用

const TestD = defineComponent({
  setup(props, ctx) {
    return () => (
      <div>
        <span>组件:TestD</span>
        <TestA
          v-slots={{
            default: (n: string) => <span>{n}</span>,
          }}
        />
      </div>
    )
  },
})

template对比

传参

<template>
    <div>
      <slot :defalut="作用域传参"></slot>
    </div>
</template>
<script lang="tsx">
</script>

使用

<todo-list>
  <template v-slot:default="slotProps">
    <span class="green">{{ slotProps}}</span>
  </template>
</todo-list>

7、v-if 和 v-for

官方:https://vue3js.cn/docs/zh/guide/render-function.html#v-if-和-v-for

const iffor = defineComponent({
  setup(props, ctx) {
    const ces = true
    const ifelse = () => {
      if (ces) return <div>if部分</div>
      else return <div>else部分</div>
    }

    const forarr = () => {
      let i = 0
      return new Array(10).fill('').map(() => {
        i++
        return <span>{i}</span>
      })
    }

    return () => (
      <div>
        {ifelse()}
        {forarr()}
      </div>
    )
  },
})

template对比

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

8、v-model的使用

<input type="text" v-model={inpt.value} onChange={onInput} />

template部分我就不放了,太简单了

9、jsx情况下可以不要.vue文件示例

示例1:

export const TestCom = defineComponent({
  setup(props, ctx) {
    return () => <TestD />
  },
})

示例2:

import { ref } from 'vue'
interface Props {
  text: string
}

// ref不能放在组件内部,否则会重新赋值,导致不会响应式变化
const num = ref(1)
const dataone = ref('测试无defineComponent数据变化')

const addnum = () => {
  num.value++
}
const inpt = ref('')
const onInput = () => {
  console.log(inpt.value)
}

const NoDcom = (props: Props, ctx: any) => {
  //console.log(props, ctx)
  return (
    <div>
      <div>{dataone.value}</div>
      <button onClick={addnum}>点击测试数据变化</button>
      <div>变化的数字{num.value}</div>
      <input type="text" v-model={inpt.value} onChange={onInput} />
    </div>
  )
}
export default NoDcom

 

10、关于在vue文件组件注册

注意jsx的文件就不要加jsx后缀了

使用和常规的组件注册一样

import { TestCom } from './Test'
export default defineComponent({
  components: { TestCom},
  setup(props, ctx) {
    return {}
  },
})
</script>

11、关于click事件之类

原本模板中是@click,全部改为onClick模式,和react比较相似

举例

<input type="text" v-model={inpt.value} onInput={onInput} onChange={onInput} />

template

<input type="text" v-model="inpt.value" @input="onInput" @change="onInput" />

12、使用Teleport

在jsx下使用vue内置的组件需要额外导入组件,并且名称不能变。

import { defineComponent, Teleport } from 'vue'
// 使用Teleport
const teleportCum = defineComponent({
  setup(props, ctx) {
    return () => (
      <Teleport to="body">
        <div>teleport值</div>
      </Teleport>
    )
  },
})

Template使用

并且是不需要单独导入,也没有大小写区分的问题

<template>
  <teleport></teleport>
</template>

13、使用Transition

和使用teleport一样,就不单独写代码了。上面的例子一样,把Teleport换成Transition就可以跑了

14、关于指令修饰符

官方自带的指令修饰符方面是说类似自己实现

地址:https://v3.cn.vuejs.org/guide/render-function.html#v-on

然后我个人在项目中因为自定义了一个可以随意拖动的v-move指令,并且定义了boundary修饰符代表是否可以只在父元素范围内移动。这时候就很烦了

最后多方百度下至少自定义的指令修饰符是通过“_”连接,如下

<div class="dht-float-win" v-dht-move_boundary={{ callmove: startmove, callstop: stopmove }}>
       {ctx.slots.default && ctx.slots.default()}
</div>

而template里面

<div class="dht-float-win" v-dht-move.boundary="{ callmove: startmove, callstop: stopmove }">
       <slot></slot>
</div>


作者:海天酱油_沃利贝尔
链接:https://juejin.cn/post/6911883529098002446/
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

三、关于jsx和常规的sfc(vue文件方式)

写到这里,说实在我有点想不到该写什么。jsx给我的感觉就是在用react语法。为什么呢?因为这个babel插件是阿里团队优先搞出来了,然后大家都知道阿里是react技术栈为主的。所以语法上面靠近react。

我说说我的几个见地吧

1、jsx并没有更加方便,只是让你更接近vue模板渲染的底层原理

2、jsx语法灵活很多,光我上面就出现了两种jsx书写方式,个人还是推荐defineComponent方式,更加是一个整体

3、jsx书写会比较强迫的让你把for循环等小逻辑模板单独成为一个函数,优点就是颗粒度更小了,和sfc书写方式相比,没有那么方便。

特别是什么呢,我上面的提供方式优点不方便在return部分写逻辑,如果你写if会报错,官方方面都是用的render方式,其实两者的差异并不大

4、如果你用vue那么jsx可以作为一种优化方式(官方推荐使用template方式意味着,template方式bug会优先解决),而不是大规模使用。特别是中小公司,大公司也好不到那里去。除了业内头部公司,有几个团队能完全规范化的呢?

这里我说一句话:何不食肉糜?

要是全用jsx,那我还是用react吧。何况react薪资更高呢

5、jsx一定要学,但是ts更要学。虽然未来一定不是ts的天下(浏览器领域),但是ts所带来的影响已经把中高端领域覆盖。

6、学习jsx是让你拓宽视野,react生态毕竟是领头羊。

四、关于vue3书写方式,我想新手很容易写出这种代码吧。

放一个我个人封装的组件地址:https://github.com/ht-sauce/visualization/tree/master/src/componentsLibrary/Popper

文档地址:https://www.yuque.com/cv8igf/oy3c8b/uy5sqv

这恐怕就是大佬所担心的面条式代码,说白了,这就是vue2所带来的习惯。vue3的情况下,那么props,函数,响应式部分都是可以抽离到单独的js文件,但是vue2恐怕只能抽离props部分了。

<script lang="tsx">
import { dataType, visibility } from './types'
import { defineComponent, onMounted, ref, reactive, onUnmounted, watch } from 'vue'
import { createPopper } from '@popperjs/core'
import type { Instance, Options } from '@popperjs/core'
import ClickOutside from '../ClickOutside'
export default defineComponent({
  name: 'DhtPopper',
  emits: ['update:modelValue', 'hide', 'show'],
  directives: {
    'dht-click-outside': ClickOutside.directive,
  },
  props: {
    trigger: {
      type: String,
      default: 'hover', // hover,click,manual
    },
    modelValue: Boolean, // 手动绑定
    disabled: {
      type: Boolean,
      default: false,
    },
    arrow: {
      type: Boolean,
      default: true,
    },
    offset: {
      type: Number,
      default: 16,
    },
    placement: {
      type: String,
      default: 'bottom',
    },
    options: {
      type: Object,
      default: () => null, // 出现ts问题参考:https://github.com/vuejs/vue-next/issues/2474
    },
    clickOutside: {
      type: Boolean,
      default: true,
    },
  },
  setup(props, ctx) {
    let popperInstance: Instance | null = null

    const data = reactive({
      show: 'hidden',
    } as dataType)

    // 被绑定的
    const popper = ref<string | HTMLElement>('popper')
    // 会移动的,这是提示内容
    const tooltip = ref<string | HTMLElement>('tooltip')

    // 合并参数
    function mergeOptions() {
      const opt = {
        placement: props.placement,
        ...props.options,
      } as Options
      const modifiers = []

      modifiers.push({
        name: 'offset',
        options: {
          offset: [0, props.offset],
        },
      })

      opt.modifiers = [...modifiers]
      return opt
    }

    onMounted(() => {
      if (props.disabled) return false

      const popperDom = popper.value as HTMLElement
      const tooltipDom = tooltip.value as HTMLElement

      popperInstance = createPopper(popperDom, tooltipDom, mergeOptions())
    })

    onUnmounted(() => {
      ;(popperInstance as Instance)?.destroy()
      popperInstance = null
    })

    function hide() {
      data.show = visibility.hidden
      ctx.emit('update:modelValue', false)
      ctx.emit('hide')
    }

    function show() {
      data.show = visibility.visible
      ctx.emit('update:modelValue', true)
      ctx.emit('show')
    }

    // 点击事件
    function onClick() {
      if (props.trigger !== 'click') return null
      if (data.show === 'hidden') show()
      else hide()
    }

    function onMouseover() {
      if (props.trigger !== 'hover') return null
      show()
    }

    function onMouseout() {
      if (props.trigger !== 'hover') return null
      hide()
    }

    watch(
      () => props.modelValue,
      (e) => {
        if (props.trigger !== 'manual') return null

        if (e) show()
        else hide()
      },
    )

    function clickOutside() {
      if (props.trigger === 'hover') return
      if (data.show === 'hidden') return
      if (props.clickOutside) hide()
    }

    return () => (
      <span v-dht-click-outside={clickOutside} class="dht-popper">
        <span ref={popper} onClick={onClick} onMouseover={onMouseover} onMouseout={onMouseout}>
          {ctx.slots.default && ctx.slots.default()}
        </span>
        <span class="tooltip" style={{ visibility: data.show }} ref={tooltip}>
          {props.arrow && <span class="arrow" data-popper-arrow />}
          {ctx.slots.tooltip && ctx.slots.tooltip()}
        </span>
      </span>
    )
  },
})
</script>

五、致谢

谢谢大家观看。喜欢的点个赞。

掘金地址(也是我):https://juejin.cn/post/6911883529098002446

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值