vue2向vue3进阶(tsx)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

0、版本讲解

注意本文主要在以下三个vue版本里讲解其实大部分人只需要看vue3.0或者3.2的写法就行‘@vue/composition-api’ 写法只针对于在vue2.x项目里使用vue3.0组合式api的库在本文中没有标注版本的地方默认为vue3.x的写法@vue/composition-api’  适用于服务器node版本低或者工作里都是vue2.x的项目无法直接用vue3的框架

0.1、@vue/composition-api尝鲜版少数人使用

import { ref, defineComponent } from '@vue/composition-api'
export default defineComponent({
    setup() {
        const str = ref<string>('hello lindadayo')
        return () => (
            <div>{ str.value }</div>
        )
    }
})

0.2、vue3.0标准版多数人使用

import { ref, defineComponent } from 'vue'
export default defineComponent({
    setup() {
        const str = ref<string>('hello lindadayo')
        return () => (
            <div>{ str.value }</div>
        )
    }
})

0.3、vue3.2标准版多数人使用

<script lang="ts" setup>
import { ref } from 'vue'
const str = ref<string>('hello lindadayo')
return () => (
   <div>{ str.value }</div>
)
</script>

1、setup

@vue/composition-api结构

import { PropType } '@vue/composition-api'
export default defineComponent({
    props: {
        age: {
            type: Number
        },
        onToggleClick: {
            type: Function as PropType<() => void>
        }
    }
    setup(props, {slots, emit, attrs, listeners, root}) {
        // slots 插槽使用在下面第6类中介绍
        // emit('xxx', xxx)
        // attrs 跟vue2的 this.$attrs类似
        // listeners 跟vue2的 this.$listeners 类似
        // root vue实例
        // 其他的还有一些比如 parent, children
    }
})

vue3.0结构:

import { PropType } from 'vue'
export default defineComponent({
    props: {
        age: {
            type: Number
        },
        onToggleClick: {
            type: Function as PropType<() => void>
        }
    }
    setup(props, {slots, emit, expose, attrs, listeners, root}) {
        // slots 插槽使用在下面第6类中介绍
        // emit('xxx', xxx)
        // expose 子组件中属性或者方法抛出
        // attrs 跟vue2的 this.$attrs类似
        // listeners 跟vue2的 this.$listeners 类似
        // root vue实例
        // 其他的还有一些比如 parent, children
    }
})

 vue3.2

<script lang="ts" setup>
    // emit用法 先定义emit执行时候和vue3.0方式一样
    const emit = defineEmits<{
        (e: 'clickBtn', data: any): void
      }>()
    // props用法
    interface Props {
      labelWidth?: string | number //标签的宽度
    }
    const props = withDefaults(defineProps<Props>(), {
        labelWidth: '120px'
    })
    // 父组件想要调用子组件方法首先在子组件将方法名抛出去
    defineExpose({ getFormValue })
<script>

2、refreactivetoReftoRefs

重点

ref

ref本质也是reactiveref(obj)等价于reactive({value: obj})

  • vue中使用ref的值不用通过.value获取
  • js中使用ref的值必须通过.value获取

reactive

  • reactive的参数必须是一个对象包括json数据和数组都可以否则不具有响应式

toReftoRefs

常用于解构props保持响应式特征

区别是toRef只能转换一个值toRefs转换所有值

3、watch

3.1、监听基础类型

  const nums = ref(0)

  watch(nums, (newValue, oldValue) => {
  })

3.2、监听对象 

const react = reactive({
	name: 'xxx',
    prop: {
        xxx: xxx
    }
})

// 监听整个对象
watch(react, (newValue, oldValue) => {
})

// 监听对象某个属性最常用
watch(() => react.name, (newValue, oldValue) => {
})

// 监听对象的第一级子属性
watch(() => ({ ...react }), (newValue, oldValue) => {
})

// 监听对象的所有属性最常用
watch(() => react, (newValue, oldValue) => {
}, { deep: true })

 3.3、监听数组

ref定义的数组下面这种写法是完全没有问题的新旧数据都是正确的

const react = ref([])

// 监听整个数组 
watch(() => [...react.value], (newValue, oldValue) => {
})

reactive定义的数组不管怎么样新数据是正确的旧数据和新数据一直都是一致这就有问题le 所以解决方式就只有将数组嵌套再对象里面然后监听数组的长度

const react = reactive([])

