Vue3中 script setup 使用总结

Vue3.2发布时,<script setup>语法糖也跟着正式发布了,结合typescript来使用的话,书写Vue组件的体验比之前要舒服太多了。本文总结了一些在<script setup lang="ts">中常用的书写方式,希望可以为你在使用<script setup lang="ts">的时候提供帮助。

开发环境搭建

  • 推荐使用vite来快速初始化一个Vue3+Typescript项目,它会配置好一些开箱即用的配置
  • 推荐使用vscode,并且安装volar插件辅助开发
  • 推荐自定义一个代码片段,辅助开发
// vue.json
{
  "vue setup ts less": {
  	"prefix": "vstl",
  	"body": [
  		"<template>",
  		"    <div></div>",
  		"</template>",
  		"<script setup lang=\"ts\">",
  		"    ",
  		"</script>",
  		"<style lang=\"less\" scoped>",
  		"</style>",
  	],
  	"description": "vue3 setup ts less"
  }
}

顶层属性

任何在 script setup 声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用; 但仅仅是使用,由于不是响应式数据,没有双向绑定,所以不会在页面上同步更新。

<template>
  <div>
    <h1>{{ hello }}</h1>
    <p>
      {{ count }}
      <button @click="handleClick">add</button>  
    </p>
  </div>
</template>

<script lang="ts" setup>
// 如果只是单类型,则 vue 会自动推到,不用加类型
let hello: string | undefined = "hello, vue";

// vue 根据后面的值,自动推导。
let count = 0;

function handleClick(): void {
  console.log(count);
  count++;
}
</script>

响应式数据

  • ref: 用来给 基本数据类型 绑定响应式数据,访问时需要通过 .value 的形式, tamplate 不需要 .value,会被解析。
  • reactive: 用来给 复杂数据类型 绑定响应式数据,直接访问即可。
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>
      当前等级:{{ count }}
      <button @click="handleAdd">升级</button>  
    </p>

    <p>宠物:{{ userInfo.pet }}</p>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive } from "vue";

// 设置基本数据类型时,使用 ref
const title = ref<string>("响应式数据");

// 也可以根据设置的默认数据推导当前类型
let count = ref(0);

// 用户信息 --- reactive 用来绑定复杂数据类型
interface IUserInfo {
  name: string,
  pet: string | undefined,
  level: number
}
const userInfo = reactive<IUserInfo>({
  name: "zs",
  level: 0,
  pet: undefined,
})

// 升级
function handleAdd(): void {
  count.value++;

  // 复杂数据类型,不需要通过 .value 的形式获取数据
  userInfo.level = count.value;
  userInfo.pet = count.value >= 5 ? '马' : undefined; // 达到 5 级 解锁宠物
}
</script>

使用 watch 优化上面的例子

  1. vue 中结构 watch;
  2. watch 是一个函数,可以一次写多个,也可以一次性监听多个例子;
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>
      当前等级:{{ count }}
      <button @click="handleAdd">升级</button>  
    </p>

    <p>宠物:{{ userInfo.pet }}</p>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive, watch } from "vue";

// 设置基本数据类型时,使用 ref
const title = ref<string>("响应式数据");

// 也可以根据设置的默认数据推导当前类型
let count = ref(0);

// 用户信息 --- reactive 用来绑定复杂数据类型
interface IUserInfo {
  name: string,
  pet: string | undefined,
  level: number
}
const userInfo = reactive<IUserInfo>({
  name: "zs",
  level: 0,
  pet: undefined,
})

// 使用 watch 监听 count 的变化
watch(count, (nl, od) => {
  if (nl >= 5) {
  userInfo.level = count.value;
  userInfo.pet = count.value >= 5 ? '马' : undefined; // 达到 5 级 解锁宠物
  }
})

// 升级
function handleAdd(): void {
  count.value++;
}
</script>

使用 computed

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>
      当前等级:{{ count }}
      <button @click="handleAdd">升级</button>  
    </p>

    <p>宠物:{{ userInfo.pet }}</p>

    <p>{{ showAddictionPrevention }}</p>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive, watch, computed } from "vue";

// 设置基本数据类型时,使用 ref
const title = ref<string>("响应式数据");

// 也可以根据设置的默认数据推导当前类型
let count = ref(0);

