Vue3常见api使用指南(TS版)

6,431 阅读9分钟

放了几篇我觉得还不错的文章,兄弟们可以参考,有遗漏的地方,基本官网都有说明

defineProps() 和 defineEmits()

  • 内置函数,无需import导入,直接使用。

  • 传入到 defineProps 和 defineEmits 的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量(比如设置默认值时),但是,它可以引用导入(import)的变量,因为它们也在模块范围内。就是说props设置默认值的时候不能直接用setup里的变量,可以用import引入的数据

    <script setup>
     import type { PropType } from 'vue';
     
    //props是响应式的不能解构
    //方法1:不能设置默认值(使用withDefaults解决)
    const props = defineProps({
      foo?: String,
      id: [Number, String],
      onEvent: Function, //Function类型
      metadata: null
    })
    
    //方法2
    const props = defineProps({ 
        foo: { type: String, required: true, default: '默认值' }, 
        bar: Number
    })
    
    //方法3-对于运行时声明,我们可以使用 `PropType` 工具类型
    interface Book {
        data?: number[]
    }
    const props = defineProps({ book: Object as PropType<Book> })
    
    //方法4-基于类型的声明。推荐:弊端:不能设置默认值(使用withDefaults解决)
    // 通过泛型参数来定义 props 的类型通常更直接, 这被称之为“基于类型的声明”。编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。
    interface Props {
        data?: number[]
    }
    const props = defineProps<Props>();
    //设置默认值
    const props = withDefaults(defineProps<Props>(), {  data: () => [1, 2] })
    
    //`Props` 也可以从从另一个源文件中导入
    //import type { Props } from './foo'
    
    </script>
    
    <script setup lang="ts">
    // 运行时
    const emit = defineEmits(['change', 'update'])
    
    // 基于选项
    const emit = defineEmits({
      change: (id: number) => {
        // 返回 `true` 或 `false`
        // 表明验证通过或失败
      },
      update: (value: string) => {
        // 返回 `true` 或 `false`
        // 表明验证通过或失败
      }
    })
    
    // 基于类型
    const emit = defineEmits<{
      (e: 'change', id: number): void
      (e: 'update', value: string): void
    }>()
    
    // 3.3+: 可选的、更简洁的语法
    const emit = defineEmits<{
      change: [id: number]
      update: [value: string]
    }>()
    </script>
    
        //将事件传递出去
    emit('change', { id: 1});
    emit('update', { value: '1111'});
    

defineExpose()

  • 内置函数,无需import导入,直接使用。Vue3中的setup默认是封闭的,如果想要使用ref或者 $parent 获取到的组件的的变量或函数,被访问的组件须使用defineExpose将属性和方法暴露出去。使用方式参考获取DOM

获取DOM

  • 官网Api
  • 注意:Vue3中,移除了 $children 属性
  • ref
  • $parent
  • $root
<!--父组件parent.vue -->
<template>
    <child ref="childRef"></child>
    <div ref="divEl"></div>
</template>
<script setup lang="ts">
import child from './components/child.vue';
 // 我用了自动导入,不需要引getCurrentInstance
import { getCurrentInstance, type ComponentInternalInstance, unref } from 'vue';

//方法一(常用推荐):
//typeof P 是获取到类,InstanceType<类>是拿到类的实例,一个是类一个是实例不一样
//为了获取组件的类型,我们首先需要通过 `typeof` 得到其类型,再使用 TypeScript 内置的 `InstanceType` 工具类型来获取其实例类型:

//获取组件。这个变量名和 DOM 上的 ref 属性必须同名,会自动形成绑定。变量名不能和组件名同名,即chilRef不能命名为child
let childRef = ref<InstanceType<typeof child> | null>(null);  

//获取dom
let divEl = ref<HTMLDivElement | null>(null)

//方法二:(不推荐)
//这样写会标红:类型“ComponentInternalInstance | null”上不存在属性“proxy”。使用类型断言或非空断言
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
//或
const { proxy } = getCurrentInstance()!;

let parentNum = ref(1)

onMounted(() => {
    console.log(divEl.value);  //直接拿到dom本身
    //或者
    console.log(unref(divEl))
   
    console.log(childRef.value?.msg);   //.value的方式调用子组件的数据和方法(defineExpose暴露)
    childRef.value?.open();
    
    console.log(proxy?.$refs);   //proxy对象{childRef: Proxy(Object), divEl: div}
});
defineExpose({
    parentNum
})

</script>
//子组件child.vue
<script setup lang="ts">
import type { ComponentInternalInstance } from 'vue'
let msg: string = '111';
const open = function() {
    console.log(222);
    
}
const { proxy } = getCurrentInstance()!;
onMounted(() => {
    //标红:类型“ComponentPublicInstance”上不存在属性“parentNum”
    console.log('parent', proxy?.$parent?.parentNum);
})
defineExpose({
    msg,
    open
})
</script>

注意: 如果ref在v-for里,将会是一个数组,这里和vue2一样。使用childRef.value[0].msg\

