前言:vue3+typescript+pinia学习
文章目录
- 一、vue3
- 1.组合式API
- 2.setup函数
- 3.reactive函数
- 4.ref函数
- 5.reactive 与 ref 的选择
- 6.setup语法糖
- 6.1.vue3+vite自动导入ref等组合式API插件 `unplugin-auto-import`
- 7.computed函数
- 7.watch函数
- 8.生命周期函数
- 9.ref获取DOM元素
- 10.ref操作组件-defineExpose
- 11.父传子defineProps函数
- 12.子传父-defineEmits函数
- 13.跨级组件通讯provide与inject函数
- 14.保持响应式-toRefs函数
- 15.综合案例
- 二、Typescript
- 1.Typescript起步
- 1.1 Typescript介绍
- 1.2 TypeScript 编译
- 2.TypeScript 核心
- 2.1 原始类型
- 2.2 数组类型
- 2.3 联合类型
- 2.4 类型别名
- 2.5 函数类型
- 2.5.1 基本使用
- 2.5.2 void 类型
- 2.5.3 可选参数
- 2.6 对象类型
- 2.6.1 拓展用法
- 2.7 接口 interface
- 2.7.1 interface 继承
- 2.7.2 type 交叉类型
- 2.7.3 interface 和 type 的相同点和区别
- 2.8 类型推断
- 2.9 字面量类型
- 2.9.1 字面量类型介绍
- 2.9.2 字面量类型应用
- 2.10 any 类型
- 2.11 类型断言
- 2.11 泛型
- 2.11.1 泛型别名
- 2.11.2 泛型接口
- 2.11.3 泛型函数
- 3.TypeScript 应用
- 3.1TypeScript与Vue
- 3.1.1 defineProps的TS写法
- 3.1.2 defineEmits的TS写法
- 3.1.3 ref的TS写法
- 3.1.4 reactive的TS写法
- 3.1.5 computed和TS
- 3.1.6 事件处理与TS
- 3.1.7 Template Ref与TS
- 3.1.8 非空断言
- 3.2 TypeScript类型声明文件
- 3.2.1 基本介绍
- 3.2.2 内置类型声明文件
- 3.2.3 第三方库类型声明文件
- 3.2.4 自定义类型声明文件
- 3.2.4.1 共享类型
- 4.TypeScript 案例(头条)
- 5. TS在vue3中使用总结
- 三、Pinia
- 3.1 Pinia介绍
- 3.2 Pinia使用步骤(以vue3为例)
- 3.3 storeToRefs的使用
- 3.4 用Pinia改造头条
一、vue3
1.组合式API
介绍:什么是组合式API,组合式API的特点
Vue3提供两种组织代码逻辑的写法:
- 通过data、methods、watch 等配置选项组织代码逻辑是选项式API写法
- 所有逻辑在setup函数中,使用 ref、watch 等函数组织代码是组合式API写法
// 选项式API
<template>
<button @click="toggle">显示隐藏图片</button>
<img v-show="show" alt="Vue logo" src="./assets/logo.png" />
<hr />
计数器:{{ count }} <button @click="increment">累加</button>
</template>
<script>
export default {
data() {
return {
show: true,
count: 0,
};
},
methods: {
toggle() {
this.show = !this.show;
},
increment() {
this.count++;
},
},
};
</script>
// 组合式api
<template>
<button @click="toggle">显示隐藏图片</button>
<img v-show="show" alt="Vue logo" src="./assets/logo.png" />
<hr />
计数器:{{ count }} <button @click="increment">累加</button>
</template>
<script>
// ref 就是一个组合式API
import { ref } from 'vue';
export default {
setup () {
// 显示隐藏
const show = ref(true)
const toggle = () => {
show.value = !show.value
}
// 计数器
const count = ref(0)
const increment = () => {
count.value ++
}
return { show, toggle, count, increment }
}
};
</script>
总结:
- 在setup中通过vue提供的函数组织代码实现功能,就是组合式API写法。
- 组合式API有什么好处? 可复用,可维护
- ref 是不是一个组合式API? 是
2.setup函数
概念:
setup
函数是组合式API
的入口函数setup
函数是Vue3
特有的选项,作为组合式API的起点- 从组件生命周期看,它在
beforeCreate
之前执行 - 函数中
this
不是组件实例,是undefined
(今后在vue3的项目中几乎用不到this
, 所有的东西通过函数获取) - 如果数据或者函数在模板中使用,需要在
setup
返回
3.reactive函数
通常使用它定义对象类型的响应式数据
原因:以前在 data 函数中返回对象数据就是响应式的,而现在 setup 中返回对象数据不是响应式的
<template>
<div>
<p>姓名:{{state.name}}</p>
<p>年龄:{{state.age}} <button @click="state.age++">一年又一年</button></p>
</div>
</template>
<script>
// 1. 从vue中导入reactive函数
import { reactive } from "vue";
export default {
setup() {
// 2. 创建响应式数据对象
const state = reactive({ name: 'tom', age: 18 })
// 3. 返回数据
return { state }
}
};
</script>
4.ref函数
通常使用它定义响应式数据,不限类型
注意:使用 ref 创建的数据,js中需要 .value,template 中可省略
<template>
<div>
<p>
计数器:{{ count }}
<button @click="count++">累加1</button>
<!-- template中使用可省略.value -->
<button @click="increment">累加10</button>
</p>
</div>
</template>
<script>
// 1. 从vue中导入ref函数
import { ref } from "vue";
export default {
setup() {
// 2. 创建响应式数据对象
const count = ref(0);
const increment = () => {
// js中使用需要.value
count.value += 10;
};
// 3. 返回数据
return { count, increment };
},
};
</script>
5.reactive 与 ref 的选择
reactive
可以转换对象成为响应式数据对象,但是不支持简单数据类型。ref
可以转换简单数据类型为响应式数据对象,也支持复杂数据类型,但是操作的时候需要.value
。- 它们各有特点,现在也没有最佳实践,没有明显的界限,所有大家可以自由选择。
- 推荐用法:如果能确定数据是对象且字段名称也确定,可使用
reactive
转成响应式数据,这样可以省去.value
其他一概使用 ref 。
6.setup语法糖
作用:简化 setup 固定套路代码 ,让代码更简洁
使用 setup 有几件事必须做:
- 默认导出配置选项
- setup函数声明
- 返回模板需要数据与函数
// 不使用setup语法糖
<script>
export default { // 1. 默认导出配置选项
setup() { // 2. setup函数声明
const say = () => console.log('hi')
return { say } // 3. 返回模板需要数据与函数
}
}
</script>
// 使用setup语法糖)
<script setup>
const say = () => console.log('hi')
</script>
// 小案例
<script setup>
// 显示隐藏
const show = ref(true)
const toggle = () => {
show.value = !show.value
}
// 计数器
const count = ref(0)
const increment = () => {
count.value ++
}
</script>
<template>
<button @click="toggle">显示隐藏图片</button>
<img v-show="show" alt="Vue logo" src="./assets/logo.png" />
<hr />
计数器:{{ count }} <button @click="increment">累加</button>
</template>
6.1.vue3+vite自动导入ref等组合式API插件 unplugin-auto-import
使用方法:
- 安装:npm install -D
unplugin-auto-import
- 在vite.config.ts中配置
//1.先引入
import AutoImport from "unplugin-auto-import/vite"
export default defineConfig({
plugins: [
vue(),
//2.然后配置
AutoImport({
imports: ['vue'],
dts: 'src/auto-import.d.ts'
}),
]
})
7.computed函数
使用场景:当需要依赖一个数据得到新的数据,就使用计算属性
<script setup>
const list = ref([80,100,90,70,60])
const betterList = computed(()=>list.value.filter((item)=>item>=90))
setTimeout(()=>{
list.value.push(92,66)
},2000)
</script>
<template>
<div>
<p>分数{{list}}</p>
<p>优秀{{betterList}}</p>
</div>
</template>
7.watch函数
使用watch函数监听数据的变化
大致内容:
- 使用 watch 监听一个响应式数据
watch(数据, 改变后回调函数)
- 使用 watch 监听多个响应式数据
watch([数据1, 数据2, ...], 改变后回调函数)
- 使用 watch 监听响应式对象数据中的一个属性(简单)
watch(()=>数据, 改变后回调函数)
- 使用 watch 监听响应式对象数据中的一个属性(复杂),配置深度监听
watch(()=>数据, 改变后回调函数, {deep: true})
案例
// 1.使用 watch 监听一个响应式数据
<script setup>
const count = ref(0)
watch(count,()=>{
console.log('count改变了');
})
setTimeout(()=>{
count.value++
},2000)
</script>
<template>
<p>计数器:{{count}}</p>
</template>
// 2.使用 watch 监听多个响应式数据
<script setup>
const count = ref(0)
const user = reactive({
name: "tom",
info: {
gender: "男",
age: 18,
},
});
// 2. 监听多个响应式数据
// watch([数据1, 数据2, ...], 改变后回调函数)
watch([count, user], () => {
console.log("数据改变了");
});
// 2s改变数据
setTimeout(() => {
count.value++;
}, 2000);
// 4s改变数据
setTimeout(() => {
user.info.age++;
}, 4000);
</script>
<template>
<p>计数器:{{count}}</p>
<p>
姓名:{{ user.name }} 性别:{{ user.info.gender }} 年龄:{{ user.info.age }}
</p>
</template>
// 3.使用 watch 监听响应式对象数据中的一个属性(简单)
<script setup>
const user = reactive({
name: "tom",
info: {
gender: "男",
age: 18,
},
});
// 监听响应式对象数据的一个数据,简单类型
// watch(()=>数据, 改变后回调函数)
watch(() => user.name, () => {
console.log("数据改变了");
});
// 2s改变数据
setTimeout(() => {
user.name = 'jack';
}, 2000);
</script>
<template>
<p>
姓名:{{ user.name }} 性别:{{ user.info.gender }} 年龄:{{ user.info.age }}
</p>
</template>
// 4.使用 watch 监听响应式对象数据中的一个属性(复杂),配置深度监听
<script setup>
const user = reactive({
name: "tom",
info: {
gender: "男",
age: 18,
},
});
// 4. 监听响应式对象数据的一个数据,复杂类型
// watch(()=>数据, 改变后回调函数, {deep: true})
watch(() => user.info,() => {
console.log("数据改变了");
},
{
// 开启深度监听
deep: true,
}
);
// 2s改变数据
setTimeout(() => {
user.info.age = 60;
}, 2000);
</script>
<template>
<p>
姓名:{{ user.name }} 性别:{{ user.info.gender }} 年龄:{{ user.info.age }}
</p>
</template>
8.生命周期函数
常用: onMounted
组件渲染完毕:发请求,操作dom,初始化图表…
<script setup>
// 生命周期函数:组件渲染完毕
// 生命周期钩子函数可以调用多次
onMounted(()=>{
console.log('onMounted触发了')
})
onMounted(()=>{
console.log('onMounted也触发了')
})
</script>
<template>
<div>生命周期函数</div>
</template>
9.ref获取DOM元素
元素上使用 ref 属性关联响应式数据,获取DOM元素
步骤:
- 创建 ref
const hRef = ref(null)
- 模板中建立关联
<h1 ref="hRef">我是标题</h1>
- 使用
hRef.value
<script setup>
// 1.创建ref
const hRef = ref(null)
//3.改变DOM数据
const clickFn = () => hRef.value.innerText = '我不是标题'
</script>
<template>
<div>
// 2.在模板中关联ref
<h1 ref="hRef">我是标题</h1>
<button @click="clickFn">操作DOM</button>
</div>
</template>
10.ref操作组件-defineExpose
组件上使用 ref 属性关联响应式数据,获取组件实例
配合 defineExpose
暴露数据和方法,ref获取的组件实例才可以使用
- 使用
<script setup>
的组件是默认关闭的,组件实例使用不到顶层的数据和函数。 - 需要配合
defineExpose
暴露给组件实例使用,暴露的响应式数据会自动解除响应式。
11.父传子defineProps函数
注意:
- 如果只使用
defineProps
接收数据,这个数据只能在模板中渲染defineProps({ 数据 })
- 如果要在
script
中操作props
属性,应该接收返回值const props = defineProps({ 数据 })
12.子传父-defineEmits函数
- 子组件通过 defineEmits获取 emit 函数(因为没有this)
- 子组件通过 emit 触发事件,并且传递数据
- 父组件提供方法
- 父组件通过自定义事件的方式给子组件注册事件
13.跨级组件通讯provide与inject函数
通过provide和inject函数可以简便的实现跨级组件通讯
总结:
- provide和inject是解决跨级组件通讯的方案
- provide 提供后代组件需要依赖的数据或函数
- inject 注入(获取)provide提供的数据或函数
官方术语:依赖注入
- App是后代组件依赖的数据和函数的提供者,Child是注入(获取)了App提供的依赖
14.保持响应式-toRefs函数
在使用reactive创建的响应式数据被展开或解构的时候使用toRefs保持响应式
<script setup>
const user = reactive({ name: "tom", age: 18 });
const { name, age } = toRefs(user)
</script>
<template>
<div>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }} <button @click="age++">一年又一年</button></p>
</div>
</template>
15.综合案例
<script setup>
import { onMounted,ref } from 'vue';
import axios from 'axios'
//可以看出vue3中的数据和逻辑是写在一起的,项目复杂后很方便
//定义学生列表数组
const list = ref([])
// 1.获取数据
const getList = async()=>{
const res = await axios.get('/list')
console.log(res);
list.value = res.data
}
onMounted(() => {
getList()
})
//2.删除数据
const delClick = async (id) => {
await axios.delete("/del?id=" + id)
getList()
}
</script>
<template>
<div class="app">
<el-table :data="list">
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column label="姓名" width="150" prop="name"></el-table-column>
<el-table-column label="籍贯" prop="place"></el-table-column>
<el-table-column label="操作" width="100" >
<template v-slot="row">
<el-button type="primary" link @click="delClick(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<style>
.app {
width: 980px;
margin: 100px auto 0;
}
</style>
二、Typescript
1.Typescript起步
1.1 Typescript介绍
TS 是 JS 的超集,支持了JS 语法和扩展了类型语法。
注意:TS 需要编译才能在浏览器运行。
TS作用:在编译时进行类型检查提示错误
// JavaScript代码
// 没有明确的类型
let age = 18
// TypeScript代码
// 有明确的类型,可以指定age是number类型(数值类型)
let age: number = 18
- TS 是静态类型的编程语言,代码会先编译再执行,在代码编译期间做类型的相关检查,如果有问题编译是不通过的,也就暴露出了问题。
- 配合 VSCode 等开发工具,TS 可以提前在编写代码的时候发现问题,找出错误。
1.2 TypeScript 编译
使用tsc 编译 ts 代码
// npm 安装
npm i -g typescript
// yarn 安装
yarn global add typescript
// 查看版本
tsc -v
2.TypeScript 核心
TS 常用类型:
- JS 已有类型
简单类型:number string boolean null undefined
复杂类型:对象 数组 函数
- TS 新增类型
联合类型、自定义类型(类型别名)、接口、元组、字面量类型、枚举、void、any、泛型 等
2.1 原始类型
- 使用简单,完全按照 JS 的类型来书写即可
let age: number = 18;
let myName: string = '呵呵';
let isLoading: boolean = false;
let nullValue: null = null;
let undefinedValue: undefined = undefined;
2.2 数组类型
- 写法 1
let numbers: number[] = [1, 3, 5];
- 写法 2
let strings: Array<string> = ['a', 'b', 'c'];
推荐使用: 类型[]
写法
2.3 联合类型
- 通过联合类型将多个类型合并为一个类型
- 类型与类型之间使用 | 连接,代表类型可以是它们当中的其中一种,这种类型叫联合类型
let arr: (number | string)[] = [1, 'a', 3, 'b'];
- 思考:
// 这是什么类型?
let arr: number | string[];
arr=[1,'a'] //报错
arr=['a'] //不报错
arr=1 //不报错
// 表示arr是数字或者字符串类型的数组
2.4 类型别名
- 语法:
type 类型别名 = 具体类型
- 定义类型别名,遵循大驼峰命名规范,类似于变量
type CustomArr = (number | string)[];
let arr: CustomArr = [1, 'a', 4];
- 使用场景:当同一类型被多次使用时,可以通过类型别名,简化该类型的使用
type CustomArr = (number | string)[];
let arr: CustomArr = [1, 'a', 4];
let arr2: CustomArr = [2, 'b', 8];
2.5 函数类型
2.5.1 基本使用
- 给函数指定类型,其实是给 参数 和 返回值 指定类型。
两种写法:
- 在函数基础上 分别指定 参数和返回值类型
// 函数声明
function add(num1: number, num2: number): number {
return num1 + num2;
}
// 箭头函数
const add2 = (a:number,b:number):number => a+b
- 使用类型别名 同时指定 参数和返回值类型(不使用于声明式函数)
const add3:(a:number,b:number)=>number =(a,b)=>a+b
//用type类型别名简化写法:
type AddFn = (a: number, b: number) => number;
const add3: AddFn = (a, b) => a+b
2.5.2 void 类型
- 如果函数没有返回值,定义函数类型时返回值类型为
void
const say = (): void => {
console.log('hi');
};
- 如果函数没有返回值,且没有定义函数返回值类型的时候,默认是
void
const say = () => {
console.log('hi');
};
注意:
- 在
JS
中如果没有返回值,默认返回的是undefined
- 但是
void
和undefined
在TypeScript
中并不是一回事 - 如果指定返回值类型是
undefined
那返回值必须是undefined
const add = (): undefined => {
return undefined;
};
2.5.3 可选参数
- 如果函数的参数,可以传也可以不传,这种情况就可以使用 可选参数 语法,参数后加
?
即可 - 注意:必选参数不能位于可选参数后
(start?: number, end: number)
这样是不行的
const fn = (n?: number) => {
// ..
};
fn(); // 不会报错
fn(10);
2.6 对象类型
- TS 的对象类型,其实就是描述对象中的属性和方法的类型,因为对象是由属性和方法组成的。
- 属性怎么写类型?
属性名: 类型
- 方法怎么写类型?
方法名(): 返回值类型
// 空对象
let person: {} = {};
// 有属性的对象
let person: { name: string; age: number } = {
name: '同学',
age: '18'
};
// 包含函数的对象
// 语法:函数名称():返回值类型
let obj: { sayHi(): void } = {
sayHi() {},
};
2.6.1 拓展用法
- 函数使用箭头函数类型
let obj: { sayHi:() => void } = {
sayHi() {},
};
- 对象属性可选
const axios = (config: { url: string; method?: string }) => {};
- 使用类型别名
// {} 会降低代码可阅读性,建议对象使用类型别名
// const axios = (config: { url: string; method?: string }) => {};
type Config = {
url: string;
method?: string;
};
const axios = (config: Config) => {};
2.7 接口 interface
-
接口声明是命名对象类型的另一种方式
-
interface
后面是接口名称,和类型别名的意思一样。 -
接口的每一行只能有一个属性或方法,每一行不需要加分号。
// 通过interface定义对象类型
interface Person {
name: string
age: number
sayHi: () => void;
}
// 使用类型
let person: Person = {
name: 'jack',
age: 19,
sayHi() {},
};
2.7.1 interface 继承
- 有两个接口,有相同的属性或者函数,如何提高代码复用?
interface Point2D {
x: number;
y: number;
}
interface Point3D {
x: number;
y: number;
z: number;
}
- 相同的属性或展示可以抽离出来,然后使用
extends
实现继承复用 - 接口继承的语法:
interface 接口A extends 接口B {}
- 继承后接口A拥有接口B的所有属性和函数的类型声明
interface Point2D {
x: number;
y: number;
}
// 继承 Point2D
interface Point3D extends Point2D {
z: number;
}
// 继承后 Point3D 的结构:{ x: number; y: number; z: number }
2.7.2 type 交叉类型
- 使用
&
可以合并连接的对象类型,也叫:交叉类型
// 使用 type 来定义 Point2D 和 Point3D
type Point2D = {
x: number;
y: number;
};
// 使用 交叉类型 来实现接口继承的功能:
// 使用 交叉类型 后,Point3D === { x: number; y: number; z: number }
type Point3D = Point2D & {
z: number;
};
2.7.3 interface 和 type 的相同点和区别
- 类型别名和接口非常相似,在许多情况下,可以在它们之间自由选择。
- 不同点:
type
不可重复定义;
type Person = {
name: string;
};
// 标识符“Person”重复 Error
type Person = {
age: number;
};
interface
重复定义会合并
interface Person {
name: string;
}
interface Person {
age: number;
}
// 类型会合并,注意:属性类型和方法类型不能重复定义
const p: Person = {
name: 'jack',
age: 18,
};
2.8 类型推断
在 TS 中存在类型推断机制,在没有指定类型的情况下,TS 也会给变量提供类型。
建议:
1.开发时,能省略类型注解的地方就省略,利用TS推断能力,提高开发效率。
2.在你还没有熟悉 ts 类型的时候建议都加上类型
3.如果不知道类型怎么写,可以把鼠标放至变量上,通过 Vscode 提示看到类型
- 声明变量并初始化时
// 变量 age 的类型被自动推断为:number
let age = 18;
- 决定函数返回值时
// 函数返回值的类型被自动推断为:number
const add = (num1: number, num2: number) => {
return num1 + num2;
};
2.9 字面量类型
2.9.1 字面量类型介绍
- 使用 js字面量 作为变量类型,这种类型就是字面量类型
// : 'jack' 是字面量类型
let name: 'jack' = 'jack';
// : 18 是字面量类型
let age: 18 = 18;
// 报错:不能将类型“19”分配给类型“18”
age = 19;
- 思考:这两个变量的类型是什么?
let str1 = 'Hello TS';
const str2 = 'Hello TS';
// 通过类型推断发现,str1 类型是 string , str2 类型是 Hello TS
// 原因:str2 是 const 声明的,值只能是 Hello TS,所以类型只能是 Hello TS
2.9.2 字面量类型应用
- 字面量类型配合联合类型来使用,表示:一组明确的可选的值
// 使用自定义类型:
type Car = 'bc' | 'bm' | 'ad'
function pickCar(car: Car) {
console.log(car)
}
// 调用函数时,会有类型提示:
// 参数pickCar的值只能是 bc/bm/ad中的任意一个
pickCar('bc')
2.10 any 类型
any
类型的作用是逃避 TS 的类型检查any
的使用越多,程序可能出现的漏洞越多,因此不推荐使用any
类型,尽量避免使用。
// 当变量的类型指定为 any的时候,不会有任何错误,也不会有代码提示,TS会忽略类型检查
let obj: any = { age: 18 }
obj.bar = 100
obj()
const n: number = obj
2.11 类型断言
- 有时候你会比
TS
更加明确一个值的类型,此时,可以使用类型断言来指定更具体的类型 - 使用
as
关键字实现类型断言,as
后面的类型是一个更加具体的类型
// 假设<img src="xxx" id="img"/>
const img = document.getElementById('img')
img?.src // src会报错:类型“HTMLElement”上不存在属性“src”
// 进行类型断言
const img = document.getElementById('img') as HTMLImageElement
img?.src // 不报错了
2.11 泛型
泛型:是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
- 泛型:定义类型别名后加上
<类型参数>
就是泛型语法, 使用的时候传入具体的类型即可 <T>
是一个变量,可以随意命名,建议遵循大驼峰即可。- 和
类型别名
配合,在类型别名后加上泛型语法,然后类型别名内就可以使用这个类型参数 - 泛型可以提高类型的复用性和灵活性
2.11.1 泛型别名
- 泛型在
类型别名type
上的应用
// 用户信息返回数据类型
// 可以看到两个对象的msg和code类型相同,怎样提高复用性呢?
type UserResponse = {
msg:string
code:number
data:{
name:string
age:number
}
}
type GoodsResponse = {
msg:string
code:number
data:{
id:number
goodsName:string
}
}
// 使用泛型提高复用性
type Data<T> = {
msg:string
code:number
data:T
}
type UserResponse = Data<{name:string;age:number}>
type GoodsResponse = Data<{id:number;goodsName:string}>
// 使用类型别名type提高代码可读性
type Data<T> = {
msg:string
code:number
data:T
}
type User = {
name:string
age:number
}
type UserResponse = Data<User>
2.11.2 泛型接口
- 泛型在
接口interface
上的应用 - 在接口名称的后面添加
<类型变量>
,那么,这个接口就变成了泛型接口,接口中所有成员都可以使用类型变量。
// 对象,获取单个ID函数,获取所有ID函数,ID的类型肯定是一致的,但是可能是数字可能是字符串
interface IdFnObj<T> {
id: () => T;
ids: () => T[];
}
const obj: IdFnObj<number> = {
id() { return 1 },
ids() { return [1, 2] },
};
//修改泛型类型
const obj: IdFnObj<string> = {
id() { return '1' },
ids() { return ['1', '2'] },
};
- 内置的泛型接口:
const arr = [1, 2, 3];
// TS有自动类型推断,其实可以看做:const arr: Array<number> = [1, 2, 3]
arr.push(4);
arr.forEach((item) => console.log(item));
2.11.3 泛型函数
- 语法:函数名称后加上
<T>
, T是类型参数,是个类型变量,命名建议遵循大驼峰即可。 - 好处:让函数可以支持不同类型复用,且保证类型是安全的。
// 函数的参数是什么类型,返回值就是什么类型
function getId<T>(id: T): T {
return id
}
let id1 = getId<number>(1)
// TS会进行类型推断,参数的类型作为泛型的类型 getId<string>('2')
let id2 = getId('2')
3.TypeScript 应用
3.1TypeScript与Vue
typescript 配合 Vue3 composition-api 使用
3.1.1 defineProps的TS写法
- vue3基础写法
// defineProps 的基本使用
const props = defineProps({
money: {
type: Number,
required: true
},
car: {
type: String,
required: false,
default: '宝马'
}
})
- vue3和TypeScript的写法
// defineProps 通过泛型参数来定义 props 的类型通常更直接:
const props = defineProps<{
money: number
car?: string
}>()
- 如果需要给 props 设置默认值,需要使用
withDefaults
函数
const props = withDefaults(defineProps<{
money: number;
car?: string;
}>(),{
car: '宝马'
})
- 上面写法太笨拙,可以使用
响应式语法糖
解构 +defineProps
就行:
const { money, car = "宝马" } = defineProps<{
money: number
car?: string
}>();
- 注意:目前需要
显式地选择开启
,因为它还是一个实验性特性。
// 在vite.config.ts中
export default defineConfig({
plugins: [
vue({
reactivityTransform: true,
}),
],
});
3.1.2 defineEmits的TS写法
defineEmits
的基本用法:
const emit = defineEmits(['changeMoney', 'changeCar'])
defineEmits
通过泛型参数来定义,可以实现更细粒度的校验:
const emit = defineEmits<{
(e: 'changeMoney', money: number): void
(e: 'changeCar', car: string): void
}>()
3.1.3 ref的TS写法
- 如果是简单类型,推荐使用类型推导
// const money = ref<number>(10)
const money = ref(10)
- 如果是复杂类型,推荐指定泛型
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id">{{item.name}}</li>
</ul>
</div>
</template>
<script setup lang='ts'>
import {ref} from 'vue'
// 类型别名: type类型别名 = 具体类型
// 数组类型:类型+[]写法
type Todo = {
id:number
name:string
done:boolean
}[]
const list = ref<Todo>([])
// 模拟从后台获取数据
// 复杂数据一般是后台返回数据,默认值是空,无法进行类型推导。
setTimeout(() => {
list.value = [
{id:1,name:'吃饭',done:false},
{id:2,name:'睡觉',done:true},
]
}, 1000)
3.1.4 reactive的TS写法
- 默认值属性是固定的,推荐使用类型推导
// 推导得到的类型:{ title: string }
const book = reactive({ title: 'Vue3' })
- 根据默认值推导不出我们需要的类型,推荐使用接口或者类型别名给变量指定类型
官方:不推荐使用
reactive()
的泛型参数,因为底层和ref()
实现不一样。
// 我们想要的类型:{ title: string, year?: number }
type Book = {
title: string
year?: number
}
const book: Book = reactive({ title: 'Vue3' })
book.year = 2022
3.1.5 computed和TS
computed()
会从其计算函数的返回值上推导出类型
// 类型推导
<script setup lang='ts'>
import { ref, computed } from 'vue'
const count = ref(100);
// 200(数字)
const doubleCount = computed(() => count.value * 2);
// 200.00(字符串)
// toFixed()方法四舍五入,返回值为字符串
const doubleMoney = computed<string>(() => (count.value * 2).toFixed(2));
</script>
- 可以通过泛型参数显式指定类型
const doubleMoney = computed<string>(() => (count.value * 2).toFixed(2));
3.1.6 事件处理与TS
- 不加类型,event默认是any,类型不安全
<template>
<input type="text" @change="handleChange" />
</template>
<script setup lang="ts">
// 提示:参数“event”隐式具有“any”类型。
const handleChange = (event) => {
console.log(event.target.value)
}
</script>
- 处理类型
<template>
// 鼠标触摸 @change,显示 (payload:Event) => void
// 说明传参类型为Event,Event是TS的内置类型
<input type="text" @change="changeFn" />
</template>
<script setup lang="ts">
// 参数“event”隐式具有“any”类型。
const changeFn = (event:Event) => {
// document.querySelector('input')看出event.target的类型为HTMLInputElement
// 类型断言为HTMLInputElement
console.log((event.target as HTMLInputElement).value)
}
</script>
3.1.7 Template Ref与TS
模板 ref
需要通过一个显式指定的泛型参数,建议默认值null
<template>
<input ref="input" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 使用泛型确定DOM的类型
const input = ref<HTMLInputElement| null>(null)
onMounted(() => {
// DOM获取完毕,渲染焦点
// 为了严格的类型安全,有必要在访问el.value时使用可选链或类型守卫
el.value?.focus()
})
</script>
3.1.8 非空断言
- 可选链
<template>
<div>App组件</div>
<input type="text" ref="input" value="abc">
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const input = ref< HTMLInputElement | null >(null)
onMounted(()=>{
// 1.可选链
console.log(input.value?.value);
})
</script>
- 逻辑判断
if (input.value) {
console.log(input.value.value)
input.value.value = '123'
}
- 非空断言
//非空断言! 表示确定某个标识符是有值的,跳过ts在编译阶段对它的检测
console.log(input.value!.value)
input.value!.value = '123'
3.2 TypeScript类型声明文件
3.2.1 基本介绍
项目中安装的第三方库里面都是打包后的JS代码,但是我们使用的时候却有对应的TS类型提示,这是为什么呢?
因为在第三方库中的JS代码都有对应的 TS类型声明文件
TS 中有两种文件类型:.ts 文件 .d.ts 文件作用是啥?
1 .ts
文件:
- 既包含类型信息又可执行代码
- 可以被编译为
.js
文件,然后,执行代码 - 用途:编写程序代码的地方
2 .d.ts
文件:
-
只包含类型信息的类型声明文件
-
不会生成
.js
文件,仅用于提供类型信息,在.d.ts
文件中不允许出现可执行的代码,只用于提供类型 -
用途:为
JS
提供类型信息
3.2.2 内置类型声明文件
- 发现,使用数组时,数组所有方法都会有相应的代码提示以及类型信息
TypeScript 给 JS 运行时可用的所有标准化内置 API 都提供了声明文件,这个声明文件就是 内置类型声明文件
const strs = ['a', 'b', 'c']
// 鼠标放在 forEach 上查看类型
strs.forEach
- 可以通过 Ctrl + 鼠标左键(Mac:Command + 鼠标左键)来查看内置类型声明文件内容
- 查看 forEach 的类型声明,在 VSCode 中会自动跳转到
lib.es5.d.ts
类型声明文件中 - 像 window、document 等 BOM、DOM API 也都有相应的类型声明文件
lib.dom.d.ts
3.2.3 第三方库类型声明文件
- 常用的第三方库都有相应的类型声明文件,只是使用的方式不同而已。
情况1:库本身自带类型声明文件
- 比如:axios,安装后可查看
node_modules/axios
可发现对应的类型声明文件。 - 导入 axios 后就会加载对应的类型文件,提供该库的类型声明。
情况2:由 DefinitelyTyped 提供
- 比如:jquery,安装后导入,提示:需要安装
@types/jquery
类型声明包 - DefinitelyTyped 是一个 github 仓库,用来提供高质量 TypeScript 类型声明
- 当安装
@types/*
类型声明包后,TS 也会自动加载该类声明包,以提供该库的类型声明 - https://www.typescriptlang.org/dt/search 可以搜索是否有对应的
@types/*
3.2.4 自定义类型声明文件
3.2.4.1 共享类型
- 若多个
.ts
文件中都用到同一个类型,此时可以创建.d.ts
文件提供该类型,实现类型共享。
操作步骤:
- 创建
index.d.ts
类型声明文件。 - 创建需要共享的类型,并使用
export
导出(TS
中的类型也可以使用import/export
实现模块化功能)。 - 在需要使用共享类型的
.ts
文件中,通过import
导入即可(.d.ts
后缀导入时,直接省略)。
// src/types/data.d.ts
export type Person = {
id: number;
name: string;
age: number;
};
// App.vue
<script lang="ts" setup>
import { Person } from './types/data'
// 使用共享的类型 Person
const p: Person = {
id: 100,
name: 'jack',
age: 19
}
</script>
4.TypeScript 案例(头条)
- data.d.ts
// 提取类型到 types 目录
// 频道对象
export type ChannelItem = {
id: number;
name: string;
};
// 频道接口响应数据
export type ChannelResData = {
data: {
channels: ChannelItem[];
};
message: string;
};
// 文章对象
export type ArticleItem = {
art_id: string;
aut_id: string;
aut_name: string;
comm_count: number;
cover: {
cover: number;
images: string[];
};
is_top: number;
pubdate: string;
title: string;
};
// 文章接口响应数据
export type ArticleResData = {
data: {
pre_timestamp: string;
results: ArticleItem[];
};
message: string;
};
- App.vue
<template>
<ChannelNav :channelId="channelId" @changeChannel="channelId = $event"></ChannelNav>
<ArticleList :channelId="channelId"></ArticleList>
</template>
<script setup lang="ts">
import ChannelNav from './components/ChannelNav.vue';
import ArticleList from './components/ArticleList.vue';
import { ref } from 'vue';
// 频道ID,用来设置高亮
// 在App组件定义数据,后续在ArticleList组件使用(父传子)
const channelId = ref(1)
</script>
- ChannelNav.vue(频道导航组件)
<template>
<div class="channel-nav">
<nav class="list">
<a class="item" :class="{ active: channelId === item.id }" href="javascript:;"
v-for="item in getList" :key="item.id" @click="emit('changeChannel', item.id)">
{{ item.name }}
</a>
</nav>
</div>
</template>
<script setup lang='ts'>
import axios from 'axios'
import { onMounted, ref } from 'vue';
import { ChannelItem, ChannelResData } from '../types/data';
//父传子
defineProps<{ channelId: number }>()
//子传父
const emit = defineEmits<{
(e: 'changeChannel', id: number): void
}>()
// 获取文章列表
const getList = ref<ChannelItem[]>([])
onMounted(async () => {
// 使用TS的时候,axios()调用需要改为 axios.request(),才可以使用泛型
const res = await axios.request<ChannelResData>({
url: 'http://geek.itheima.net/v1_0/channels',
method:'GET'
})
getList.value = res.data.data.channels
})
</script>
- ArticleList.vue(文章列表组件)
<template>
<div class="article-list">
<div class="article-item" v-for="item in articles" :key="item.art_id">
<p class="title">{{ item.title }}</p>
<img v-for="(image, index) in item.cover.images" :key="index" class="img" :src="image" alt="" />
<div class="info">
<span>{{ item.aut_name }}</span>
<span>{{ item.comm_count }}评论</span>
<span>{{ item.pubdate }}</span>
</div>
</div>
</div>
</template>
<script setup lang='ts'>
import axios from 'axios'
import { ref, watch } from 'vue'
import { ArticleItem, ArticleResData } from '../types/data'
// 父传子
const props = defineProps<{ channelId: number }>()
const articles = ref<ArticleItem[]>([])
watch(
() => props.channelId,
async () => {
// 使用TS的时候,axios()调用需要改为 axios.request(),才可以使用泛型
const res = await axios.request<ArticleResData>({
url: 'http://geek.itheima.net/v1_0/articles',
params: {
channel_id: props.channelId,
timestamp: Date.now()
}
})
articles.value = res.data.data.results
},
{ immediate: true }
)
</script>
5. TS在vue3中使用总结
- 父传子:defineProps
const {money,car='xxx'} = defineProps<{
money:number
car?:string
}>()
- 子传父:defineEmits
const emit = defineEmits<{
{e:'changeChannel',id:number}:void
}>()
- ref
const count = ref(0)
const list = ref<TodoItem[]>([])
- reactive
const book = reactive({title:'xxx'})
//推导不正确,需要加类型,变量后类型注解
const book: {title:string,year?:number} = reactive({title:'xxx'})
- computed
const double = computed(()=>count.value*2)
const money = computed<string>(()=>(count.value*2).toFixed(2))
- 事件处理函数
// $event是啥? 分情况
// 情况1:绑定的事件是原生JS事件,$event就是原生JS对象的event
// 情况2:组件自定义事件的$enevt就是emit('事件名','传参or数据')中的传参or数据
// input @change="handleChange($event)"
const handleChange = (event:Event) => {
(event.target as HTMLInputElement).value
}
- ref属性拿dom对象
const dom = ref<HTMLInputElement | null>(null)
onMounted(() => {
//非空断言
//dom.value拿到的就是input
dom.value!.value
})
<input ref="dom">
三、Pinia
3.1 Pinia介绍
Pinia中文文档
Pinia
是一个状态管理工具,它和Vuex
一样为Vue
应用程序提供共享状态管理能力。- 语法和
Vue3
一样,它实现状态管理有两种语法:选项式API
与组合式API
,我们学习组合式API
语法。 - 它也支持
Vue2
也支持devtools
,当然它也是类型安全的,支持TypeScript
Pinia的数据流转图
- 可以创建多个全局仓库,不用像
Vuex
一个仓库嵌套模块,结构复杂。 - 管理数据简单,提供数据和修改数据的逻辑即可,不像
Vuex
需要记忆太多的API
。
3.2 Pinia使用步骤(以vue3为例)
总结:通过 const useXxxStore = defineStore('id',函数)
创建仓库得到使用仓库的函数
- 安装
pinia
yarn add pinia
# 或者使用 npm
npm install pinia
main.ts
中引入注册
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
//创建pinia插件
const pinia = createPinia()
const app = createApp(App);
app.use(pinia)
app.mount('#app')
- 在
src/store
下新建一个counter.ts
文件
// counter.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
//创建仓库
//参数1:id是仓库的唯一标识,一般和文件名字一致,方便找到对应的仓库
//参数2:storeSetup类似vue3的setup。作用:函数中定义数据和函数返回,他们就是store的数据和函数
//参数3:options? 额外配置
export const useCounterStore = defineStore('counter', () => {
//state
const count = ref(100)
//getters
const doubleCount = computed(() => count.value * 2)
//mutations
const update = () => count.value++
//actions
const asyncUpdate = () => {
setTimeout(() => {
count.value += 1000
}, 1000);
}
return { count, doubleCount, update, asyncUpdate }
})
- 在App.vue中使用
<template>
<div>
<p>APP组件{{ store.count }} {{ store.doubleCount }}</p>
<button @click="store.update()">改Count</button>
<button @click="store.asyncUpdate()">异步改Count</button>
</div>
</template>
<script setup lang='ts'>
import { useCounterStore } from './store/counter';
// 使用仓库
const store = useCounterStore()
</script>
3.3 storeToRefs的使用
- 问题:当我们想解构
store
提供的数据时候,发现数据是没有响应式的。 - 解决:使用
storeToRefs
解决解构仓库状态丢失响应式的问题
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
3.4 用Pinia改造头条
- 主要文件目录
- main.js
import { createApp } from 'vue'
import './styles/index.css'
// 1. 导入创建pinia的函数
import { createPinia } from 'pinia'
import App from './App.vue'
// 2. 创建pinia插件实例
const pinia = createPinia()
const app = createApp(App);
// 3. 使用插件
app.use(pinia)
app.mount('#app')
- data.d.ts
// 提取类型到 types 目录
// 频道对象
export type ChannelItem = {
id: number;
name: string;
};
// 频道接口响应数据
export type ChannelResData = {
data: {
channels: ChannelItem[];
};
message: string;
};
// 文章对象
export type ArticleItem = {
art_id: string;
aut_id: string;
aut_name: string;
comm_count: number;
cover: {
cover: number;
images: string[];
};
is_top: number;
pubdate: string;
title: string;
};
// 文章接口响应数据
export type ArticleResData = {
data: {
pre_timestamp: string;
results: ArticleItem[];
};
message: string;
};
- store/channel.ts
// channel 仓库,提供频道ID数据和修改频道ID的函数
import {defineStore} from 'pinia'
import {ref} from 'vue'
export const useChannelStore = defineStore('channel',()=>{
//频道ID数据
const channelId = ref(0)
//修改频道ID的函数
const changeChannel = (id:number) => {
channelId.value = id
}
return {channelId,changeChannel}
})
- App.vue
<template>
<!-- 频道导航组件 -->
<ChannelNav></ChannelNav>
<!-- 文章列表组件 -->
<ArticleList></ArticleList>
</template>
<script setup lang="ts">
// vue3中只需引入就能使用子组件,无需注册
import ChannelNav from './components/ChannelNav.vue';
import ArticleList from './components/ArticleList.vue';
</script>
- ChannelNav.vue
<!-- 频道导航组件 -->
<template>
<div class="channel-nav">
<nav class="list">
<a class="item" :class="{ active: store.channelId === item.id }" href="javascript:;"
v-for="item in getList" :key="item.id" @click="store.changeChannel(item.id)">
{{ item.name }}
</a>
</nav>
</div>
</template>
<script setup lang='ts'>
import axios from 'axios'
import { onMounted, ref } from 'vue';
import { useChannelStore } from '../store/channel';
import { ChannelItem, ChannelResData } from '../types/data';
// 定义文章列表数组
const getList = ref<ChannelItem[]>([])
onMounted(async () => {
// 使用TS的时候,axios()调用需要改为 axios.request(),才可以使用泛型
const res = await axios.request<ChannelResData>({
url: 'http://geek.itheima.net/v1_0/channels',
method:'GET'
})
getList.value = res.data.data.channels
})
// 使用名为 useChannelStore的仓库
const store = useChannelStore()
</script>
- ArticleList.vue
<!-- 文章列表组件 -->
<template>
<div class="article-list">
<div class="article-item" v-for="item in articles" :key="item.art_id">
<p class="title">{{ item.title }}</p>
<img v-for="(image, index) in item.cover.images" :key="index" class="img" :src="image" alt="" />
<div class="info">
<span>{{ item.aut_name }}</span>
<span>{{ item.comm_count }}评论</span>
<span>{{ item.pubdate }}</span>
</div>
</div>
</div>
</template>
<script setup lang='ts'>
import axios from 'axios'
import { ref, watch } from 'vue'
import { useChannelStore } from '../store/channel';
import { ArticleItem, ArticleResData } from '../types/data'
// 使用名为 useChannelStore的仓库
const store = useChannelStore()
const articles = ref<ArticleItem[]>([])
watch(
() => store.channelId,
async () => {
// 使用TS的时候,axios()调用需要改为 axios.request(),才可以使用泛型
const res = await axios.request<ArticleResData>({
url: 'http://geek.itheima.net/v1_0/articles',
method: 'GET',
params: {
channel_id: store.channelId,
timestamp: Date.now()
}
})
articles.value = res.data.data.results
console.log(res.data.data);
},
{ immediate: true }
)
</script>