// 用户信息 --- reactive 用来绑定复杂数据类型
interface IUserInfo {
  name: string,
  pet: string | undefined,
  level: number
}
const userInfo = reactive<IUserInfo>({
  name: "zs",
  level: 0,
  pet: undefined,
})

// 是否显示防沉迷
const showAddictionPrevention = computed(() => {
  if (count.value === 0) return "Welcome";
  if (count.value >= 5) return "登录时间过长";
  return '';
})

// watch: 监听一个属性;
watch(count, (nl, od) => {
  if (nl >= 5) {
    userInfo.level = count.value;
    userInfo.pet = count.value >= 5 ? '马' : undefined; // 达到 5 级 解锁宠物
  }
})

// 升级
function handleAdd(): void {
  count.value++;
}
</script>

使用生命周期

生命周期方法vue 结构后,它是一个函数,传入 callback 后即可执行。

<template>
  <div>
    <h1>{{ title }}</h1>
  </div>
</template>

<script lang="ts" setup>
import { ref, onMounted } from "vue";

// 设置基本数据类型时,使用 ref
const title = ref<string>("Props And Emits");

onMounted(() => {
  console.log("加载完成...");
});
</script>

props and emit

这是基于 ts 的写法,所以按照泛型的形式传入,如果不是 ts ,可以按照 vue 的语法进行实现。

<!-- Father.vue -->
<template>
  <div>
    <h1>{{ title }}</h1>
    <Child :count="count" @change="handleChange"/>
  </div>
</template>

<script lang="ts" setup>
import { ref, } from "vue";
// 子组件不需要注册,可以直接使用
import Child from "./Child.vue";

// 设置基本数据类型时,使用 ref
const title = ref<string>("Props And Emits");
const count = ref<number>(1);

function handleChange(val: number):void {
  count.value = val;
}
</script>

<!-- Child.vue -->
<template>
  <div>
    <h2>Child</h2>
    <p>{{ props.count }}</p>
    <button @click="handleAdd">add</button>
  </div>
</template>


<script lang="ts" setup>
import { defineProps, defineEmits } from "vue";

// 使用 
interface IProps {
  count: number
}
const props = defineProps<IProps>();

const emit = defineEmits<{
  (event: 'change', total: number): void
}>()


function handleAdd(): void {
  emit("change", props.count + 1);
}
</script>

使用响应式变量

<script setup lang="ts">中定义的响应式变量默认都是暴露给模板的

  • 使用refref会对传入的初始值做类型推导,如果是复杂的类型,还可以通过泛型参数来指定
