前言
学习一下 vite2 + Vue3 + ts
的案例和知识点,从零到一实操练习,记录所遇的坑儿。
初始化
命令行创建 vite2 项目
npm init @vitejs/app
复制代码
启动项目时!
解决:将 node 版本 >= 12.0.0
我个人解决办法:
nvm use 12
跟着步骤创建完项目后,webStorm
不支持 Vue3 的语法,频繁报错,于是安装 VS Code
及其插件。
VS Code: 安装
Volar
,禁用Vetur
,会发生冲突。快速调用命令行窗口control
+~
在导入文件时,因 ts 的原因,需要
shims-vue.d.ts
文件默认声明 ts 导出变量才能 import 进来。
第一个 Demo
代码
<template>
<h1>{{msg}}</h1>
<div>默认的 count: {{state.count}} </div>
<div>默认的 count: {{state.double}} </div>
<button @click="increment">增加</button>
</template>
<script setup lang="ts">
import {reactive, computed, defineProps} from 'vue'
defineProps({
msg: String
})
type DState = {
count: number;
double: number;
}
const state: DState = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
</script>
复制代码
总结
- 了解 setup 语法糖
- 了解 defineProps
- 了解 reactive
- 了解 computed hook 的钩子函数
- 了解 ts 的声明
Ref 和 Reactice
代码
<template>
<h1>{{msg}}</h1>
---
<div>ref: {{count}}</div>
<div>reactive: {{state.count}} || {{obj.name}}</div>
<div>结构 reactive: {{name}} || {{age}}</div>
</template>
<script setup lang="ts">
import {reactive, computed, defineProps, ref, toRefs} from 'vue'
defineProps({
msg: String
})
// ref 语法糖
ref: count = 0
// 响应式数据,若不用语法糖则需要 count.value++
count++
// reactive 包裹单独的 ref
const state = reactive({
count
})
// 单独声明响应式对象数据
const obj = reactive({
name: "大大",
age: 21
})
// 把 reactice 响应式对象解构出来
const {name, age} = {...toRefs(obj)}
</script>
复制代码
总结
- 设置响应式数据:
ref
和reactive
- ref:声明原始数据类型
- reactive:声明复杂数据类型
- ref 的语法糖
ref: count = 0
===const count = ref(0)
- 若更改响应式数据,有语法糖的差异
- reactive 的声明
- 单独声明一个响应式对象数据
- 该 reactive 对包裹着一个个 ref,也就是将其 proxy 代理
- reactive 的解构:
...toRefs(obj)
templateref
代码
<template>
<h1>{{msg}}</h1>
---
<input type="text" ref="root" value="ref使用例子" />
</template>
<script setup lang="ts">
import {defineProps, onMounted, ref} from 'vue'
defineProps({
msg: String
})
// ref 会根据传入的数据来指向对应的数据类型
// 若传入一个 null,则会指向 DOM
const root = ref(null)
// 首次渲染挂载完成时,才能获取到真实的 DOM
onMounted(() => {
console.log("ref.null:", root.value);
})
</script>
复制代码
总结
- 了解 templateRef:其实就是 ref 指向了真实的 DOM
- ref 类型:会根据传入的数据来指向对应的数据类型
- 传入 0:
Ref(number)
- 传入 “”:
Ref(string)
- 传入 null:
Ref(null)
,指向的是 DOM 对象
- 传入 0:
- onMounted 生命钩子函数调用
- 在首次渲染挂载完成时,调用
Ref(null).value
来获取真实的 DOM 对象,这个就是 templateRef。
- 在首次渲染挂载完成时,调用
computed
代码
<template>
<h1>{{msg}}</h1>
---
<div>ref: {{count}}</div>
<div>computed: {{plusOne}}</div>
<div>computed(get/set): {{plusTwo}}</div>
</template>
<script setup lang="ts">
import {defineProps, computed} from 'vue'
defineProps({
msg: String
})
// ref 语法糖
ref: count = 1
ref: count2 = 0
// 计算属性
const plusOne = computed(() => count += 1)
// 查看计算属性生成对象
console.log("coumputed:", plusOne);
// get/set 重新设置计算属性
const plusTwo = computed({
get: () => count2 + 2,
set: (val) => count2 = val - 1
})
plusTwo.value = Math.random()
</script>
复制代码
总结
- computed 计算属性方法包裹响应式数据
- computed 生成对象
- 只读
- 生成新的 ref
- dirty 值为 false
- computed 利用
get/set
进行遮罩
注意:在书写案例期间,遇到一个有意思的地方,若将
const plusOne = computed(() => count += 1)
中的count += 1
换成count ++
也就是自加,结果为ref: 2
而computed: 1
。
watch
代码
<template>
<h1>{{msg}}</h1>
---
<div>state: {{state.count}}</div>
<div>state2: {{state2.count.count}}</div>
<div>ref1: {{ref1}}</div>
<div>ref2: {{ref2}}</div>
<button @click="add">添加</button>
</template>
<script setup lang="ts">
import {defineProps, reactive, watch} from 'vue'
defineProps({
msg: String
})
const state = reactive({
count: 0
})
// 复杂的响应式对象
const state2 = reactive({
count: {count: 1}
})
// 多个 ref
ref: ref1 = 2
ref: ref2 = 3
/**
* 参数:1.数据源, 2.回调, 3.参数配置
*/
watch(() => state.count, (newValue, oldValue) => {
console.log("state 监听的值:", newValue, oldValue);
}, {
immediate: true, // 立即监听
})
// 监听深拷贝
watch(() => state2.count, (newValue, oldValue) => {
console.log("state2 监听的值:", newValue.count, oldValue.count);
}, {
deep: true
})
// 监听多个 ref
watch(() => [ref1, ref2], ([newRef1, oldRef1], [newRef2, oldRef2]) => {
console.log('ref1:', newRef1, oldRef1);
console.log('ref1:', newRef2, oldRef2);
})
const add = () => {
state.count++
state2.count.count++
ref1++
ref2++
}
</script>
复制代码
总结
官方内容
watch
API 完全等同于组件侦听器 property。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
与 watchEffect 比较,watch
允许我们:
-
懒执行副作用;
-
更具体地说明什么状态应该触发侦听器重新运行;
-
访问侦听状态变化前后的值。
注意:当使用 watch 监听多个 ref 时,若没有使用回调函数监听数组,则会提示警告监听数据非响应式数据。
// 监听多个 ref watch([ref1, ref2], ([newRef1, oldRef1], [newRef2, oldRef2]) => { console.log('ref1:', newRef1, oldRef1); console.log('ref1:', newRef2, oldRef2); }) 复制代码
改正:
// 监听多个 ref watch(() => [ref1, ref2], ([newRef1, oldRef1], [newRef2, oldRef2]) => { console.log('ref1:', newRef1, oldRef1); console.log('ref1:', newRef2, oldRef2); }) 复制代码
Watch 和 Watcheffect
代码
<template>
<h1>{{msg}}</h1>
---
<div>watchEffect: {{num}}</div>
<button @click="add">增加</button>
</template>
<script setup lang="ts">
import {defineProps, ref, watchEffect, onMounted} from 'vue'
defineProps({
msg: String
})
ref: num = 0;
onMounted(() => {
console.log("onMounted");
})
/**
* 1. 不需要手动传入依赖
* 2. 不是 lazy,初始化时就执行分析依赖
* 3. 无法获取原始值
* 4. 适合操作异步操作
* 5. 第一个参数处理副作用
*/
const stop = watchEffect((onInvalidate) => {
console.log("watcherEffed 在 onMounted 之前调用", num);
// 清除副作用
onInvalidate(() => {
console.log("监听后的副作用");
})
}, {
// flush: "sync", // 调用的时机
onTrigger(e) { // 将在依赖项变更导致副作用被触发时被调用。
console.log('onTrigger', e);
},
onTrack(e) { // 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
console.log('onTrack', e);
}
})
const add = () => {
num++
}
// 暂停监听
stop()
</script>
复制代码
总结
- watchEffect:立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
- 优点
- 不需要手动传入依赖
- 不是 lazy,初始化时就执行分析依赖
- 无法获取原始值
- 适合操作异步操作
- 副作用刷新时机:在核心的具体实现中,组件的
update
函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件update
前执。- 如果需要在组件更新后重新运行侦听器副作用,我们可以传递带有
flush
选项的附加options
对象pre
:update 前(默认)post
:update 后sync
:强制效果始终同步触发。然而,这是低效的,不推荐使用
- 如果需要在组件更新后重新运行侦听器副作用,我们可以传递带有
- onInvalidate:侦听副作用传入的函数可以接收一个
onInvalidate
函数作入参,用来注册清理失效时的回调。 - 侦听器调试:在开发模式下
onTrack
和onTrigger
,可用于调试侦听器的行为。onTrack
只在响应式 property 或 ref 作为依赖项被追踪时被调用。onTrigger
将在依赖项变更导致副作用被触发时被调用。
- 显式调用返回值以停止侦听
stop()
Vue3 生命周期
代码
<template>
<h1>{{msg}}</h1>
---
<div>Vue3 生命周期</div>
<div>{{count}}</div>
</template>
<script setup lang="ts">
import {defineProps, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured} from 'vue'
defineProps({
msg: String
})
ref: count = 1
onBeforeMount(() => {
console.log('onBeforeMount');
})
onMounted(() => {
console.log('onMounted');
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate');
})
onUpdated(() => {
console.log('onUpdated');
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount');
})
onUnmounted(() => {
console.log('onUnmounted');
})
onErrorCaptured(() => {
console.log('onErrorCaptured');
})
setTimeout(() => {
count++
}, 100)
</script>
复制代码
总结
-
Vue3 与 Vue2 生命周期对比
Vue2--------------vue3 beforeCreate -> setup() created -> setup() beforeMount -> onBeforeMount mounted -> onMounted beforeUpdate -> onBeforeUpdate updated -> onUpdated beforeDestroy -> onBeforeUnmount destroyed -> onUnmounted activated -> onActivated deactivated -> onDeactivated errorCaptured -> onErrorCaptured 复制代码
-
使用场景
- setup() :开始创建组件之前,在beforeCreate和created之前执行。
- onBeforeMount() : 组件挂载到节点上之前执行的函数。
- onMounted() : 组件挂载完成后执行的函数。
- onBeforeUpdate(): 组件更新之前执行的函数。
- onUpdated(): 组件更新完成之后执行的函数。
- onBeforeUnmount(): 组件卸载之前执行的函数。
- onUnmounted(): 组件卸载完成后执行的函数。
- onActivated(): 被包含在中的组件,会多出两个生命周期钩子函数。被激活时执行。
- onDeactivated(): 比如从 A 组件,切换到 B 组件,A 组件消失时执行。
- onErrorCaptured(): 当捕获一个来自子孙组件的异常时激活钩子函数。
provide&inject
代码
// Parent.vue
<template>
<h1>{{msg}}</h1>
---
<div>provide&inject</div>
<Child />
<input @click="change" type="button" value="注入">
</template>
<script setup lang="ts">
import {defineProps, ref, provide} from 'vue'
import Child from './Child.vue'
defineProps({
msg: String
})
const themeRef = ref("dark")
provide("test", themeRef)
const change = () => {
themeRef.value = Math.random().toString()
}
</script>
复制代码
// Child.vue
<template>
<div>inject: {{theme}}</div>
</template>
<script lang="ts">
import {defineComponent, inject, Ref, watchEffect} from 'vue'
export default defineComponent({
name: "Child",
setup() {
const theme = inject("test", "")
watchEffect(() => {
console.log("主题修改", (theme as unknown as Ref<string>).value);
})
return {
theme
}
}
})
</script>
复制代码
总结
-
provide&inject:父组件可以作为其所有子组件的依赖项提供程序,而不管组件层次结构有多深。这个特性有两个部分:父组件有一个
provide
选项来提供数据,子组件有一个inject
选项来开始注入使用这个数据。 -
ref 语法糖对应 provide&inject 未响应支持
-
ref: theme = "dark" const change = () => { themeRef = Math.random().toString() } 复制代码
-
expose
代码
// Parent.vue
<template>
<h1>{{msg}}</h1>
---
<div>expose</div>
<Child ref="child"/>
<button @click="">点击变化</button>
</template>
<script setup lang="ts">
import {defineProps, ref, onMounted} from 'vue'
import Child from './Child.vue'
defineProps({
msg: String
})
ref: child = null
onMounted(() => {
(child as any)?.changeNum()
})
</script>
复制代码
<template>
<div>expose: {{num}}</div>
</template>
<script setup lang="ts">
import {useContext, ref} from 'vue'
const {expose} = useContext()
// let num = ref(0)
ref: num = 0
const changeNum = () => {
num = Math.random()
}
expose({
changeNum
})
</script>
复制代码
总结
- expose:个人理解就是 Vue2 中利用 ref 获取到子组件实例上的内容和操作
- 在子组件通过 useContext 钩子函数来获取 expose 函数,抛出方法到子组件实例上。
- useContext 代替了 setup(props, cox)
- 在父组件通过 ref 获取,需要在 onMounted 生命周期中获取 DOM 实例
- 注意获取时因利用
ref(null)
获取 DOM,故内容为 null 时,ts 会报错默认不存在 DOM 实例。 - 故设置类型为
any
或(theme as unknown as Ref<() => {changeNum() {}}>).changeNum()
- 注意获取时因利用
封装 ts 请求 hooks
代码
// src/hooks/useApi.ts
import {Ref, ref} from 'vue';
export type ApiRequest = () => Promise<void>
export interface UsableAPI<T> {
response: Ref<T | undefined>;
requst: ApiRequest;
}
function useApi<T>(url: RequestInfo, options?:RequestInit) {
const response:Ref<T | undefined> = ref();
const request = async () => {
const res = await fetch(url, options);
const data = await res.json();
response.value = data
};
return {
response,
request
}
}
export default useApi
复制代码
// src/hooks/index.ts
export {useApi} from './useApi'
复制代码
总结
泛型
概念
function identity(arg: any): any {
return arg;
}
复制代码
若使用any
类型会导致这个函数可以接收任何类型的arg
参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function identity<T>(arg: T): T {
return arg;
}
复制代码
我们给identity添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。 之后我们再次使用了 T
当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
我们把这个版本的identity
函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any
,它不会丢失信息,可以保持准确性,传入数值类型并返回数值类型。
使用
我们定义了泛型函数后,可以用两种方法使用。
第一种是,传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // type of output will be 'string'
复制代码
这里我们明确的指定了T
是string
类型,并做为一个参数传给函数,使用了<>
括起来而不是()
。
第二种方法更普遍。利用了类型推论 — 即编译器会根据传入的参数自动地帮助我们确定T的类型:
let output = identity("myString"); // type of output will be 'string'
复制代码
注意我们没必要使用尖括号(<>
)来明确地传入类型;编译器可以查看myString
的值,然后把T
设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
内容
- 封装请求方法 useApi,类型为泛型
<T>
- response 调用 vue 中的
ref
,并且类型是Ref<undefined>
- 因为可能是泛型,所以
Ref<T | undefined>
- 因为可能是泛型,所以
- 定义接口 interface:UsableAPI
- requst 的 类型 type:ApiRequest
- 最后返回 request 和 response
使用 ts 请求 hooks
代码
- 封装 ts 接口 modules
// src/modules/products.ts
// https://ecomm-products.modus.workers.dev
import { useApi } from "@/hooks";
import {Ref, ref} from 'vue'
export interface Product {
id: string;
title: string;
category: string;
description: string;
images: string[];
variants: Variant[];
price: string;
tags: Tag[];
}
export enum Tag {
Awesome = "Awesome",
Ergonomic = "Ergonomic",
Fantastic = "Fantastic",
Generic = "Generic",
Gorgeous = "Gorgeous",
Handcrafted = "Handcrafted",
Handmade = "Handmade",
Incredible = "Incredible",
Intelligent = "Intelligent",
Licensed = "Licensed",
Practical = "Practical",
Refined = "Refined",
Rustic = "Rustic",
Sleek = "Sleek",
Small = "Small",
Tasty = "Tasty",
Unbranded = "Unbranded",
}
export interface Variant {
id: string;
quantity: number;
title: string;
sku: string;
}
export type UsableProducts = Promise<{products: Ref<Product[] | undefined>}>
export default async function useProducts():UsableProducts {
const {response: products, request} = useApi<Product[]>('https://ecomm-products.modus.workers.dev')
const loaded = ref(false)
if (loaded.value === false) {
await request();
loaded.value = true
}
return {products};
}
复制代码
- 子组件调用
// HelloWorld.vue
<template>
<h1>{{msg}}</h1>
---
<ul>
<li v-for="product in products" :key="product.id">
{{product.title}}
</li>
</ul>
</template>
<script setup lang="ts">
import {defineProps, ref, onMounted} from 'vue'
defineProps({
msg: String
})
import useProducts from '@/modules/products';
const {products} = await useProducts()
</script>
复制代码
- 父组件调用
// App.vue
<template>
<img alt="Vue logo" src="./assets/logo.png"/>
<div>
<Suspense>
<template #default>
<HelloWorld msg="Hello Vue 3 + TypeScript + Vite"/>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</div>
</template>
<script lang="ts" setup>
import HelloWorld from '@/components/HelloWorld.vue'
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
复制代码
总结
- 根据请求 json 格式来快速转为指定类型的声明代码 – 「QuickType」
- 使用QuickType工具从json自动生成类型声明代码
- 上边代码中的
modules/products.ts
就是这样生成的
- 请求接口方法的 ts 声明要好好学学,尤其是需要套上 Promise。
- 如果需要封装个 ref 就要声明 Ref 类型
- 在组件中使用请求接口时,可以直接
await
,setup 语法糖支持。 - 倘若在组件的 setup 中使用异步,则需要在调用的地方套上
<Suspense>
标签。否则内容无法加载出来(即便是在控制台中已经拿到了数据)
后言
该 Vue3 + vite2 + ts
的入门级教程,学习自【2021年5月更新】Vue3+TypeScript从入门到实战全系列教程 – 2021.06.21
代码都是手打的噢,也是一步步进行实操练习,原本是想自己做个案例,只是实习的需要,得去熟悉学习下 react
了。那就跟 Vue
暂时先错过一下吧。