// 新数据正确旧数据有问题
watch(react, (newValue, oldValue) => {
})

// 新数据正确旧数据有问题
watch(react, (newValue, oldValue) => {
}, {deep: true})

// 不管加不加deep都无法运行
watch(() => react.length, (newValue, oldValue) => {
})

const react = reactive({
    arr: []
})

// 新旧数据正确
watch(() => react.arr.length, (newValue, oldValue) => {
})

3.4、watchEffect

其实和watch的区别我的理解哈watchEffect类似于immediat为true开局立即启动了将watchEffect赋值给一个变量再调用的话就是解除监听了这一点要看你喜欢用watch还是watchEffect当然那些副作用啥的概念我也没去研究就暂时不讲解那些了

// 监听 
const handle = watchEffect(() => {
    /* ... */
  })

// 解除监听
handle()

4、computed

其实就和vue2的computed类似

const xxx = ref(0)

// 只get
computed(() => xxx.value)

// set get
computed({
    set(val) {
    },
    get() {
    }
})

5、css

当然我建议将css提出来这样tsx里面就只有逻辑层和渲染层了再很一点的话将逻辑层也提出来用一个单独的ts文件存放tsx就只放渲染层的代码

css文件名: index.module.scss

方式一直接import的话就是用的全局样式
index.module.scss:
:global{
    .top-container{
        width: 100px;
    }
}

import 'index.module.scss';

export default defineComponent({
    return () => (
        <div class='top-container'></div>
    )
})

方式二import Styles from 'index.module.scss' 的话就是组件scoped方式不过里面样式要写成小驼峰形式建议使用
index.module.scss:
.topContainer{
    width: 100px;
}

import Styles from 'index.module.scss';
export default defineComponent({
    return () => (
        <div class={styles.topContainer}></div>
    )
})

6、tsx

6.1、花括号和小括号使用

花括号一般用于js逻辑处理的时候和属性、事件赋值时候类似于插值表达式主要是逻辑层处理

           
export default defineComponent({
    return () => (
        <div>
            {
              showLogo.value && <Logo collapse={isCollapse.value} />
            }
        </div>
    )
})

小括号一般用于最外层 包裹组件的时候 或者 包裹一段单独的逻辑时候主要是渲染层处理

export default defineComponent({
    return () => (
        <div></div>
    )
})

6.2、事件绑定

tsx目前还没有事件修饰符的写法所以想要加修饰符就两种方式

1、使用 @vue/babel-preset-jsx 依赖的时候使用 vOn:click_stop 类似的方式

2、自行实现修饰符的功能比如最简单的两个

.prevent 写的时候就是 e,preventDefault()

.stop 写的时候就是e.stopPropagation()

但是当常规开发时, 使用 短横线或者小驼峰形式都是可以的

export default defineComponent({
    return () => (
        <div>
            <div on-toggle-click={xxx}></div>
            <div onToggleClick={xxx}></div>
        </div>
    )
})

6.3、动态class动态style

class下面是主流的几种class写法

举个例子class.headerSearch的classModule 是 模块化class写法定义变量一般的话你可以不用管它直接类似下面第3,4,5写法就行了