<template>
    <div>{{ numberRef }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const numberRef = ref(0) // ===> Ref<number>
const stringRef = ref("") // ===> Ref<string>
interface IFoo {
    bar: string
}
const fooRef = ref<IFoo>() // ===> Ref<IFoo | undefined>
</script>
  • 使用reactive:Vue文档介绍了三种方式来为reactive声明类型
<template>
    <div>{{ book1.bar }}</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
interface IFoo {
    bar: string
}
// 第一种
const book1 = reactive<IFoo>({ bar: 'bar' })
// 第二种
const book2: IFoo = reactive({ bar: 'bar' })
// 第三种
const book3 = reactive({ bar: 'bar' }) as IFoo
</script>

使用computed

computed方法会自动推导出类型

  • 基础使用方式
<template>
    <div>{{ fooComputed?.bar }}</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
const numberRef = ref(0) // ===> Ref<number>
const numberComputed = computed(() => numberRef.value) // ===> ComputedRef<number>
interface IFoo {
    bar: string
}
const fooRef = ref<IFoo>() // ===> Ref<IFoo | undefined>
const fooComputed = computed(() => {
    return fooRef.value
}) // ===> ComputedRef<IFoo | undefined>
</script>
  • 可写的computed
<template>
    <div>{{ fooComputedWritable?.bar }}</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
interface IFoo {
    bar: string
}
const fooRef = ref<IFoo>() // ===> Ref<IFoo | undefined>
const fooComputedWritable = computed({
    get: () => {
        return fooRef.value
    },
    set: (value) => {
        fooRef.value = value
    }
}) // ===> WritableComputedRef<IFoo | undefined>
</script>

使用watchwatchEffect

直接引入watchwatchEffect方法就可以使用了

<template>
    <div>{{ numberRef }}</div>
</template>
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue';
const numberRef = ref(0) // ===> Ref<number>
interface IFoo {
    bar: string
}
const fooRef = ref<IFoo>() // ===> Ref<IFoo | undefined>

watchEffect(() => console.log(fooRef.value?.bar))
watch(numberRef, () => {
    console.log(`numberRef变化了`)
})
const stop = watch(fooRef, () => {
    console.log(`fooRef变化了`)
}, {
    deep: true
}) // 检查深度嵌套的对象或数组
stop(); // 停止侦听
</script>

使用nextTick

直接引入nextTick方法使用

<template>
    <div></div>
</template>
<script setup lang="ts">
import { nextTick } from 'vue';

nextTick(() => {
    // ...
})

// 还可以使用 async/await
async () => {
    await nextTick()
    // ....
}
</script>

使用其他组件

直接引入其他的组件,然后在模板中使用就可以了,引入的组件会自动注册的。需要注意的是,使用动态组件的时候,应该使用动态的 :is 来绑定

<template>
    <Foo></Foo>
    <foo-item></foo-item>
    <component :is="Foo" />
    <component :is="someCondition ? Foo : FooItem" />
</template>
<script setup lang="ts">
import Foo from "./Foo.vue"
import FooItem from "./FooItem.vue"
const someCondition = false
</script>

声明props

使用defineProps来声明props,分为不使用泛型的方式和使用泛型的方式,两者都可以同时声明类型和默认值

  • 非泛型方式:此时它接收和 props 选项相同的值。
<template>
    <div>
        <!-- 直接使用即可,不需要toRefs转换 -->
        {{ foo }}
    </div>
</template>
<script setup lang="ts">
import { toRefs } from 'vue';
interface ICustomType {
    foo: string,
    bar: string
}
const props = defineProps({
    foo: String, // 使用构造函数声明类型
    fooMultiTypes: [String, Number], // 多个类型
    fooCustomType: Object as () => ICustomType, // 自定义类型
    fooCustomTypeWithRequire: {
        type: Object as () => ICustomType,
        required: true
    }, // 自定义类型,必选
    fooCustomTypeWithDefault: {
        type: Object as () => ICustomType,
        default: () => {
            return {
                foo: "foo",
                bar: "bar"
            }
        }
    }, // 自定义类型,带默认值
})

// 1. 可以在模板<template>中使用声明的props,不需要用toRefs转换
// 2. 如果某一个值需要在setup中使用,则需要用toRefs转换下,然后把它解构出来
const {
    foo,  // ===> Ref<string | undefined> | undefined
    fooMultiTypes, // ===> Ref<string | number | undefined> | undefined
    fooCustomType, // ===> Ref<ICustomType | undefined> | undefined
    fooCustomTypeWithRequire, // ===> Ref<ICustomType>
} = toRefs(props)
</script>
  • 泛型方式:此时声明默认值,需要 withDefaults 编译器宏
<template>
    <div>
        <!-- 直接使用即可,不需要toRefs转换 -->
        {{ foo }}
    </div>
</template>
<script setup lang="ts">
import { toRefs } from 'vue';
interface ICustomType {
    foo: string,
    bar: string
}
const props = defineProps<{
    foo?: string,
    fooWithRequire: string,
    fooMultiTypes: string | number,
    fooCustomType?: ICustomType,
    fooCustomTypeWithRequire: ICustomType
}>()

// 泛型方式声明默认值,需要使用withDefaults 编译器宏
const propsWithDefault = withDefaults(
    defineProps<{
        fooCustomTypeWithDefault: ICustomType
    }>(),
    {
        fooCustomTypeWithDefault: () => {
            return {
                foo: "foo",
                bar: "bar"
            }
        }
    })

// 1. 可以在模板<template>中使用声明的props,不需要用toRefs转换
// 2. 如果某一个值需要在setup中使用,则需要用toRefs转换下,然后把它解构出来
const {
    foo,  // ===> Ref<string | undefined> | undefined
    fooWithRequire,  // ===> Ref<string>
    fooMultiTypes, // ===> Ref<string | number>
    fooCustomType, // ===> Ref<ICustomType | undefined> | undefined
    fooCustomTypeWithRequire, // ===> Ref<ICustomType>
} = toRefs(props)

const {
    fooCustomTypeWithDefault,  // ===> Ref<ICustomType>
} = toRefs(propsWithDefault)
</script>

声明emits

使用defineEmits来声明,它接收和 emits 选项相同的值。

<template>
    <div @click="emitsWithObject('bar', $event)"></div>
</template>
<script setup lang="ts">
interface IUserInput {
    email: string,
    password: string
}
// 数组方式
const emitsWithArray = defineEmits(["foo", "bar"])
// 对象方式
const emitsWithObject = defineEmits({
    // 带有验证函数,并且指定负载类型
    foo: (payload: IUserInput) => {
        if (payload.email && payload.password) {
            return true
        } else {
            console.warn(`Invalid submit event payload!`)
            return false
        }
    },
    // 仅指定负载类型
    bar: (evevnt: MouseEvent) => true
})
// 泛型方式
const emitsWithGeneric = defineEmits<{
    (e: 'foo', id: number): void
    (e: 'bar', value: string): void
}>()

// 触发事件
emitsWithArray("foo")
emitsWithObject("foo", { email: "bar@aa.com", password: 'bp' })
emitsWithGeneric("foo", 1)
</script>

递归组件

一个单文件组件可以通过它的文件名被其自己所引用,但是在有命名冲入的情况下,需要使用import 别名导入来避免冲突

// FooBar.vue
<template>
    <div>
        <!-- 一个单文件组件可以通过它的文件名被其自己所引用 -->
        <FooBar></FooBar>
        <foo-bar></foo-bar>

        <foo-bar-other></foo-bar-other>
    </div>
</template>
<script setup lang="ts">
// 使用 import 别名导入避免冲突
import { default as FooBarOther } from './others/FooBar.vue'
</script>

生命周期钩子使用

直接引入对应的生命周期钩子函数使用即可

<template>
    <div></div>
</template>
<script setup lang="ts">
import {
    onBeforeMount,
    onMounted,
    onBeforeUpdate,
    onUpdated,
    onBeforeUnmount,
    onUnmounted,
    onErrorCaptured,
    onRenderTracked,
    onRenderTriggered,
    onActivated,
    onDeactivated
} from "vue"

// 直接使用就好了
onMounted(() => {
    // ...
})
</script>

暴露组件属性

<script setup>中的变量默认是不会暴露出给其他组件的,使用defineExpose可以暴露需要其他组件可以访问的属性

  • Bar.vue中暴露属性,由于使用defineExpose暴露的属性类型暂时还没法被InstanceType<typeof Bar>这种方式提取出来有,具体的可以参照这个#4397,所以需要使用export手动的将暴露的类型导出。
<template>
  <div></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const a = 1
const b = ref(2)

const exposeValue = {
  a,
  b
}
defineExpose(exposeValue)
export type IExposeType = typeof exposeValue
</script>
  • Foo.vue中通过模板引用使用暴露的属性
<template>
    <bar ref="barRef"></bar>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import Bar, { IExposeType } from './Bar.vue';
const barRef = ref<IExposeType>();
const a = barRef.value?.a  // number | undefined
const b = barRef.value?.b  // Ref<number> | undefined
</script>

使用provide和inject

provideinject支持通过泛型来控制注入的变量的类型

  • Foo.vue中使用provide提供需要注入的依赖
<template>
    <div></div>
</template>
<script setup lang="ts">
import { provide } from 'vue';
export interface IUser {
    name: string,
    age: number
}
provide("name", "foo")
provide<IUser>("user", {
    name: "foo",
    age: 23
})
</script>
  • Bar.vue中使用inject引入注入的依赖
<template>
    <div></div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
import { IUser } from './Foo.vue';

const name = inject<string>("name") // ===> string | undefined
const user = inject<IUser>("user") // ===> IUser | undefined
</script>

此外,如果想要实现父子组件绑定可以参考:

https://www.jianshu.com/p/da07df7cfbbb

给TA打赏
共{{data.count}}人
人已打赏
前端/移动

一为导航添加中间功能模块

2022-2-19 19:40:02

手机数码

黑鲨4 Pro首发评测

2021-4-26 17:40:12

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索