<!--  v-for中使用ref -->
<template>
 <a-timeline>
     <a-timeline-item ref="itemRefs" v-for="item in dataList" :key="item.value">
     </a-timeline-item>
 </a-timeline>
</template>
<script setup lang="ts">
  import { ref, onMounted } from 'vue';
  const itemRefs = ref<any[]>([]);
  onMounted(() => {
      //第一个a-timeline-item的dom
      console.log(itemRefs.value[0]?.$el)
      //第二个timeline-item的dom
      console.log(itemRefs.value[1]?.$el)
  })
</script>

useAttrs()

  • 包含了父组件传递过来的所有属性(子组件内没有被definePropsdefineEmits声明的),包括 classstyle 以及事件(相当于vue2中相当于listeners)。在vue2中,listeners)。在vue2中,attrs 是接到不到 class,style, 事件
//parent.vue
<template>
  <child
    foo="222"
    foo2="333"
    class="child"
    :style="{}"
    @test="handleTest"
    @test2="handleTest"
  ></child>
</template>
<script setup lang="ts">
function handleTest() {}
</script>
//child.vue
<template>
    <div></div>
</template>
<script setup lang="ts">
    const props = defineProps(['foo2'])
    const emits = defineEmits(['test2'])
    console.log(props); //{foo2: '333'}

    const attrs = useAttrs()
    console.log(attrs);  // {foo: '222', class: 'child', style: {…}, onTest: f handleTest()}
</script>

全局注册

  • vue2

    //vue2中
    Vue.prototype.$axios = xxx
    
    //使用
    this.$axios
    
  • vue3 扩展全局属性

    //main.ts
    import { createApp } from 'vue'
    import App from './App.vue'
    const app = createApp(App)
    
    app.config.globalProperties.name = '猪八戒'
    app.mount('#app')
    
    <script setup lang="ts">
    import type { ComponentInternalInstance } from 'vue';
    //这两都很少用
    // proxy 就是当前组件实例,可以理解为组件级别的 this,没有全局的、路由、状态管理之类的
    const { proxy, appContext  } = getCurrentInstance() as ComponentInternalInstance;
    
    //global 就是全局实例
    const global = appContext.config.globalProperties
    
    console.log(global.name) 
    console.log(proxy?.name) //会标红
    </script>
    

异步组件

## 异步组件,路由懒加载

设置组件名称

zhuanlan.zhihu.com/p/481640259

  1. 在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,即使是在配合 <KeepAlive> 使用时也无需再手动声明。不写index.vue,写一个具象的组件名称

  2. 再加个平级的script标签(注意:两个script标签使用的语言要同步,lang="ts")

    <script lang="ts" setup>
    </script>
    <script lang="ts">
      export default {
        name: 'draft',
        inheritAttrs: false,
        customOptions: {},
      };
    </script>
    
  3. defineOptions 这个宏可以用来直接在 <script setup> 中声明组件选项,而不必使用单独的 <script> 块
    官网解释

    <script lang="ts" setup>
        defineOptions({
            name:'draft'
        })
    </script>
    
  4. 利用插件

    //会报错[vueSetupExtend不是一个函数],删掉package.json 中的 type: module即可
    //vite.config.ts
    import { defineConfig, Plugin } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import vueSetupExtend from 'vite-plugin-vue-setup-extend-plus'
    
    export default defineConfig({
      plugins: [vue(), vueSetupExtend()],
    })
    
    //SFC
    <template>
      <div>hello world {{ a }}</div>
    </template>
    
    <script lang="ts" setup name="App" inheritAttrs="false">
      const a = 1
    </script>
    

watch的使用

官网

当我们需要在数据变化时执行一些“副作用”:如更改 DOM、执行异步操作(发起网络请求等),我们可以使用 watch 函数:

  • 监听基本数据类型

    const name = ref('猪八戒')
    // 监听 ref 属性 
    watch(name, (newValue, oldValue) => { 
    
    })
    
  • 监听对象

    const obj = reactive({
        a: 1,
        b: 2,
        c: {
            d: 1,
            e: 2
        },
        f: []
    })
    //法一:深层次,当直接侦听一个响应式对象时,侦听器会自动启用深层模式:
    //可以监听到obj.f = [1]和obj.c.d ++
    watch(obj, (newValue, oldValue) => {
    })
    
    //法二:深层次,必须写deep: true,不然浅层的也监听不到
    watch(() => obj, (newValue, oldValue) => {
    }, { deep: true })
    
    //法三:浅层, 监听不到obj.f = [1]和obj.c.d ++
    watch(() => { ...obj }, (newValue, oldValue) => {
    })
    
  • 监听对象的某个属性

    watch(() => obj.a, (newValue, oldValue) => {
    })
    
    //如果是对象的属性是引用数据类型,必须加deep: true才能监听到
    watch(() => obj.f, (newValue, oldValue) => {
    }, { deep: true })
    
  • 监听多个数据

    //法一:
    watch([() => obj.a, name], ([newA, newName], [oldA, oldName]) => {
    
    });
    //法二:
    watch(() => [obj.a, obj.b], (newValue, oldValue) => {
    })
    
    

watchEffect