import classModule from './index.module.scss'
export default defineComponent({
    return () => (
        <div>
            <div class={[classModule.headerSearch, { [classModule.show]: show.value }]></div>
            <div class={[classModule.avatarContainer, classModule.rightMenuItem]}></div>
            <div class='back-to to-top'></div>
            <div class={['back-to', { 'show': show.value }]}></div>
            <div class={[show.value ? 'active' : '', 'tags-view-item']}></div>
        </div>
    )
})

style 下面是主流的几种style写法

export default defineComponent({
    return () => (
        <div>
            <div style={{ 'top': props.buttonTop + 'px', 'background-color': theme.value }}></div>
            <div style={{ display: visible.value ? 'block' : 'none' }}></div>
            <div style='padding: 8px 10px;'></div>
        </div>
    )
})

6.4、v-for数组

有童鞋要问了那我想使用filter筛选怎么办呢 既然用了tsx就灵活点呗tsx里面是允许写逻辑的直接在里面用filter就可以了

           
export default defineComponent({
    return () => (
        <div>
            {
              array.value.map(route => {
                return <SideBarItem
                  item={route}
                  basePath={route.path} />
              })
            }
        </div>
    )
})

6.5、v-if条件判断

逻辑与运算符

           
export default defineComponent({
    return () => (
        <div>
            {
              showLogo.value && <Logo collapse={isCollapse.value} />
            }
        </div>
    )
})

三目运算符 

           
export default defineComponent({
    return () => (
        <div>
            {
              showLogo.value ? <Logo collapse={isCollapse.value} /> : null
            }
        </div>
    )
})

6.6、v-show、v-model 

vue3 + tsx 默认支持这两个用法喔~和常规写法一致不需要像其他tsx转换写法

6.7、slot插槽

6.7.1、下面是vue2关于插槽的讲解链接 

vue学习5vue高阶slot插槽使用

6.7.2、下面是 vue2.x + tsx尝鲜版关于插槽的使用方式

child.tsx: 子组件 

          
export default defineComponent({
    setup(props, { slots }) {
    const { proxy } = getCurrentInstance()
    return () => (
        <fragment>
          <div>{ slots.header?.() }</div>
          <div>{ slots.default?.() }</div>
        </fragment>
    )}
})

 parent.tsx: 父组件

          
// 方式一更偏向于v2的slot用法
export default defineComponent({
  setup() {
    return () => (
      <child>
        <div slot='header'>这才是标题</div>
        <div>默认插槽</div>
      </child >
    );
  }
})
          
// 方式二更偏向于v3的jsx用法
export default defineComponent({
  setup() {
    return () => (
      <child
        scopedSlots={{
            header: () => {
                return <div>这才是标题</div>
            },
            default: () => {
               return <div>默认插槽</div> 
            }
        }}
      >
      </child >
    );
  }
})

6.7.3、下面是vue3 + tsx标准版 关于 插槽的使用方式 

scopedSlot作用域插槽其意旨在子组件里操作父组件的数据下面一般都有默认插槽和具名插槽举个例子 el-table 的使用

           
export default defineComponent({
    return () => (
        <div>
          <el-table
            data={errorLogs.value}
            border>
            <el-table-column
              label='Message'
              scopedSlots={{
                default: scope => {
                  return (
                    <div>
                      <div>
                        <span class='message-title'>Msg:</span>
                        <el-tag type='danger'>
                          {scope.row.err.message}
                        </el-tag>
                      </div>
                    </div>
                  )
                }
              }}
            >
            </el-table-column>
          </el-table>
        </div>
    )
})

v-slot在tsx里应修改为v-slots

child.tsx  子组件

          
export default defineComponent({
    const { proxy } = getCurrentInstance()
    setup(props, {slots}) {
        return () => (
        <>
          <div>{ slots.header?.() }</div>
          <div>{ slots.default?.() }</div>
        </>
    )
    }
})

 parent.tsx 父组件

          
// 方式一
export default defineComponent({
    setup() {
        const slots = {
          header: () => <span>具名插槽喔</span>,
        }
    return () => (
      <child v-slots={slots}>
        <div>这才是标题</div>
      </child >
    );
  }
})
// 方式二
export default defineComponent({
    setup() {
    return () => (
      <child>
        {{
          default: () => <div>这才是标题</div>,
          header: () => <span>具名插槽喔</span>,
        }}
      </child >
    );
  }
})

7、xxx.d.ts 使用

注意点

1、项目内命名为 xxx.d.ts的文件不需要在main.ts内引入因为只要是d.ts结尾的都被视为同级

2、在 d.ts 声明文件中任何的 declare 默认就是 global 的了所以你在 d.ts 文件中是不用出现 declare global 的。只有在具体ts文件中的定义如果想要全局就使用 declare global

vue3 + ts 下的 d.ts 推荐写法

 import { ComponentRenderProxy } from '@vue/composition-api'; 
 declare namespace JSX {
    type Element = VNode
    type ElementClass = ComponentRenderProxy
    interface ElementAttributesProperty {
      $props: any; // specify the property name to use
    }
    interface IntrinsicElements {
      [elem: string]: BasicVueElement;
    }
    interface IntrinsicClassAttributes {
      class?: any;
      id?: string;
      ref?: string;
      style?: any;
      key?: any;
      props?: any;
      scopedSlots?: object;
    }
  }

  interface BasicVueElement {
      [param: string]: any;
      props?: {
        [key: string]: any;
      };
      ref?: any;
      class?: any | any[];
      onClick?: (e: MouseEvent) => void;
      onMouseUp?: (e: MouseEvent) => void;
      onKeyUp?: (e: KeyboardEvent) => void;
      onKeyDown?: (e: KeyboardEvent) => void;
      'v-model'?: any;
      vModel?: any;
    }

8、疑难解答

1、vue3 下 tsx 有几种渲染方式

两种一种是setup外面 render一种是setup里面return

render有this和antd vue这种组件库写到tsx中还要再加this要改的地方比较多不推荐

import { defineComponent, ref, reactive } from 'vue';

export default defineComponent({
    setup(){
        interface Item {
            [T: string]: any
        }
        const num = ref<number>(0)
        const arr = reactive<Array<Item>>([])
        return {
            num,
            arr
        }
    },
    render() {
        return (
            <>
                <div>{this.num}</div>
                <div>hello world</div>
                {this.arr.map((item: any, index: number) => {
                  return (
                    <div key={index}>
                      {item.name}:{item.value}
                    </div>
                  )
                })}
            </>
        )
    }
})

return 推荐

import { defineComponent, ref, reactive } from 'vue';

export default defineComponent({
    setup(){
        interface Item {
            [T: string]: any
        }
        const num = ref<number>(0)
        const arr = reactive<Array<Item>>([])
        return () => (
            <>
                <div>{num.value}</div>
                <div>hello world</div>
                {arr.map((item: any, index: number) => {
                  return (
                    <div key={index}>
                      {item.name}:{item.value}
                    </div>
                  )
                })}
            </>
        )
    }
})

2、如何解决tsx下多节点问题

vue3及以上 新增fragment特性可以直接写多个根节点vue2想要实现多个根节点安装依赖 vue-fragment

注意vue-fragment 是在vue2项目里用的 

// main.ts
import Fragment from 'vue-fragment'
Vue.use(Fragment.Plugin)

使用方式如下其实就是绕过vue的内核编译然后在渲染时会将最外层fragment节点删除掉就不会有多出一层div的结构了在react里也有类似的结构, React.Fragment 或者 <></>语法糖

vue2 + tsx方式

import { defineComponent, toRefs } from '@vue/composition-api';

export default defineComponent({
  name: 'MenuItem',
  props: {
    icon: {
      type: String,
      default: ''
    }
  },
  setup(props) {
    const { icon } = toRefs(props)
    return () => (
      <fragment>
        {icon.value && (() => {
          if (icon.value.includes('el-icon')) {
            return <i class={[icon.value, 'sub-el-icon']} style={{ color: 'currentColor', width: '1em', height: '1em' }} />
          } else if (icon.value.includes('yxp_nav')) {
            return <i class={[icon.value, 'yxp_nav']} />
          } else {
            return <svg-icon icon-class={icon.value}/>
          }
        })()}
      </fragment>
    )
  }
})

vue3 + tsx: 默认支持fragment不需要额外写进去 

import { defineComponent, ref, reactive } from 'vue';

export default defineComponent({
    setup(){
        interface Item {
            [T: string]: any
        }
        const num = ref<number>(0)
        const arr = reactive<Array<Item>>([])
        return () => (
            <>
                <div>{num.value}</div>
                <div>hello world</div>
                {arr.map((item: any, index: number) => {
                  return (
                    <div key={index}>
                      {item.name}:{item.value}
                    </div>
                  )
                })}
            </>
        )
    }
})

react方式

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <div>Hello</div>
      </React.Fragment>
    );
  }
}

