文章目录
创建项目
创建项目
-
打开命令行窗口,键入
npm init vue@letest
. 需要 node16 及以上版本还可以使用如下命令
yarn create vite pnpm create vite
-
输入项目名,然后回车
-
选择项目类型
-
选择语言类型,这里开始学习的时候选择JavaScript,后续我们会用到TypeScript,本文亦有TypeScript的基础使用讲解
-
等待创建完毕后,进入项目文件夹,打开命令行输入
yarn
或者npm i
下载项目依赖,下载完毕后输入yarn dev
或者npm run dev
启动项目
Vue3 与 Vue2 中的 main.js 的区别
- Vue3 中不再使用 new Vue()的方式去创建 Vue 实例,而是使用 createApp 函数去创建
// Vue2
import Vue from 'vue'
import App from './App'
new Vue({
render: h => h(App),
}).$mount('#app')
// vue3
import ccreateApp from 'vue'
import App from './App.vue' // 这里不能用@ 也不能省略后缀名
const app = createApp()
app.mount('#app')
组合式 API (composition API)
Vue3 提供两种组织代码逻辑的写法,即 Vue2 的选项式 Api(Options API),以及 Vue3 新推出的的组合式 API(Composition API)
- 选项式 API Options API
<script>
export default {
methods: {/** **/}
create() {/** **/}
}
</script>
-
组合式 API
Vue3 提供的setup 函数是所有组合式 API 的起点,从生命周期上来看,它在 beforeCreate 之前执行, 也因为这一点,在 setup 函数中的 this 是undefined,不是组件实例。这点必须与React中类组件中的this为undefined区分开来,因为这是class类的特性——类里面是开启了严格模式的,严格模式下的方法里面的this均为undefined
使用组合式 Api 的优点:
- 可复用、可维护性高,总之就是爽
<!-- 在Vue3中,将script代码写在最前面,这是Vue3提供的默认写法,也可以遵循原来Vue2的写法 -->
<script>
setup() {
const msg = 'hi vue3'
const btnclick = () => {
console.log('Hi VUE3!')
}
// setup 中的数据如果需要在模板中被使用,就必须在 setup 函数中返回出去
// 后续我们会学习setup的语法糖,就可以省略这一步了
return { msg, btnclick }
}
</script>
<template>
<!-- Vue3模板支持多节点写法,不再是Vue2的单根节点 -->
<h1> {{ msg }}</h1>
<button @click="btnclick">按钮</button>
</tempalte>
setup 函数
- setup 函数是所有组合式 Api 的入口
- 在 beforeCreate 之前就执行,因此该函数中的 this 是 undefined
- 如果数据或者函数需要在 setup 中返回
- 今后在 Vue3 项目中几乎用不到 this,所有的东西通过函数获取
数据响应式 — reactive 函数以及 ref 函数
在 setup 中返回的数据,如果没有做响应式处理,那么它就不是响应式
- reactive 函数 用来对复杂数据类型做响应式
<!-- 每次都要将模板需要的数据返回出去都很麻烦,可以使用setup语法糖,即在script标签上添加setup,这时模板就可以随意使用setup中的数据与方法了 -->
<script setup>
// 在使用ref和reactive的时候必须先从vue中引入
import { reactive } from 'vue'
const obj = reactive({ name: '张三', age: 10 })
const score = reactive([90, 23, 123, 123, 111, 118])
setTimeOut(() => {
score.push(33, 69, 80)
}, 1000)
</script>
<template>
<h1>{{ obj.name }}</h1>
<h1>{{ obj.age }}</h1>
<h1>成绩: {{ score }}</h1>
<button @click="obj.name = '李四'">改变名字</button>
</template>
- ref 函数 可以对简单数据类型和复杂数据类型都做响应式,与
reactive
函数的定义的响应式数据在使用上有所不同,请看如下示例:
<script setup>
// 同reactive一样,在使用前必须要引入
import { ref } from 'vue'
const count = ref(0)
const addTen = () => {
// 不像reactive定义的数据可以直接修改,ref响应式数据的值,就必须用xxx.value去修改
count.value += 10
}
// ref 还可以将一个对象变成响应式的
const obj = ref({ name: '李四' })
</script>
<template>
<h1>{{ count }}</h1>
<button @click="count++">count++</button>
<button @click="addTen">count+10</button>
<h1>{{ obj.name }}</h1>
<button @click="obj.name = 'LiSi'">改变名字</button>
</tempalte>
-
在实际开发中,可以根据具体需要,在 ref 和 reactive 之间做取舍,例如当对象已知且固定时,就可以用 reactive,在数据是不确定时,就使用 reactive。为了不增加心智负担,建议响应式数据统一用ref去定义
-
在 setup 函数中,或者在其内的方法中,同时对一个做了响应式的数据和对一个没做响应式的数据做出了更改,二者的变化都会反应到页面中,这是为什么?
在改变响应式数据的同时,也可以改变没有响应式的数据,因为前者会驱动视图的更新,连带着将没有响应式的后者的最新数据也更新了
上述情况仅仅针对复杂数据类型 没做响应式的简单数据类型发生的变化则不会随着响应式数据的变化反应到页面中
<!-- 代码重现 -->
<script setup>
import { ref } from 'vue'
let count = ref(10)
let obj = {
age: 19,
}
let agej = () => {
count.value += 5
obj.age += 5
}
</script>
<template>
<h1>count: {{ count }}</h1>
<h1>age: {{ obj.age }}</h1>
<button @click="agej">按钮</button>
</template>
计算属性 computed 函数
computed函数接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象
<template>
<div>
原来的成绩: {{ score }} <br />
优秀的成绩:{{ mscore }}
</div>
<button type="button" @click="computedRef++">count is {{ computedRef }}</button>
</template>
<script setup>
import { ref , computed } from 'vue'
const score = ref([100, 90, 60, 70, 68])
// 注意mscore虽然是ref对象,但是它是只读的!不能修改!
const mscore = computed(() => score.value.filter(e => e >= 90))
setTimeout(() => {
score.value.push(90, 98)
}, 3000)
const count = ref(0)
// 创建一个可写的computed
const computedRef = computed({
get () {
return count.value
},
set (val) {
count.value = val++
}
})
</script>
监视属性 watch 函数
watch函数的第一个参数它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组;第二个参数是一个数据变化是要调用的回调函数,该回调函数接收三个值,分别是新值、旧值,以及一个用于注册副作用清理的回调函数(很少使用,具体用法官网查看);第三个参数接收一个配置对象,接收以下配置项:
immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined。
deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。默认如果监听的是一个响应式对象,自动会开启深度监听。开启深度监听是一件损耗性能的事,请考虑应用场景
flush:调整回调函数的刷新时机。参考回调的刷新时机及 watchEffect()。
onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器。
<template>
<div>计数器:{{ count }} <button @click="count++">count++</button></div>
<div>
<p>姓名: {{ user.name }}</p>
<p>性别: {{ user.info.gender }}</p>
<p>年龄: {{ user.info.age }}</p>
<button @click="user.info.age += 1">age++</button>
</div>
</template>
<script setup>
import { ref, watch, reactive } from 'vue'
const count = ref(0)
const user = reactive({
name: '张三',
info: {
gender: '男',
age: 18,
},
})
// watch的用法
// 1. 第一个参数是一个ref、响应式对象、计算属性
// 用法 watch(监听的参数,改变后执行的回调)
watch(count, (newValue, oldValue) => {
console.log('数据被改变了', newValue, oldValue)
})
// 第一个
watch(user, (newValue, oldValue) => {
console.log('数据被改变了', newValue, oldValue)
})
// 2. 第一个参数是多个数据源组成的数组
// 用法 watch([数据1,数据2],数据改变后的回调)
// 数组写法能监听到对象里面的所有变化,但是使用回调方式监听一个对象属性却不行,比如用法4,必须加deep属性
watch([count, user.info.gender], (nv, ov) => {
console.log('数据被改变了', nv, ov)
})
setTimeout(() => {
user.name = '李四'
}, 1000)
setTimeout(() => {
user.info.gender = '女'
}, 2000)
// 3. 第一个参数是一个getter函数,这种方法一般用来监听对象里面的某个属性
// 用法 watch( () => 对象.属性名,数据改变后的回调)
// 我们不能直接用 xxx.xxx 这种方式作为watch函数的第一个值,getter函去获取
// 如果采用这种写法,那么只有当被侦听的数据完全改变才行,简单数据类型不必多说,只要发生改变就会监听到
// 针对复杂数据类型,如果这个对象所指向的地址不发生改变,是监听不到对象的变化的,这时候就要使用第三个参数,开启深度监听
watch(
() => user.info.age,
() => {
console.log('数据改变了')
},
{
deep:true
})
// 一个监听响应式对象内部复杂数据类型属性的示例
// 对象属性是复杂数据类型那么只有开启深度监听才能监听到
watch(
() => obj.person,
(nv, ov) => {
console.log(
`nv: {age:${nv.age},name:${nv.name}}; \n ov: {age:${ov.age},name:${ov.name}}`
)
},
{ deep: true })
// 对象属性是简单数据类型直接就能监听到
watch(
() => obj.person.name,
() => {
console.log('The date is change')
}
)
</script>
Vue3 生命周期函数
使用步骤
- 先从 vue 中导入以
on打头
的生命周期钩子函数 - 在 setup 函数中调用生命周期函数并传入回调函数
- 生命周期钩子函数可以调用多次
Vue3的生周期钩子:
- setup、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted、onActivated、onDeactivated
与Vue2生命周期钩子的对比:
- 省去了created和beforeCreate
- 除了destroyed和beforeDestroyed被更名为onBeforeUnmount和onUnmounted之外,其它的生命周期都是在vue2的基础上加了个on
ref 获取 DOM 元素
在元素上使用 ref 属性关联响应式数据,即可获取到 DOM 元素
<script setup>
import { ref } from 'vue'
// 如果确定该ref对象是用来装对象的,建议初始值给null,不要给undefined,这在很多规范中是不合法的
const h1ref = ref(null)
const inputText = e => {
h1ref.value.innerText = e.target.value
}
</script>
<template>
<h1 ref="h1ref">111</h1>
<input type="text" @input="inputText" />
</template>
ref 操作组件- defineExpose
defineExpose 用以对组件外暴露数据和方法,父组件通过拿到组件实例调用这些数据或者方法
<!-- 子组件暴露数据 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const validate = () => {
console.log('调用了表单校验方法', count.value)
}
// 调用defineExpose暴露数据给父组件
defineExpose({ count, validate })
</script>
<!-- 父组件通过组件实例拿到这里面的数据 -->
<script>
import Son from './Son.vue'
import { ref } from './'
const sonRef = ref(null)
// console.log(sonRef.value.count) // 像这种写法就是违法的,因为setup的生命周期问题,此时DOM还未渲染,sonRef依旧是null
// 我们所有的对组件实例的调用,应当在正确的生命周期钩子或者自定义事件中去调用
const btn = () => {
// 父组件在使用子组件里面的数据时,就无需再.value
sonRef.value.count++
sonRef.value.validate()
}
</script>
<template>
<Son ref="sonRef" />
<button @click.native="btn">click me!</ button>
</template>
父子通讯 defineProps & defineEmits
在 vue2 中,父子通讯主要靠 props 以及事件实现,Vue3 中当然也支持这种写法,只不过写法稍有区别
- defineProps 传值
<!-- 子组件定义要接收的props数据 -->
<script>
const props = defineProps({
// defineProps里面也支持类型检查
money: Number,
userinfo: {
type: Object,
// 也支持其它选项
// required: true, // 必填
default: 'Hello Vue' // 默认值
},
})
// 传过来的复杂数据类型是可以修改的,但最好是遵循props的单向数据流原则,谁传的,谁去修改
const changeFatherData = ()=>{
props.userinfo.age = 20
}
</script>
<template>
<h2>
<!-- 注意: props可加可不加,而且传过来的基础数据类型是只读的,不可修改的 -->
money: {{ props.money }}
</h2>
<h2>
userinfo: {{ userinfo }}
<button @click.native="changeFatherData">click me!</ button>
</h2>
</template>
<!-- 父组件定义传值 -->
<script>
import { ref } from 'vue'
import Son from './Son.vue'
let money = ref(0)
let userInfo = ref({
name: '张三',
age: 18,
})
</script>
<template>
<!-- 传值 -->
<Son :money="money" :userinfo="userInfo" />
</template>
- defineEmits 传递事件
<!-- 还是用props传值的模板,稍加修改,省略号即代表上一模板的代码 -->
<!-- 子组件定义要接收的Emits事件 -->
<script>
// ...
const emits = defineEmits(['changeMoney'])
</script>
<template>
<h2>
money: {{ props.money }}
<button @click="emit('changeMony')">加钱</button>
</h2>
// ...
</template>
<!-- 父组件定义传值 -->
<script>
// ...
const changeMoney = () => {
money.value += 1
}
</script>
<template>
<!-- 传递自定义事件 @changeMoney 可以写成@change-money -->
<Son :money="money" :userinfo="userInfo" @change-money="changeMoney" />
</template>
跨组件通讯 provide 与 inject 函数 (依赖注入)
provide(数据提供方)与inject(数据接收方)与 props 差不多,但是前者影响所有的子孙(谁需要谁就用 inject 接收),后者是只影响其一级子组件,若想用 props 实现前者的功能,就需要一级一级的传递
注意事项:
- provide 与 inject 函数都需要引入
- provide 与 inject 函数虽然之间传递的所有数据都能够修改,但是依旧要遵循单向数据流原则,谁提供,谁修改,避免代码变得混乱,难以维护
- inject 接收的数据遵循就近原则,即从祖先辈最近的地方拿
<!-- 顶层祖先组件 -->
<script>
import { reactive, provide } from 'vue'
const userInfo = reactive({
name: '张三',
})
// 定义修改数据的方法,我们一定要遵循单向数据流原则,避免代码紊乱
const changeName = () => {
userInfo.name = '李四'
}
provide('userInfo', userInfo)
provide('changeName', changeName)
</script>
<!-- 第二层祖先组件 -->
<script>
import { reactive, inject, provide } from 'vue'
const userInfo = inject('userInfo')
console.log(userInfo)
// inject遵循的是就近匹配原则,之后该组件的子组件inject到的的changeName就都是它了
const changeName = () => {
console.log('我没有改名字的方法')
}
provide('changeName', changeName)
</script>
<!-- 最后一层组件 -->
<script>
import { reactive, inject } from 'vue'
const changeName = inject('changeName')
</script>
toRefs 保持数据响应式
该函数用以在复杂数据类型展开或者解构的时候保持响应式
<script>
import { reactive, toRefs } from 'vue'
const userInfo = reactive({
name:"张三",
age:18,
info:{
telNumber: '176xxxxx',
address: '湖南省邵阳市'
}
})
// 解构数据 解构出来的数据可以在模板中直接使用
const {{ name, age }} = toRefs(userInfo)
// 展开数据 展开的数据,特别是第一层的数据,由于被转换成了'ObjectRefImpl',所以在使用的时候要加 .value,但是这只会影响第一层,还要注意的是,拷贝出来的对象在vue开发工具中是undefined,仅仅是原数据的一个映射
const userInfoCopy = {...toRefs(userInfo)}
</script>
<template>
<h1>name: {{ name }} age: {{ age }}</h1>
<!-- userInfoCopy -->
<h1>name: {{ userInfoCopy.value.name }} age: {{ userInfoCopy.value.age }}</h1>
</template>
TypeScript
简介
优点 1.代码的可读性和可维护性 2.编译阶段就发现大部分错误,避免了很多 bug 3.增强了编辑器和 IDE 的功能,包括代码补全,接口提示,跳转到定义,重构
缺点 1.有一定的学习成本,需要理解接口(interface),泛型(generics),类(classes),枚举类型(Enums) 2.会增加一些开发成本 3.一些 JS 库需要兼容,提供声明文件,像 vue2,底层对 ts 的兼容就不是很好
5.ts 编译需要时间
类型注解
TypeScript 的特点就是有类型的 JavaScript,能够在编码的时候就能指出大部分错误,大大提升了代码的稳定性.类型注解就是为了给变量限定类型
let age: number = 0 // 如此,后续若要改变age的值,则必须是number类型,如果是其它类型,则在编码的时候就会报错提醒
原始类型
- 原始类型,使用简单,完全按照 JS 的类型来书写
let age: number = 18
let myName: string = '黑马程序员'
let isLoading: boolean = false
let nullValue: null = null
let undefinedValue: undefined = undefined
数组类型
// 数组类型有两种写法,写法1
let numbers: number[] = [1,2,3,4]
// 写法2
let numbers2: Array<string> = ['a','b','d']
// 如果数组需要存储多种类型数据则为以下写法 写法2叫做泛型,之后做详细介绍
let arr1 = (number | string)[] = [1,2,'a','c']
let arr2 = Array<number | string> = [1,2,'a','c']
// 写法1一定要加括号,如写成以下格式,则表示接收字符串或者数字数组类型
let arr3 = string | number[] = 'fight'
联合类型
// 上述 type | type 类型注解即为联合类型
let arr: number | string = 1
类型别名
// 类型别名可以定义一些可被复用的类型注解 使用type关键字定义 尽量采用大驼峰命名规则
type NumberType = string | number
函数类型
在 typescript 中,函数变量最好指定类型以及返回值
- 基本使用
function add(num1: number, num2: number): void {
console.log(num1 + num2)
}
// 如果返回类型为undefined,则必须指定返回值为undefined
function add(num1: number, num2: number): undefined {
console.log(num1 + num2)
return undefined
}
- 使用类型别名代替类型注解 (仅在有变量接收的函数表达式中生效)
type FunctionType = (a: number, b: number) => number
const add: FunctionType = (a, b) => {
return a + b
}
const add: FunctionType = function (a, b) {
return a + b
}
- 可选参数 ?: 定义可选参数一定要在所有必选参数的后面,否则会报错
function add(num1: number, num2?: number): undefined {
console.log(num1 + num2)
return undefined
}
// 函数表达式形式
type FunctionType = (a: number, b?: number) => number
const add: FunctionType = (a, b) => {
return a + b
}
对象类型
对象类型就是描述对象内部属性或方法的类型,因为对象是由属性或者方法组成的
let person:{} = {} // 空对象类型
// 有属性的对象类型
let person:{ name: string } = { name:'同学' }
// 有属性和方法的对象类型,一行书写多个类型用分号;隔开
let person:{ name: string, action:()=>void} = {
name:'ls',
action:()=>{
console.log('hello')
}
}
// 换行写属性类型可以省略分号
type Person = {
name:string
action()=>void
}
let person: Person = {
name:'ls',
action:()=>{
console.log('hello')
}
}
// 动态添加属性
type ObjNumber = {
[key:string]: number
}
const TestObj:ObjNumber = {}
// index即是动态值
TestObj[`active${index}`] = xxx
接口 Interface
接口类型是命名对象类型的另外一种方式
类型别名也可以命名对象类型,但是接口是专门为了对象而服务的
// 定义接口 接口名后面跟类型就别名就不一样了,不是等号=,而是直接花括号{}
interface Person {
name: string
age: number
gender: string
}
interface Student extends Person {
class: string
grade: stirng
}
// 定义一个对象,同时继承Student接口
const student1: Student = {
name: '李四',
age: 18,
gender: '男',
class: '365班',
grade: '高三',
}
Interface 继承
// 定义接口
interface Parne2D {
x: number
y: number
}
interface Parne3D {
z: number
}
// 对象继承接口
const obj: Parne2D = {
x: 1,
y: 2,
}
const obj3D: Parne2D extends Parne3D = {
x: 1,
y: 2,
z:50,
}
// 接口拓展 1. 继承已有接口
interface Parne3D extends Parne2D {
z: number
}
// 对象继承接口
const obj2: Parne3D = {
x: 1,
y: 2,
z: 3,
}
// 拓展2. 继承已有类型别名
type user = { name: string }
interface Person extends user {
age: number
}
// 对象继承接口
const person: Person = {
age: 19,
name: '李四',
}
// 拓展3. 类型别名合并接口 & 可以合并接口还有类型
interface Person2 {
age: number
}
type user2 = Person2 & { name: string }
const person2: user2 = {
age: 2,
name: '王五',
}
接口与类型还是有去别的,类型不能够重复定义,但是接口可以,而且还会将所有的重复定义合并在一起
类型推断
在 typescript 中存在类型推断机制,即使不写类型,TS 也会指定类型.掌握好类型推断,会大大提升开发效率,少写很多代码.但是在刚刚接触 TS 的时候,最好还是写上
// 变量 age 的类型被自动推断为:number
let age = 18
// 函数返回值的类型被自动推断为:number
const add = (num1: number, num2: number) => {
return num1 + num2
}
字面量类型
即以 JS 字面量作为变量类型
// 基本使用
let str1: '男' | '女' = '男' // 变量类型为 '男'|'女'
// 以及以下场景需特别注意
let str2 = 'Hello TS' // 由于let是可变的,给它赋一个字符串就,类型推断就认为变量类型是stirng
const str3 = 'Hello TS' // 由于const是不可变的,那么此时就表示它的变量类型是 'Hello TS'
any 类型
any 类型的作用就是为了逃避 TS 的类型检查,使用 any 类型的代码越多,程序的漏洞就有可能越多
let obj: any = { name: 'zs' }
obj.age = 22
obj()
const n: string = obj
// 尽管上述代码不会报错,但是这不符合TS的严谨性,应避免
// 隐式any的情况
let anydata // 定义变量未初始化
const fn = n => {} // 函数参数不给类型或者初始值
类型断言
我们可以指定一个值的类型,使其变得更加明确,例如如下代码,这样就不会导致 a 标签的类型范围太宽,没包括 a 标签特有的属性和方法
const dom = document.querySelector('#alink') as HTMLAnchorElement
泛型
泛型可以用来实现对象,方法,以及类,接口的复用
// 泛型 用于实现类型的复用
type User = {
name: String
age: number
}
type Student<T> = {
classNum: string
data: T
}
const xiaoM: Student<User> = {
classNum: '一年',
data: {
name: '小明',
age: 18,
},
}
// 实现方法的复用
const initFn: <T>(n: T) => T = n => n
const initFn2 = <T,>(n: T) => n
initFn('str')
initFn(1)
initFn([])
// 实现接口复用
interface userDat<T> {
// 定义返回泛型数据T的方法
id: (arg0: T) => T
// 定义返回泛型数组T的方法
ids: (arg: T[]) => T[]
}
const xiaoH: userDat<number> = {
id(arg0) {
return arg0
},
ids(arg) {
return arg
},
}
const xiaoG: userDat<string> = {
id(arg0) {
return arg0
},
ids(arg) {
return arg
},
}
xiaoH.id(1111)
xiaoH.ids([12])
xiaoG.id('as')
xiaoG.ids(['assa'])
枚举类型
枚举类型用于例举一些可选的项,类似于数据库中的字典
// 枚举使用enum关键字定义
enum Gender {
男, // 如果未定义枚举属性的值,那么默认从0开始递增
女 = 2,
未知, // 如果前面指定了值后面的不指定,那么就从前一个的值开始递增
// 如果要使用递增,那么该枚举类型里面就只能有number类型的值例如: 下面这行代码给枚举属性为字符串,那么,其它枚举属性就必须全部赋值,不能再使用递增和默认值
// 未知性别="***"
}
const gender: Gender = Gender.男
const gender2: Gender = Gender.未知
console.log(gender) // 输出 0
console.log(gender2) // 输出 3
元组类型
数组的元素有限定的个数和类型组成时,称为元组
let user: (string | number)[] = ['ls', 18, 'man'] // 假设此处不限定数组元素的个数以及类型 那么后续可以随意更改这个数组,这不符合TS严谨的本意,例如:
user = [1, 2, 2323, 1, 'woman'] // 这是不对的
// 声明元组
let user: [string, number, string] = ['ls', 18, 'man']
user = ['zs', 20, 'woman'] // 后续再去更改就只能严格遵守这个格式
// 它也支持可选项
let user: [string, number, string?] = ['ls', 18, 'man']
user = ['zs', 20]
类型声明文件
在项目中安装的第三方库里面的都是打包后的 JS 代码.但是在使用的时候却有 ts 提示,这是因为在第三方库中的 JS 代码都有对应的**TS 类型声明文件**
在 TS 中,以.d.ts
为后缀的文件,就称之为 TS 类型声明文件,它的主要作用就是描述 JS 模块内所有导出成员的类型信息.
TS 中的文件类型分为.ts
, .d.ts
两种
-
.ts 文件
既包含类型信息又有可执行代码
可以被编译为.js 文件
编写业务代码的地方
-
.d.ts 文件
只包含类型信息的类型声明文件
不会参与编译生成.js 文件,仅用于提供类型信息,且该文件中不允许出现任何可执行代码,只能用于提供类型
为 JS 提供类型信息
-
.ts
是implementation
代码实现文件,.d.ts
是declaration
类型声明文件. 如果要为 JS 库或者模块提供类型,就需要类型声明文件
内置声明文件
TS 给 JS 运行时可用的标准化内置 API 都提供了类型声明文件,这些声明文件就是内置类型声明文件
第三方库类型声明文件
有的库自带类型声明文件,我们可以直接前往项目依赖中查看. 重点说明一些没有类型声明文件的第三方库
比如 JQuery,在安装导入后,就会提示需要安装: @types/jquey
类型声明包
这是来自于一个叫 DefinitelyTyped 的 github 库,可在搜索TypeScript: Search for typed packages (typescriptlang.org)搜索是否有对应的@types/*
类型声明包
自定义类型声明文件
-
共享类型
如果多个 ts 文件都用到同一个类型,此时可以创建.d.ts 文件提供该类型,实现类型共享
操作步骤
- 创建
index.d.ts
类型声明文件。 - 创建需要共享的类型,并使用
export
导出(TS 中的类型也可以使用import/export
实现模块化功能)。 - 在需要使用共享类型的
.ts
文件中,通过import
导入即可(.d.ts
后缀导入时,直接省略)。
- 创建
-
给 JS 文件提供类型
在导入 .js 文件时,TS 会自动加载与 .js 同名的 .d.ts 文件,以提供类型声明。
declare 关键字:
- 用于类型声明,为其他地方(比如,.js 文件)已存在的变量声明类型,而不是创建一个新的变量。
- 对于
type
interface
等这些明确就是 TS 类型的(只能在 TS 中使用的),可以省略
declare 关键字。 - 其他 JS 变量,应该使用
declare
关键字,明确指定此处用于类型声明。
// add/idnex.d.ts declare const add: (a: number, b: number) => number type Position = { x: number y: number } declare const point: (p: Position) => void export { add, point }
// add/index.js const add = (a, b) => { return a + b } const point = p => { console.log('坐标:', p.x, p.y) } export { add, point }
// main.js import { add, point } from './add' // 此时,导入的方法就已经有了类型说明了 add(3, 10) point({ x: 100, y: 200 })
TypeScript 与组合式 API
创建 TSVue 项目
# yarn
yarn create vite my-vue-ts-app --template vue-ts4
# npm 6.x
npm create vite@latest my-vue-ts-app --template vue-ts
# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-ts-app -- --template vue-ts
# pnpm
pnpm create vite my-vue-ts-app --template vue-ts
defineProps 写法
// 传统基于js的v3项目中的defineProps写法
const prosp = defineProps({
money: {
type: Number,
required: true,
},
car: {
type: String,
default: '本田'
},
})
// 使用ts定义defineProps,以及默认值
// 通过泛型类型来定义props的类型 car?: string 就相当于上面写法的 car: { type: String,/****/ },表示非必填,且类型为string
// 注意TS对于类型的约束更加严格,string并不等于String,前者是原始数据类型,后者是一个构造函数,区别巨大
const props = defineProps<{ money: number; car?: string }>()
// 为props指定默认值
// 使用withDefaults为props指定默认值,第一个参数是defineProps函数,注意切不可拿个变量接收这个函数,再把变量丢进withDefaults中
// 第二个参数是个对象,用于对props进行初始化
const props = withDefaults(defineProps<{ money: number; car?: string }>(), {
car: '特斯拉',
})
// 使用
console.log(props.car)
// 上述方法太笨拙,可以使用响应式语法糖解构+defineProps来解决
const { money, car = '汽车' } = defineProps<{ money: number; car: string }>()
// 这个方法还需要到vite.config.ts中显示开启才可用 因为上述方法还处于实验阶段,如果追求稳定性,更推荐withDefaults+defineProps
export default defineConfig({
plugins: [
vue({
reactivityTransform: true,
}),
],
})
defineEmits 写法
<!-- 子组件 -->
<script setup lang="ts">
// 使用ts定义defineEmits 在父组件再监听addMoney事件即可
// 在泛型中给了一个对象,对象中又给了一个函数,这种写法叫函数签名,即该对象既可以当对象来使用,又可以当函数来使用
const emits = defineEmits<{ (e: 'addMoney', n: number, arr?: Event): void }>()
// const emits = defineEmits(['addMoney'])
// 在ts中,如果不指定事件对象的类型,那么默认就是any
const click = (e: Event) => {
emits('addMoney', 300)
}
</script>
<template>
<button @click="click">调用父组件加钱的方法</button>
</template>
ref 与 reactive
<script setup lang="ts">
import { ref, reactive } from 'vue'
// 在ts中,可以限定ref和reactive的类型,严格控制后续的修改
let money = ref<number>(10)
money.value = 1 // 正确
// money.value = '1' // 错误
const obj = ref<{ name: string; id: number }[]>([{ name: '1', id: 135 }])
// obj.value[0].id = '1' // 错误修改
obj.value[0].id = 1 // 正确修改
obj.value.push({ name: 'ls', id: 12345 }) // 正确push
// obj.value.push({name:'ls',id:'12345'}) // 错误push id需要number类型
// const obj2 = reactive<{ name: string; id: number }[]>([{ name: '1', id: 135 }]) // reactive不推荐这种泛型的写法 因为reactive的底层实现跟ref不一样 而是采用下面类型别名的写法
const obj2: { name: string; id: number }[] = reactive([{ name: '1', id: 135 }])
obj2.push({ name: 'asd', id: 12121 })
// ref和reactive都会进行隐式类型推导,如下:
const reftest = ref(18) // Ref<number>
const reactivetest = reactive({ name: 'zs' }) // { reactivetest:string }
// 但是针对复杂数据类型,都推荐指定类型,ref通过泛型,reactive通过类型别名或者接口
// 再次强调,reactive不推荐使用泛型来指定类型,因为reactive的底层实现跟ref不一样
// 且如不是操作数组,更不推荐使用reactive,会很不方便
const reftest2 = ref<{ name: string; age: number }>({ name: 'zs', age: 20 })
type Person = { name: string; age: number }
interface PersonInter {
name: string
age: number
}
const reactivetest2: PersonInter = reactive({ name: 'zs', age: 20 })
const reactivetest3: Person = reactive({ name: 'zs', age: 20 })
</script>
computed
<script setup lang="ts">
// 在ts中computed也会根据返回的结果推导类型
const doubleCount1 = computed(() => count.value * 2)
// 也可以通过泛型显性指定返回类型
const doubleCount2 = computed<string>(() => (count.value * 2).toFixed(2))
</script>
非空断言
<script setup lang="ts">
// 所谓非空断言,就是断定该属性一定存在
const obj: { name: string; fn?: () => void; id?: number } = {
name: '',
fn() {
console.log('function')
},
}
obj.fn!() // 非空断言就是不论存在或者不存在都会去拿去这个值,如果断言一个不存在的值,那么返回值就是undefined,如果断言一个不存在的方法,并执行这个方法,那么就会报not a function错误. 上述两种情况必须均在对象类型中已声明属性的存在,否则在都不会通过编译阶段
obj!.name = 'jkll'
console.log(obj!.id)
// 与可选链操作符相比,非空断言不论有没有都会去拿去结果或者方法,而可选链当要拿取的目标不存在或者为undefined或者null时,就会直接返回undefined
</script>
拿取组件实例
- 父组件代码
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import test14s from './14 拿取组件实例.vue'
let money = ref(1000)
let car = ref('宝骏')
// 通过ref拿取组件实例 在ts中使用InstanceType搭配typeof来获取组件的实例
const test14ref = ref<null | InstanceType<typeof test14s>>(null)
// 在onMounted钩子中去通过组件实例操作组件的属性以及方法
onMounted(() => {
setTimeout(() => {
// 这是通过组件实例直接去操作组件内部的方法,这些方法或者属性,一定要都暴露出去才能够通过组件实例拿到
// test14ref.value!.changeCars()
// 函数身上的非空断言不要写!.了,直接!()即可
// test14ref.value?.changeCars!()
// 在通过组件实例操作组件内属性的时候,一定要使用非空断言,可选断言是没有效果的
// test14ref.value!.moneys = 1200
// 还可以通过$emit来触发组件内的事件
test14ref.value?.$emit('changeCar', 'masha')
test14ref.value?.$emit('changeMoney', 8)
}, 1000)
})
const changeCar = (v: string) => {
car.value = v
}
const changeMoney = (v: number) => {
money.value = v
}
</script>
<template>
<div>
<test14s
:money="money"
:car="car"
ref="test14ref"
@change-car="changeCar"
@change-money="changeMoney" />
</div>
</template>
- 子组件代码
<script setup lang="ts">
import { ref } from 'vue'
// 定义props并初始化部分props
withDefaults(defineProps<{ money: number; car?: string }>(), {
car: '宝骏',
})
// 定义emits
const emits = defineEmits<{
(e: 'changeMoney', m: number): void
(e: 'changeCar', c: string): void
}>()
const changeCars = () => {
emits('changeCar', '玛莎拉蒂')
}
const changeMoneys = () => {
emits('changeMoney', 50)
}
let moneys = ref<number>(1000)
// 如果要通过实例来调用子组件身上的方法或者属性,那就必须要暴露出去
defineExpose({ changeCars, changeMoneys, moneys })
</script>
<template>
<h1>money: {{ money }}</h1>
<h1>car: {{ car }}</h1>
<h2>moneys: {{ moneys }}</h2>
</template>
全局自定义类型声明文件(待修补)
TypeScript内置类型
Pick 与 Omit
- Pick用于从一个对象类型中取出某些属性
type Person = {
name: String
age: number
}
type PickPerson = Pick<Person, 'age'>
// PickPerson ===> { age: string }
- Omit 可以从一个对象类型中排除某些属性
type Person = {
name: string
age: number
}
type OmitPerson = Omit<Person, 'age'>
// OmitPerson ===> { name: string }
- 合并举例
type Teacher = {
name: string
age: number
edu: string
}
type OmitStudent = Omit<Teacher,'edu'>
type Student = OmitStudent & {
class: string,
grade: string
}
/*
Student ===> {
name: string
age: number
class: string
grade: string
}
*/
状态管理工具— Pinia
Pinia
是一个状态管理工具,它和 Vuex
一样为 Vue
应用程序提供共享状态管理能力。
语法和 Vue3
一样,它实现状态管理有两种语法:选项式API
与 组合式API
,我们学习组合式 API 语法。
它可以在 Vue2 中使用,也支持 devtools,它同时也是类型安全的,因此支持 TypeScript
基本使用
- 下载
yarn add pinia
- 全局注册
import { createApp } from 'vue'
import App from './App.vue'
// 在main.ts中引入createPinia
import { createPinia } from 'pnia'
// 实例化pinia
const pinia = createPinia()
// 注册pinia
createApp(App).use(pinia).mount('#app')
- 创建仓库&使用仓库
// store/counter.ts
import { ref } from 'vue'
// 创建仓库需引入defineStore
import { defineStore } from 'pinia'
// defineStore('仓库名',()=>{/* 回调函数,里面写仓库数据以及方法 并使用return {} 将需要共享的数据以及方法暴露出去 */})
// 创建仓库并导出 仓库名一般遵守 use仓库名Store 的命名规则,defineStore的第一个参数则一般是文件名
export const useCounterStore = defineStore('counter', () => {
let count = ref(0)
const addCount = () => {
count.value++
}
return {
count,
addCount,
}
})
- 组件中使用仓库数据
// ./App.vue
<script setup lang="ts">
// 引入仓库
import { useCountStore } from '../store/counter.ts'
// 为了防止引入的数据丢失响应式,我们需要引入storeToRefs方法来保持Pinia中数据的响应式
import { storeToRefs } from 'pinia'
// 使用仓库
const counter = useCountStore()
// 通过storeToRefs解构数据,这样就不会丢失数据响应式
const { count } = storeToRefs(counter)
</script>
<template>
<!-- 在模板中使用 -->
<h1>count: {{ count }}</h1>
<!-- 通过仓库实例调用它暴露出来的方法 -->
<button @click="counter.addCount">count++</button>
</template>
多仓库互相引用
// store/counter.ts
import { ref } from 'vue'
import { defineStore,storeToRefs } from 'pinia'
import { useCounter2Store } from './counter2' // 引入一个自定义仓库
export const useCounterStore = defineStore('counter', () => {
// 关于实例化仓库 一定要在defineStore的回调里面去执行,在外面实例化会找不到pinia,因为pinia还没有全局挂载
const counter2 = useCounter2Store()
// 保持数据响应式
const { count2 } = storeToRefs( counter2 )
// ...
// 定义一个方法更改counter2里面的数据
const addCount2 = () => {
counter2.value++
}
return {
// ...
addCount2
}
})
// store/counter2.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounter2Store = defineStore('counter2', () => {
let count2 = ref(0)
return {
count2,
}
})
数据持久化
Pinia数据持久化
Pinia数据的持久化需要下载插件 pinia-plugin-persistedstate
- 使用和抽离main.js中的pinia代码,以实现仓库统一导出
/* 在stores文件夹下新建index.ts stores/index.ts */
import { createPinia } from 'pinia'
// 引入Pinia数据持久化插件
import Persistedstate from 'pinia-plugin-persistedstate'
export default createPinia().use(Persistedstate)
/* main.js */
import { createApp } from 'vue'
import App from './App.vue'
// 引入 ./store/index.ts
import pinia from './store'
createApp(App).use(pinia).mount("#app)
- 实现Pinia仓库数据持久化以及部分数据持久化
/* stores/user */
import { defineStore } from 'pinia'
import type { User } from '@/types/user'
import { ref } from 'vue'
// 定义数据类型
type User = {
token: string
id: string
account: string
mobile: string
avatar: string
}
export const useUserStore = defineStore(
'user',
() => {
// 用户信息
const user = ref<User>()
// 设置用户信息
const setUser = (u: User) => {
user.value = u
}
// 删除用户信息
const delUser = () => {
user.value = undefined
}
return { user, setUser, delUser }
},
// {
// // 对所有的数据开启数据持久化
// persist: true,
// },
{
// persist还支持配置项,用于配置哪些数据应持久化
persist: {
// 要持久化的数据
paths: ['user.token'],
}
}
)
import { defineStore } from 'pinia'
import { ref } from 'vue'
// 使用pinia仓库的数据
export const useCounter2Store = defineStore('counter2', () => {
let count2 = ref(0)
return {
count2,
}
})
数据持久化
Pinia数据持久化
Pinia数据的持久化需要下载插件 pinia-plugin-persistedstate
- 使用和抽离main.js中的pinia代码,以实现仓库统一导出
/* 在stores文件夹下新建index.ts stores/index.ts */
import { createPinia } from 'pinia'
// 引入Pinia数据持久化插件
import Persistedstate from 'pinia-plugin-persistedstate'
export default createPinia().use(Persistedstate)
/* main.js */
import { createApp } from 'vue'
import App from './App.vue'
// 引入 ./store/index.ts
import pinia from './store'
createApp(App).use(pinia).mount("#app)
- 实现Pinia仓库数据持久化以及部分数据持久化
/* stores/user */
import { defineStore } from 'pinia'
import type { User } from '@/types/user'
import { ref } from 'vue'
// 定义数据类型
type User = {
token: string
id: string
account: string
mobile: string
avatar: string
}
export const useUserStore = defineStore(
'user',
() => {
// 用户信息
const user = ref<User>()
// 设置用户信息
const setUser = (u: User) => {
user.value = u
}
// 删除用户信息
const delUser = () => {
user.value = undefined
}
return { user, setUser, delUser }
},
// {
// // 对所有的数据开启数据持久化
// persist: true,
// },
{
// persist还支持配置项,用于配置哪些数据应持久化
persist: {
// 要持久化的数据
paths: ['user.token'],
}
}
)