官网

  • watch 默认是懒执行的:仅当数据源变化时,才会执行回调。可以传入immediate: true 选项来强制侦听器的回调立即执行
  • watchEffect的回调会立即执行(依赖收集),不需要指定 immediate: true。在执行期间,它会在同步执行过程中,自动追踪所有能访问到的响应式属性watch:手动指定依赖> > watchEffect:自动收集依赖(注意: 依赖太多各种坑)
  • watchEffect 无法访问侦听数据的新值和旧值
//侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 `todoId` 的引用发生变化时使用侦听器来加载一个远程资源
const todoId = ref(1)
const data = ref(null)

watch(todoId, async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
}, { immediate: true })

//使用watchEffect简化代码,我们不再需要明确传递 `todoId` 作为源值.
watchEffect(async () => { 
   const response = await fetch( `https://jsonplaceholder.typicode.com/todos/${todoId.value}`);
   data.value = await response.json() 
})
  • 使用哪种随你喜欢,如果不需要使用先前值(oldValue)并且希望立即执行就用watchEffect,可以少写一点代码。watch的自由度更高,watchEffect相当于封装了一层
  • 推荐在大部分时候用 watch 显式的指定依赖以避免不必要的重复触发,也避免在后续代码修改或重构时不小心引入新的依赖。watchEffect 适用于一些逻辑相对简单,依赖源和逻辑强相关的场景

停止监听器

  • 一个关键点是,侦听器必须用同步语句创建,这时它会在宿主组件卸载时自动停止。如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。
    // 它会自动停止 
    watchEffect(() => {}) 
    
    // ...这个则不会! 
    setTimeout(() => { watchEffect(() => {}) }, 100)
    
    //停止监听器
    const unwatch = watchEffect(() => {}) // ...当该侦听器不再需要时 unwatch()
    
    //第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。
    //该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
    watch(id, async (newId, oldId, onCleanup) => {
      const { response, cancel } = doAsyncWork(newId)
      // 当 `id` 变化时,`cancel` 将被调用,
      // 取消之前的未完成的请求
      onCleanup(cancel)
      data.value = await response
    })
    

computed

官网

//创建一个只读的计算属性 ref:
const count = ref(1) 
const plusOne = computed(() => count.value + 1) 

console.log(plusOne.value) // 2 
plusOne.value++ // 错误

//创建一个可写的计算属性
onst count = ref(1) 
const plusOne = computed({ 
    get: () => count.value + 1, 
    set: (val) => { count.value = val - 1 } 
}) 

plusOne.value = 1 console.log(count.value) // 0

计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要在 getter 中做异步请求或者更改 DOM

CSS

  • 样式穿透:Vue3 中不支持 /deep/ 或者 >>>写法, 支持:deep(.class)

    <style scoped> 
        .a :deep(.b) { /* ... */ } 
    </style>
    
  • css绑定js变量(v-bind):单文件组件的 <style> 标签支持使用 v-bind CSS 函数将 CSS 的值链接到动态的组件状态。

    <script setup>
    const theme = {
      color: 'red'
    }
    </script>
    
    <template>
      <p>hello</p>
    </template>
    
    <style scoped>
    p {
      color: v-bind('theme.color');
    }
    </style>
    

provie和inject

为 provide / inject 标注类型

//父级
const name = ref('猪八戒'); 
const changeName = (newName: string) => {
    name.value = newName;
};
provide('name', name);
provide('changeName', changeName);  //更改name的方法

//子级孙级
const name = inject('name') as string;  //使用类型断言,不然会有红色波浪线
//或
const name:string = inject('name')
const changeName = inject('changeName') as Fn;

vite中引入图片

Vite 官方中文文档 | 静态资源处理

说明: vite默认资源文件在assets文件夹打包后会把图片名加上 hash值。例如,imgUrl 在开发时会是 /img.png,在生产构建后会是 /assets/img.2d8efhg.png。行为类似于 Webpack 的 file-loader。区别在于导入既可以使用绝对公共路径(基于开发期间的项目根路径),也可以使用相对路径。

  1. 引入一张图片(非public文件夹)

    import imageA from '@/assets/images/image_a.png'
    
    //可行
    <img :src="imageA" />
    
    //以下方式均不可,线上找不着路径
    //直接引入并不会在打包的时候解析,导致开发环境可以正常引入,打包后却不能显示的问题。
    //引入的文件是/assets/img.png, 实际有效文件确是 /assets/img.2d8efhg.png
    <img :src="@/assets/images/image_a.png" />
    <img :src="/src/assets/images/image_a.png" />
    
    //报错require is not defind,因为 require是属于 Webpack 的方法
    <img :src="require('@/assets/images/image_a.png')" />
    
    
  2. 动态引入(非public文件夹)

    //我们首先想到的是,此法不行,vite无法解析
     <img :src="`/src/assets/images/${变量}.png`" />
     //下面可行
     <img :src="getImageUrl(变量)" />
     const getImageUrl = function (name) {
         return new URL(`../assets/images/${url}.png`, import.meta.url).href 
     }
    
  3. 直接将文件放到public里

image.png