或者

class Columns extends React.Component {
  render() {
    return (
      <>
        <div>Hello</div>
      </>
    );
  }
}

3、 vue3 + tsx 下的el-form表单使用

下面我以填写手机号和验证码的表单来举例

import { defineComponent, ref, reactive } from 'vue';

export default defineComponent({
    setup(){
        const formData = reactive<{mobile: string, verifyCode: string}>({
          mobile: '',
          verifyCode: ''
        })
        const checkPhone = (rule, value, callback) => {
          const reg = /^1[3456789]\d{9}$/
          if (reg.test(value)) {
            return callback();
          }
          callback('请输入正确格式的手机号码!');
        }
        const rules = reactive({
          mobile: [
            { required: true, validator: checkPhone, message: '请输入正确格式的手机号码', trigger: 'blur' }
          ],
          verifyCode: [
            { required: true, message: '请输入短信验证码', trigger: 'blur' }
          ]
        })
        const form = ref(null)
        return () => (
            <el-form
                ref={form}
                rules={rules}
                props={{
                  model: formData
                }}
              >
                <el-form-item prop='mobile'>
                  <el-input
                    v-model={formData.mobile}
                    placeholder='请输入手机号'
                  >
                    <i slot='prefix' class={['el-input__icon']} />
                  </el-input>
                </el-form-item>
                <el-form-item prop='verifyCode'>
                  <el-input
                    v-model={formData.verifyCode}
                    placeholder='请输入短信验证码'
                  >
                    <i slot='prefix' class={['el-input__icon']} />
                  </el-input>
                  {
                    verShow.value ? <el-button
                      获取短信验证码
                    </el-button> : <el-button>
                      <span>{timer.value}</span>秒后重新获取
                    </el-button>
                  }
                </el-form-item>
                <el-form-item>
                  <el-button
                  >确定</el-button>
                  <el-button
                  >取消</el-button>
                </el-form-item>
              </el-form>
        )
    }
})

那么这里其实有两个不容易解决的点

1、Invalid handler for event "input": got undefined

 解决方法如下

            <el-form
                ref={form}
                rules={rules}
                props={{
                  model: formData
                }}
              >
             </el-form>

2、Cannot add property isRootInsert, object is not extensible

 出现这个问题的原因是v-show不能同时出现在同一级上应该跟v-if的方式一样,使用三目运算符解决方法如下

                  {
                    verShow.value ? <el-button>
                      获取短信验证码
                    </el-button> : <el-button>
                      <span>{timer.value}</span>秒后重新获取
                    </el-button>
                  }

 4、el-form 自定义校验规则的ts写法

       vue3情况下 XXX.d.ts: 定义在d.ts后缀的都被认为是声明全局

      @vue/composition-api情况下 主动声明为global 才会被ts校验过,在外部tsx文件使用global里面的interface

interface CallbackType {
  (message?: string | Error | undefined): Error | void
}
declare global {
  interface ValidateType {
    (_rule: any, value: string, callback: CallbackType): void
  }
}

使用方式

xxx.tsx

  const checkPhone: ValidateType = (rule, value, callback) => {
      const reg = /^1[3456789]\d{9}$/
      if (reg.test(value)) {
        return callback();
      }
      callback('请输入正确格式的手机号码!');
    }

5、props里是对象或者数组的时候如何类型处理

'@vue/composition-api' 写法和vue3.x标准写法都和下面例子一致  

type: Object as PropType<ObjType> 或者 type: Array as PropType<ArrType[]>

// @vue/composition-api
import { defineComponent, PropType } from '@vue/composition-api';
// vue3.x
import { defineComponent, PropType } from 'vue';

interface ObjType{
  type?: string; 
}
interface ArrType{
  type?: string; 
}
export default defineComponent({
  name: 'MenuItem',
  props: {
    obj: {
      type: Object as PropType<ObjType>,
      default: () => {
        return {
          type: 'default'
        }
      }
    },
    arr: {
        type: Array as PropType<ArrType[]>,
        default: () => {
            return [
              {
                type: 'drop'
              }
            ]
        }
    }
  },
  setup(props) {
    const { icon } = toRefs(props)
    return () => (
        <div>
            {props.obj.type}
            {
                props.arr.map(item => {
                    return <div>{item.type}</div>
                })
            }
        </div>
    )
  }
})

9、有趣知识科普

1、@vue/composition-api 下的h函数和vue下的h函数

vue3框架里有四种写法

1、模板template常规不讲解了
2、h函数 写法高难这里着重讲解
3、jsx写法中难这篇博客上面的写法都是这种jsx的, jsx是h的语法糖更简洁好用
4、函数式组件写法稀有太少用了我都没咋写过

@vue/composition-api 下h函数写法 注意在return时不能当做一个组件  <ele />来用噢~

// @vue/composition-api
import { defineComponent, h } from '@vue/composition-api';

export default defineComponent({
  setup() {
    const ele = h('div', null, 'hello world')
    return () => (
      {
        ele 
      }
    )
  }
})

vue3.x 下h函数写法h函数的核心就是createVNode方法包裹着一层

<script lang="ts">
  import { defineComponent, createVNode } from 'vue'

  export default defineComponent({
    return () => createVNode(
        'div', null, 'hello world'
      )
  })
</script>

// 或者

<script lang="ts">
  import { defineComponent, h } from 'vue'

  export default defineComponent({
    return () => h(
        'div', null, 'hello world'
      )
  })
</script>

---持续更新有问题或者我有错误的地方都可以底下评论噢~---

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue