vue3.0学习笔记
一、vue3.0支持了Typescript
所以得先了解typescript的基本用法
1、数据声明方式
1.对数据类型进行了限制,否则会编译报错
编译ts命令 tsc xxx.ts -w
(-w参数加上会表示监控,自动编译),会生成一个xxx.js的文件,再使用命令node xxx.js
执行即可
2.类型断言
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。 其一是“尖括号”语法, 另一个为 as 语法
/*
类型断言(Type Assertion): 可以用来手动指定一个值的类型
语法:
方式一: <类型>值
方式二: 值 as 类型 tsx中只能用这种方式
*/
/* 需求: 定义一个函数得到一个字符串或者数值数据的长度 */
function getLength(x: number | string) {
if ((<string>x).length) {
return (x as string).length
} else {
return x.toString().length
}
}
console.log(getLength('abcd'), getLength(1234))
3.类型推断
类型推断: TS 会在没有明确的指定类型的时候推测出一个类型 有下面 2 种情况: 1. 定义变量时赋值了, 推断为对应的类型. 2. 定义变量时没有赋值, 推断为 any 类型
/* 定义变量时赋值了, 推断为对应的类型 */
let b9 = 123 // number
// b9 = 'abc' // error
/* 定义变量时没有赋值, 推断为any类型 */
let b10 // any类型
b10 = 123
b10 = 'abc'
2、数据类型
ts的数据类型比js更丰富
-
比如字面类型:
-
比如unknown和any:unknown是只能随意切换自己的类型但不能将类型赋值给第三方类型,不能跳过类型检验,any可以随意切换类型,也可以跳过类型检验
-
比如数组类型和对象,
-
比如void和never
-
比如元组tuple,和数组类似但是会严格限制数组元素类型,
-
比如枚举enum,类似Java枚举,java枚举具体使用参考:一、第一阶段------3、线程锁------闭锁(CountDownLatch:倒计时,为0放行)------枚举的介绍
ts的枚举介绍如下:
-
自定义类型(也叫类型别名)type,可以指定类型也能直接指定值
大家可能觉得这个和接口没多大区别,这不是重复了吗?其实不是,类型别名可以针对接口没法覆盖的场景,例如组合类型、交叉类型等;
// 1. 组合类型
type NumAndString = number | string;
// 2. 交叉类型
type SectionType = { name: string; age: number } & {
height: number;
name: string;
};
interface PersonInfo {
name: string;
height: number;
}
// 3. 提取接口属性类型
type PersonHeight = PersonInfo["height"];
let zs: SectionType = {
name: "张三",
age: 20,
height: 180,
};
// 黑魔法
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string & {}; // vscode提示字面类型都被保留
总结: 自定义类型和接口(interface和type)的区别:1、都可以用来自定义类型;2、类型别名支持联合和交叉类型定义;3、类型别名不支持重复定义, 接口可以
- Symbol,创建一个新的对象
例如
let a:symbol=Symbol("123");
let b:symbol=Symbol("123");
a==b 输出为 false
- 非原始类型object和Object
object不包含原始类型,Object包含原始类型,且可以用{}代替
let a:object={"name":"xxx"}
let b:object=[{"name":"xxx"}]
let obj:object = {a:1};
let arr:object = [1];
// et num;object = 20; // 报错 不能将类型“number”分配给类型“object”// let str:object ="hello"; // 报错
let obj1:Object = {b:1};
let arr1:Object = [2,3];
let num1:Object = 2;
let str1:Object = "2";
let bool1:Object = true;
let obj2:{} = {b:1};
let arr2:{ = [2,3];
let num2:{} = 2;
let str2:{} ="2";
let bool2:{} = true;
- 联合类型和交叉类型
- 可以把“|”类比为 JavaScript 中的逻辑或 “||”,只不过前者表示可能的类型。
let age: number | string = 20;
- 在 TypeScript 中,还存在一种类似逻辑与行为的类型——交叉类型(Intersection Type),它可以把多个类型合并成一个类型,合并后的类型将拥有所有成员类型的特性。使用“&”操作符来声明交叉类型(并集)。
// 思考这里有一个值满足m的类型要求吗?
let m : string & number;
let zs: { name: string; age: number } & { height: number } = {
name: "张三",
age: 20,
height: 180,
};
- 联合、交叉组合
联合、交叉类型本身就可以直接组合使用,这就涉及 |、& 操作符的优先级问题。联合操作符 | 的优先级低于交叉操作符 &,同样,我们可以通过使用小括弧 () 来调整操作符的优先级,这个和js一样。
let m:{ id: number } & { name: string } | { id: string } & { name: number };
m = {
id: 1,
name: ''
}
m = {
id: '',
name: 1
}
- 工具类型
3、typescript编译
ts要编译成js,再用node执行js文件,其中编译最好是能将所有需要编译的文件管理起来,下面介绍下编译配置文件步骤:
1、cmd进入ts文件所在目录,输入命令tsc --init,会生成一个文件tsconfig.json
2、编辑tsconfig.json
3、配置项属性compilerOptions介绍
ts自动编译总结:
1). 生成配置文件tsconfig.json
tsc --init
2). 修改tsconfig.json配置
“outDir”: “./js”,
“strict”: false,
3). 启动监视任务:
终端 -> 运行任务 -> 监视tsconfig.json
4、ts类和面向对象介绍
和java基本一致、属性、方法、方法的重载、构造器、继承、接口、继承接口后方法必须实现的控制、泛型。。。等都有
函数,也就是方法,对某些行为进行封装,比如下面的代码:
function add(x: number, y: number): number {
return x + y
}
let myAdd = function(x: number, y: number): number {
return x + y
}
//上面的代码还可以继续优化,将返回值接收的变量进行强定义
//`(x: number, y: number) => number `参数列表和函数一致,
//只是返回值类型的冒号‘:’变成了‘=>’,写出完整的函数代码
let myAdd2: (x: number, y: number) => number = function(x: number, y: number): number {
return x + y
}
方法的重载:
函数重载: 函数名相同, 而形参不同的多个函数 在 JS 中, 由于弱类型的特点和形参与实参可以不匹配, 是没有函数重载这一说的 但在 TS 中, 与其它面向对象的语言(如 Java)就存在此语法
/*
函数重载: 函数名相同, 而形参不同的多个函数
需求: 我们有一个add函数,它可以接收2个string类型的参数进行拼接,也可以接收2个number类型的参数进行相加
*/
// 重载函数声明
function add(x: string, y: string): string
function add(x: number, y: number): number
// 定义函数实现
function add(x: string | number, y: string | number): string | number {
// 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 x + y
if (typeof x === 'string' && typeof y === 'string') {
return x + y
} else if (typeof x === 'number' && typeof y === 'number') {
return x + y
}
}
console.log(add(1, 2))
console.log(add('a', 'b'))
// console.log(add(1, 'a')) // error
/*
访问修饰符: 用来描述类内部的属性/方法的可访问性
public: 默认值, 公开的外部也可以访问
private: 只能类内部可以访问
protected: 类内部和子类可以访问
特别注意: readonly:是关键字不是属性修饰符。关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
*/
抽象类:
抽象类做为其它派生类的基类使用。 它们不能被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。
/*
抽象类
不能创建实例对象, 只有实现类才能创建实例
可以包含未实现的抽象方法
*/
abstract class Animal {
abstract cry()
run() {
console.log('run()')
}
}
class Dog extends Animal {
cry() {
console.log(' Dog cry()')
}
}
const dog = new Dog()
dog.cry()
dog.run()
//不使用泛型
function createArray(value: any, count: number): any[] {
const arr: any[] = []
for (let index = 0; index < count; index++) {
arr.push(value)
}
return arr
}
const arr1 = createArray(11, 3)
const arr2 = createArray('aa', 3)
console.log(arr1[0].toFixed(), arr2[0].split(''))
================================================
//使用泛型
function createArray2<T>(value: T, count: number) {
const arr: Array<T> = []
for (let index = 0; index < count; index++) {
arr.push(value)
}
return arr
}
const arr3 = createArray2<number>(11, 3)
console.log(arr3[0].toFixed())
// console.log(arr3[0].split('')) // error
const arr4 = createArray2<string>('aa', 3)
console.log(arr4[0].split(''))
// console.log(arr4[0].toFixed()) // error
================================================
//一个函数可以定义多个泛型参数
function swap<K, V>(a: K, b: V): [K, V] {
return [a, b]
}
const result = swap<string, number>('abc', 123)
console.log(result[0].length, result[1].toFixed())
================================================
//在定义接口时, 为接口中的属性或方法定义泛型类型 在使用接口时, 再指定具体的泛型类型
interface IbaseCRUD<T> {
data: T[]
add: (t: T) => void
getById: (id: number) => T
}
class User {
id?: number //id主键自增
name: string //姓名
age: number //年龄
constructor(name, age) {
this.name = name
this.age = age
}
}
================================================
//泛型接口
class UserCRUD implements IbaseCRUD<User> {
data: User[] = []
add(user: User): void {
user = { ...user, id: Date.now() }
this.data.push(user)
console.log('保存user', user.id)
}
getById(id: number): User {
return this.data.find(item => item.id === id)
}
}
const userCRUD = new UserCRUD()
userCRUD.add(new User('tom', 12))
userCRUD.add(new User('tom2', 13))
console.log(userCRUD.data)
================================================
//泛型类,在定义类时, 为类中的属性或方法定义泛型类型 在创建类的实例时, 再指定特定的泛型类型
class GenericNumber<T> {
zeroValue: T
add: (x: T, y: T) => T
}
let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function(x, y) {
return x + y
}
let myGenericString = new GenericNumber<string>()
myGenericString.zeroValue = 'abc'
myGenericString.add = function(x, y) {
return x + y
}
console.log(myGenericString.add(myGenericString.zeroValue, 'test'))
console.log(myGenericNumber.add(myGenericNumber.zeroValue, 12))
================================================
//泛型约束,如果我们直接对一个泛型参数取 length 属性, 会报错, 因为这个泛型根本就不知道它有这个属性
// 没有泛型约束
function fn<T>(x: T): void {
// console.log(x.length) // error
}
//我们可以使用泛型约束来实现
interface Lengthwise {
length: number
}
// 指定泛型约束
function fn2<T extends Lengthwise>(x: T): void {
console.log(x.length)
}
//我们需要传入符合约束类型的值,必须包含必须 length 属性:
fn2('abc')
// fn2(123) // error number没有length属性
5、属性修饰符
1、可选属性
interface IPerson {
id: number
name: string
age: number
sex?: string
}
//带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。
//可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。
const person2: IPerson = {
id: 1,
name: 'tom',
age: 20
// sex: '男' // 可以没有
}
2、 只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly 来指定只读属性:
interface IPerson {
readonly id: number
name: string
age: number
sex?: string
}
//一旦赋值后再也不能被改变了。
const person2: IPerson = {
id: 2,
name: 'tom',
age: 20
// sex: '男' // 可以没有
// xxx: 12 // error 没有在接口中定义, 不能有
}
person2.id = 2 // error
总结:readonly vs const。最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用 readonly。
特别注意: readonly:是关键字不是属性修饰符。关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
3、函数类型
接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
/*
接口可以描述函数类型(参数的类型与返回的类型)
*/
interface SearchFunc {
(source: string, subString: string): boolean
}
这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。
const mySearch: SearchFunc = function(source: string, sub: string): boolean {
return source.search(sub) > -1
}
console.log(mySearch('abcd', 'bc'))
4、参数属性
在上面的例子中,我们必须在 Person 类里定义一个只读成员 name 和一个参数为 name 的构造函数,并且立刻将 name 的值赋给 this.name,这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Person 类的修改版,使用了参数属性
class Person2 {
constructor(readonly name: string) {}
}
const p = new Person2('jack')
console.log(p.name)
注意看我们是如何舍弃参数 name,仅在构造函数里使用 readonly name: string 参数来创建和初始化 name 成员。 我们把声明和赋值合并至一处。
参数属性通过给构造函数参数前面添加一个访问限定符来声明。使用 private 限定一个参数属性会声明并初始化一个私有成员;对于 public 和 protected 来说也是一样。
5、存取器
相当于java中实体类的get、set方法。TypeScript 支持通过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
下面来看如何把一个简单的类改写成使用 get 和 set。 首先,我们从一个没有使用存取器的例子开始。
class Person {
firstName: string = 'A'
lastName: string = 'B'
get fullName() {
return this.firstName + '-' + this.lastName
}
set fullName(value) {
const names = value.split('-')
this.firstName = names[0]
this.lastName = names[1]
}
}
const p = new Person()
console.log(p.fullName)
p.firstName = 'C'
p.lastName = 'D'
console.log(p.fullName)
p.fullName = 'E-F'
console.log(p.firstName, p.lastName)
6、静态属性
到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用 static 定义 origin,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在 origin 前面加上类名。 如同在实例属性上使用 this.xxx 来访问属性一样,这里我们使用 Grid.xxx 来访问静态属性。
注意:构造函数是不能通过static来进行修饰的
/*
静态属性, 是类对象的属性
非静态属性, 是类的实例对象的属性
*/
class Person {
name1: string = 'A'
static name2: string = 'B'
}
console.log(Person.name2)
console.log(new Person().name1)
6、声明文件
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
什么是声明语句?
假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过
但是在 ts 中,编译器并不知道 $ 或 jQuery 或$.ajax()
是什么东西。
/*
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
声明语句: 如果需要ts对新的语法进行检查, 需要要加载了对应的类型说明代码
declare var jQuery: (selector: string) => any;
声明文件: 把声明语句放到一个单独的文件(jQuery.d.ts)中, ts会自动解析到项目中所有声明文件
下载声明文件: npm install @types/jquery --save-dev
*/
jQuery('#foo')
// ERROR: Cannot find name 'jQuery'.
这时,我们需要使用 declare var 来定义它的类型
declare var jQuery: (selector: string) => any
declare function $(n:string):any;
declare namespace ${
function ajax():void;
}
jQuery('#foo')
declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:
jQuery('#foo')
一般声明文件都会单独写成一个 xxx.d.ts 文件
创建 01_jQuery.d.ts, 将声明语句定义其中, TS 编译器会扫描并加载项目中所有的 TS 声明文件
declare var jQuery: (selector: string) => any
declare function $(n:string):any;
declare namespace ${
function ajax():void;
}
很多的第三方库都定义了对应的声明文件库, 库文件名一般为 @types/xxx, 可以在 https://www.npmjs.com/package/package 进行搜索
有的第三库在下载时就会自动下载对应的声明文件库(比如: webpack),有的可能需要单独下载(比如 jQuery/react)。
二、vite构建工具
两种打包工具webpack和vite,但是vite比webpack快很多性能也好很多
1、vite2创建项目
需要node 12.0以上版本
输入命令 npm create vite
三、vue3.0新特性
Vue2和Vue3的相比较而言的相关的面试题
1、2020年9月发布的正式版
2、Vue3支持大多数的Vue2的特性
3、Vue中设计了一套强大的组合APi代替了Vue2中的option API ,复用性更强了
4、更好的支持TS。最主要: Vue3中使用了proxy配合Reflect代替了Vue2中object.defineProperty()方法实现数据的响应式(数据代)
5、重写了虚拟DOM,速度更快了
6、新的组件: Fragment(片段) Teleport(瞬移) Suspense(不确定)设计了一个新的脚手架工具,vite
1、高亮检测插件不同
- 搭建好vue3.0的项目框架后会出现一堆高亮下划线报错,原因是vue语法检测高亮提示由vetur改成了volar,所以要先禁用或卸载vetur
2、配置文件不同
3、vue3的setup及其语法
1. setup细节介绍
- setup 执行的时机:
在 beforeCreate 之前执行(一次), 此时组件对象还没有创建
this 是 undefined, 不能通过 this 来访问 data/computed/methods / props
其实所有的 composition API 相关回调函数中也都不可以 - setup 的返回值
一般都返回一个对象: 为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性/方法
返回对象中的属性会与 data 函数返回对象的属性合并成为组件对象的属性
返回对象中的方法会与 methods 中的方法合并成功组件对象的方法
如果有重名, setup 优先
注意:
一般不要混合使用: methods 中可以访问 setup 提供的属性和方法, 但在 setup 方法中不能访问 data 和 methods
setup 不能是一个 async 函数: 因为返回值不再是 return 的对象, 而是 promise, 模板看不到 return 对象中的属性数据 - setup 的参数
setup(props, context) / setup(props, {attrs, slots, emit})
props: 包含 props 配置声明且传入了的所有属性的对象
context包含了attrs, slots, emit,是一个对象,里面有attrs对象(获取当前组件标签上的所有的属性的对象,但是该属性是在props中没有声明接收的所有的尚需经的对象),emit方法(分发事件的),slots对象(插槽)
attrs: 包含没有在 props 配置中声明的属性的对象, 相当于this.$attrs
slots: 包含所有传入的插槽内容的对象, 相当于this.$slots
emit: 用来分发自定义事件的函数, 相当于this.$emit
<template>
<h2>App</h2>
<p>msg: {{ msg }}</p>
<button @click="fn('--')">更新</button>
<child :msg="msg" msg2="cba" @fn="fn" />
</template>
<script lang="ts">
import { reactive, ref } from 'vue'
import child from './child.vue'
export default {
components: {
child
},
setup() {
const msg = ref('abc')
function fn(content: string) {
msg.value += content
}
return {
msg,
fn
}
}
}
</script>
<template>
<div>
<h3>{{ n }}</h3>
<h3>{{ m }}</h3>
<h3>msg: {{ msg }}</h3>
<h3>msg2: {{ $attrs.msg2 }}</h3>
<slot name="xxx"></slot>
<button @click="update">更新</button>
</div>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue'
export default defineComponent({
name: 'child',
props: ['msg'],
emits: ['fn'], // 可选的, 声明了更利于程序员阅读, 且可以对分发的事件数据进行校验
data() {
console.log('data', this)
return {
// n: 1
}
},
beforeCreate() {
console.log('beforeCreate', this)
},
methods: {
// update () {
// this.n++
// this.m++
// }
},
// setup (props, context) {
setup(props, { attrs, emit, slots }) {
console.log('setup', this)
console.log(props.msg, attrs.msg2, slots, emit)
const m = ref(2)
const n = ref(3)
function update() {
// console.log('--', this)
// this.n += 2
// this.m += 2
m.value += 2
n.value += 2
// 分发自定义事件
emit('fn', '++')
}
return {
m,
n,
update
}
}
})
</script>
2. vue2.0和vue3的响应式数据定义方式不同
(1)、ref的两个用法
(1-1)ref:定义一个数据的响应式。一般用来定义一个基本类型的响应式数据。
ref 的数据操作: 在 js 中要.value, 在模板中不需要(内部解析模板时会自动添加.value)。
ref 内部: 通过给 value 属性添加 getter/setter 来实现对数据的劫持
语法: const xxx = ref(initValue):创建一个包含响应式数据的引用(reference)对象
js 中操作数据: xxx.value
模板中操作数据: 不需要.value
<template>
<h2>{{ count }}</h2>
<hr />
<button @click="update">更新</button>
</template>
<script>
import { ref } from 'vue'
export default {
/* 在Vue3中依然可以使用data和methods配置, 但建议使用其新语法实现 */
// data () {
// return {
// count: 0
// }
// },
// methods: {
// update () {
// this.count++
// }
// }
/* 使用vue3的composition API */
setup() {
// 定义响应式数据 ref对象
const count = ref(1)
console.log(count)
// 更新响应式数据的函数
function update() {
// alert('update')
count.value = count.value + 1
}
return {
count,
update
}
}
}
</script>
(1-2)、ref 获取元素,相当于vue2中的this.$ref.xxx
利用 ref 函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
<template>
<h2>App</h2>
<input type="text" />
---
<input type="text" ref="inputRef" />
</template>
<script lang="ts">
import { onMounted, ref } from 'vue'
/*
ref获取元素: 利用ref函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
注意:接收的参数inputRef必须与标签中的ref="inputRef"名称保持一致
*/
export default {
setup() {
const inputRef = ref<HTMLElement | null>(null)
onMounted(() => {
inputRef.value && inputRef.value.focus()
})
return {
inputRef
}
}
}
</script>
(2)reactive:作用: 定义多个数据的响应式,一般用于对象;其次,如果用 ref 声明对象/数组, 内部会自动将对象/数组转换为 reactive 的代理对象。
const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
reactive 内部: 通过使用 Proxy 来实现对对象内部所有数据的劫持, 并通过 Reflect 操作对象内部数据。
响应式转换是“深层的”:会影响对象内部所有嵌套的属性
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
<template>
<h2>name: {{ state.name }}</h2>
<h2>age: {{ state.age }}</h2>
<h2>wife: {{ state.wife }}</h2>
<hr />
<button @click="update">更新</button>
</template>
<script>
/*
reactive:
作用: 定义多个数据的响应式
const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
响应式转换是“深层的”:会影响对象内部所有嵌套的属性
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
*/
import { reactive } from 'vue'
export default {
setup() {
/*
定义响应式数据对象
*/
const state = reactive({
name: 'tom',
age: 25,
wife: {
name: 'marry',
age: 22
}
})
console.log(state, state.wife)
const update = () => {
state.name += '--'
state.age += 1
state.wife.name += '++'
state.wife.age += 2
}
return {
state,
update
}
}
}
</script>
(3)总结:比较 Vue2 与 Vue3 的响应式。
(3-1)vue2 的响应式。对象: 通过 defineProperty 对对象的已有属性值的读取和修改进行劫持(监视/拦截);数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
Object.defineProperty(data, 'count', {
get() {},
set() {}
})
问题:对象直接新添加的属性或删除已有属性, 界面不会自动更新;
直接通过下标替换元素或更新 length, 界面不会自动更新 arr[1] = {}。
(3-2)Vue3 的响应式:通过 Proxy(代理): 拦截对 data 任意属性的任意(13 种)操作, 包括属性值的读写, 属性的添加, 属性的删除等…;通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作,后面所有的操作都是通过代理对象来操作被代理对象内部属性
(4)、toRefs:把一个响应式对象转换成普通对象
把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref。比如普通响应式对象使用解构语法糖(let obj={ foo: 'a', bar: 'b' }; let {foo1,bar1}= {...obj}
);后,每一个结构出来的属性foo1、bar1就不再是响应式数据了;
应用: 当从合成函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用。
问题: reactive 对象取出的所有属性值都是非响应式的。
解决: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性。
<template>
<h2>App</h2>
<h3>foo: {{ foo }}</h3>
<h3>bar: {{ bar }}</h3>
<h3>foo2: {{ foo2 }}</h3>
<h3>bar2: {{ bar2 }}</h3>
</template>
<script lang="ts">
import { reactive, toRefs } from 'vue'
/*
toRefs:
将响应式对象中所有属性包装为ref对象, 并返回包含这些ref对象的普通对象
应用: 当从合成函数返回响应式对象时,toRefs 非常有用,
这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
*/
export default {
setup() {
const state = reactive({
foo: 'a',
bar: 'b'
})
const stateAsRefs = toRefs(state)
setTimeout(() => {
state.foo += '++'
state.bar += '++'
}, 2000)
const { foo2, bar2 } = useReatureX()
return {
// ...state,
...stateAsRefs,
foo2,
bar2
}
}
}
function useReatureX() {
const state = reactive({
foo2: 'a',
bar2: 'b'
})
setTimeout(() => {
state.foo2 += '++'
state.bar2 += '++'
}, 2000)
return toRefs(state)
}
</script>
3. 子组件引入即可,无需声明
4. 数据无需export和return
5. v-bind可用于css样式绑定
6. 计算属性与监视
(1)computed 函数:与 computed 配置功能一致;只有 getter;有 getter 和 setter。
(2)watch 函数:与 watch 配置功能一致;监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调;默认初始时不执行回调, 但可以通过配置 immediate 为 true, 来指定初始时立即执行第一次;通过配置 deep 为 true, 来指定深度监视。
(3)watchEffect 函数:不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据;默认初始时就会执行第一次, 从而可以收集需要监视的数据;监视数据发生变化时回调。注意,此方法只能收集同步代码的依赖
代码如下:
<template>
<h2>App</h2>
fistName:
<input v-model="user.firstName" />
<br />
lastName:
<input v-model="user.lastName" />
<br />
fullName1:
<input v-model="fullName1" />
<br />
fullName2:
<input v-model="fullName2" />
<br />
fullName3:
<input v-model="fullName3" />
<br />
</template>
<script lang="ts">
/*
计算属性与监视
1. computed函数:
与computed配置功能一致
只有getter
有getter和setter
2. watch函数
与watch配置功能一致
监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
默认初始时不执行回调, 但可以通过配置immediate为true, 来指定初始时立即执行第一次
通过配置deep为true, 来指定深度监视
3. watchEffect函数
不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
默认初始时就会执行第一次, 从而可以收集需要监视的数据
监视数据发生变化时回调
如果是异步监听,await前面的数据才会被收集吗,比如:
let res = ref("")
let a = ref(1)
let b = ref(2)
watchEffect( async () => {
a=a*2; //可以被监听收集
res.value = await axios.get(url)
b=b*2; //await之后的数据都不能被监听
})
*/
import { reactive, ref, computed, watch, watchEffect } from 'vue'
export default {
setup() {
const user = reactive({
firstName: 'A',
lastName: 'B'
})
// 只有getter的计算属性
const fullName1 = computed(() => {
console.log('fullName1')
return user.firstName + '-' + user.lastName
})
// 有getter与setter的计算属性
const fullName2 = computed({
get() {
console.log('fullName2 get')
return user.firstName + '-' + user.lastName
},
set(value: string) {
console.log('fullName2 set')
const names = value.split('-')
user.firstName = names[0]
user.lastName = names[1]
}
})
const fullName3 = ref('')
/*
watchEffect: 监视所有回调中使用的数据
*/
/*
watchEffect(() => {
console.log('watchEffect')
fullName3.value = user.firstName + '-' + user.lastName
})
*/
/*
使用watch的2个特性:
深度监视
初始化立即执行
*/
watch(
user,
() => {
fullName3.value = user.firstName + '-' + user.lastName
},
{
immediate: true, // 是否初始化立即执行一次, 默认是false
deep: true // 是否是深度监视, 默认是false
}
)
/*
watch一个数据
默认在数据发生改变时执行回调
*/
watch(fullName3, value => {
console.log('watch')
const names = value.split('-')
user.firstName = names[0]
user.lastName = names[1]
})
/*
watch多个数据:
使用数组来指定
如果是ref对象, 直接指定
如果是reactive对象中的属性, 必须通过函数来指定
*/
watch([() => user.firstName, () => user.lastName, fullName3], values => {
console.log('监视多个数据', values)
})
return {
user,
fullName1,
fullName2,
fullName3
}
}
}
</script>
7. vue2、vue3生命周期和钩子函数对比
生命周期的各钩子函数与 2.x 版本生命周期相对应的组合式 API对比
beforeCreate -> 使用 setup()
created -> 使用 setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured
新增的钩子函数,组合式 API 还提供了以下调试钩子函数:
onRenderTracked
onRenderTriggered
使用代码如下:
<template>
<div class="about">
<h2>msg: {{ msg }}</h2>
<hr />
<button @click="update">更新</button>
</div>
</template>
<script lang="ts">
import { ref, onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from 'vue'
export default {
beforeCreate() {
console.log('beforeCreate()')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
beforeUnmount() {
console.log('beforeUnmount')
},
unmounted() {
console.log('unmounted')
},
setup() {
const msg = ref('abc')
const update = () => {
msg.value += '--'
}
onBeforeMount(() => {
console.log('--onBeforeMount')
})
onMounted(() => {
console.log('--onMounted')
})
onBeforeUpdate(() => {
console.log('--onBeforeUpdate')
})
onUpdated(() => {
console.log('--onUpdated')
})
onBeforeUnmount(() => {
console.log('--onBeforeUnmount')
})
onUnmounted(() => {
console.log('--onUnmounted')
})
return {
msg,
update
}
}
}
</script>
执行结果:vue3.x的生命周期比vue2.x的快
生命周期图对比:由图可知,就两处不同.1、vue2的beforeDestroy在vue3为beforeUnmount。2、vue2的destroyed在vue3为unmounted
8. 自定义 hook 函数,类似于 vue2 中的 mixin 技术
- 使用 Vue3 的组合 API 封装的可复用的功能函数
- 自定义 hook 的作用类似于 vue2 中的 mixin 技术
- 自定义 Hook 的优势: 很清楚复用功能代码的来源, 更清楚易懂
(1)案例一,需求 1: 收集用户鼠标点击的页面坐标
新建 hooks/useMousePosition.ts,代码如下:
import { ref, onMounted, onUnmounted } from 'vue'
/*
收集用户鼠标点击的页面坐标
*/
export default function useMousePosition() {
// 初始化坐标数据
const x = ref(-1)
const y = ref(-1)
// 用于收集点击事件坐标的函数
const updatePosition = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}
// 挂载后绑定点击监听
onMounted(() => {
document.addEventListener('click', updatePosition)
})
// 卸载前解绑点击监听
onUnmounted(() => {
document.removeEventListener('click', updatePosition)
})
return { x, y }
}
调用方法代码:
<template>
<div>
<h2>x: {{ x }}, y: {{ y }}</h2>
</div>
</template>
<script>
import { ref } from 'vue'
/*
在组件中引入并使用自定义hook
自定义hook的作用类似于vue2中的mixin技术
自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂
*/
import useMousePosition from './hooks/useMousePosition'
export default {
setup() {
const { x, y } = useMousePosition()
return {
x,
y
}
}
}
</script>
(2)案例二,需求 2: 封装发 ajax 请求的 hook 函数
新建 hooks/useRequest.ts
import { ref } from 'vue'
import axios from 'axios'
/*
使用axios发送异步ajax请求
*/
export default function useUrlLoader<T>(url: string) {
const result = ref<T | null>(null)
const loading = ref(true)
const errorMsg = ref(null)
axios
.get(url)
.then(response => {
loading.value = false
result.value = response.data
})
.catch(e => {
loading.value = false
errorMsg.value = e.message || '未知错误'
})
return {
loading,
result,
errorMsg
}
}
调用
<template>
<div class="about">
<h2 v-if="loading">LOADING...</h2>
<h2 v-else-if="errorMsg">{{ errorMsg }}</h2>
<!-- <ul v-else>
<li>id: {{result.id}}</li>
<li>name: {{result.name}}</li>
<li>distance: {{result.distance}}</li>
</ul> -->
<ul v-for="p in result" :key="p.id">
<li>id: {{ p.id }}</li>
<li>title: {{ p.title }}</li>
<li>price: {{ p.price }}</li>
</ul>
<!-- <img v-if="result" :src="result[0].url" alt=""> -->
</div>
</template>
<script lang="ts">
import { watch } from 'vue'
import useRequest from './hooks/useRequest'
// 地址数据接口
interface AddressResult {
id: number
name: string
distance: string
}
// 产品数据接口
interface ProductResult {
id: string
title: string
price: number
}
export default {
setup() {
// const {loading, result, errorMsg} = useRequest<AddressResult>('/data/address.json')
const { loading, result, errorMsg } = useRequest<ProductResult[]>('/data/products.json')
watch(result, () => {
if (result.value) {
console.log(result.value.length) // 有提示
}
})
return {
loading,
result,
errorMsg
}
}
}
</script>
9. Composition API(其它部分)
(1)、shallowReactive 与 shallowRef
- shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)
- shallowRef: 只处理了 value 的响应式, 不进行对象的 reactive 处理
- 什么时候用浅响应式呢?
一般情况下使用 ref 和 reactive 即可
如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
<template>
<h2>App</h2>
<h3>m1: {{ m1 }}</h3>
<h3>m2: {{ m2 }}</h3>
<h3>m3: {{ m3 }}</h3>
<h3>m4: {{ m4 }}</h3>
<button @click="update">更新</button>
</template>
<script lang="ts">
import { reactive, ref, shallowReactive, shallowRef } from 'vue'
/*
shallowReactive与shallowRef
shallowReactive: 只处理了对象内最外层属性的响应式(也就是浅响应式)
shallowRef: 只处理了value的响应式, 不进行对象的reactive处理
总结:
reactive与ref实现的是深度响应式, 而shallowReactive与shallowRef是浅响应式
什么时候用浅响应式呢?
一般情况下使用ref和reactive即可,
如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
*/
export default {
setup() {
const m1 = reactive({ a: 1, b: { c: 2 } })
const m2 = shallowReactive({ a: 1, b: { c: 2 } })
const m3 = ref({ a: 1, b: { c: 2 } })
const m4 = shallowRef({ a: 1, b: { c: 2 } })
const update = () => {
// m1.b.c += 1
// m2.b.c += 1
// m3.value.a += 1
m4.value.a += 1
}
return {
m1,
m2,
m3,
m4,
update
}
}
}
</script>
(2)、readonly 与 shallowReadonly
- readonly:
- 深度只读数据
- 获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
- 只读代理是深层的:访问的任何嵌套 property 也是只读的。
- shallowReadonly
- 浅只读数据
- 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换,比如
{a: 1,b: {c: 2 } }
,属性a、b是只读的,b中的c属性不是只读的。 - 应用场景:在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
<template>
<h2>App</h2>
<h3>{{ state }}</h3>
<button @click="update">更新</button>
</template>
<script lang="ts">
import { reactive, readonly, shallowReadonly } from 'vue'
/*
readonly: 深度只读数据
获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
只读代理是深层的:访问的任何嵌套 property 也是只读的。
shallowReadonly: 浅只读数据
创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换
应用场景:
在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
*/
export default {
setup() {
const state = reactive({
a: 1,
b: {
c: 2
}
})
// const rState1 = readonly(state)
const rState2 = shallowReadonly(state)
const update = () => {
// rState1.a++ // error
// rState1.b.c++ // error
// rState2.a++ // error
rState2.b.c++
}
return {
state,
update
}
}
}
</script>
(3)、toRaw 与 markRaw
- toRaw
- 返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。
- 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。
- markRaw
- 标记一个对象,使其永远不会转换为代理。返回对象本身
- 应用场景:有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
<template>
<h2>{{ state }}</h2>
<button @click="testToRaw">测试toRaw</button>
<button @click="testMarkRaw">测试markRaw</button>
</template>
<script lang="ts">
/*
toRaw: 得到reactive代理对象的目标数据对象
*/
import { markRaw, reactive, toRaw } from 'vue'
export default {
setup() {
const state = reactive<any>({
name: 'tom',
age: 25
})
const testToRaw = () => {
const user = toRaw(state)
user.age++ // 界面不会更新
}
const testMarkRaw = () => {
const likes = ['a', 'b']
// state.likes = likes
state.likes = markRaw(likes) // likes数组就不再是响应式的了
setTimeout(() => {
state.likes[0] += '--'
}, 1000)
}
return {
state,
testToRaw,
testMarkRaw
}
}
}
</script>
(4)、 toRef
- 为源响应式对象上的某个属性创建一个 ref 对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
- 区别 ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
- 应用: 当要将 某个 prop 的 ref 传递给复合函数时,toRef 很有用
<template>
<h2>App</h2>
<p>{{ state }}</p>
<p>{{ foo }}</p>
<p>{{ foo2 }}</p>
<button @click="update">更新</button>
<Child :foo="foo" />
</template>
<script lang="ts">
/*
toRef:
为源响应式对象上的某个属性创建一个 ref对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
应用: 当要将某个 prop 的 ref 传递给复合函数时,toRef 很有用
*/
import { reactive, toRef, ref } from 'vue'
import Child from './Child.vue'
export default {
setup() {
const state = reactive({
foo: 1,
bar: 2
})
const foo = toRef(state, 'foo')
const foo2 = ref(state.foo)
const update = () => {
state.foo++
// foo.value++
// foo2.value++ // foo和state中的数据不会更新
}
return {
state,
foo,
foo2,
update
}
},
components: {
Child
}
}
</script>
<template>
<h2>Child</h2>
<h3>{{ foo }}</h3>
<h3>{{ length }}</h3>
</template>
<script lang="ts">
import { computed, defineComponent, Ref, toRef } from 'vue'
const component = defineComponent({
props: {
foo: {
type: Number,
require: true
}
},
setup(props, context) {
const length = useFeatureX(toRef(props, 'foo'))
return {
length
}
}
})
function useFeatureX(foo: Ref) {
const lenth = computed(() => foo.value.length)
return lenth
}
export default component
</script>
(5)、customRef
- 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
- 需求: 使用 customRef 实现 debounce 的示例
<template>
<h2>App</h2>
<input v-model="keyword" placeholder="搜索关键字" />
<p>{{ keyword }}</p>
</template>
<script lang="ts">
/*
customRef:
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
需求:
使用 customRef 实现 debounce 的示例
*/
import { ref, customRef } from 'vue'
export default {
setup() {
//如果需要实时响应,不做延迟,就直接用const keyword =ref(''),customRef 就是自己去扩展下ref()
const keyword = useDebouncedRef('', 500)
console.log(keyword)
return {
keyword
}
}
}
/*
实现函数防抖的自定义ref
*/
function useDebouncedRef<T>(value: T, delay = 200) {
let timeout: number
return customRef((track, trigger) => {
return {
get() {
// 告诉Vue追踪数据
track()
return value
},
set(newValue: T) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
// 告诉Vue去触发界面更新
trigger()
}, delay)
}
}
})
}
</script>
(6)、provide 与 inject
-
provide和inject提供依赖注入,功能类似 2.x 的provide/inject
-
实现跨层级组件(祖孙)间通信
<template>
<h1>父组件</h1>
<p>当前颜色: {{ color }}</p>
<button @click="color = 'red'">红</button>
<button @click="color = 'yellow'">黄</button>
<button @click="color = 'blue'">蓝</button>
<hr />
<Son />
</template>
<script lang="ts">
import { provide, ref } from 'vue'
/*
- provide` 和 `inject` 提供依赖注入,功能类似 2.x 的 `provide/inject
- 实现跨层级组件(祖孙)间通信
*/
import Son from './Son.vue'
export default {
name: 'ProvideInject',
components: {
Son
},
setup() {
const color = ref('red')
provide('color', color)
return {
color
}
}
}
</script>
<template>
<div>
<h2>子组件</h2>
<hr />
<GrandSon />
</div>
</template>
<script lang="ts">
import GrandSon from './GrandSon.vue'
export default {
components: {
GrandSon
}
}
</script>
<template>
<h3 :style="{ color }">孙子组件: {{ color }}</h3>
</template>
<script lang="ts">
import { inject } from 'vue'
export default {
setup() {
const color = inject('color')
return {
color
}
}
}
</script>
(7)、响应式数据的判断
- isRef: 检查一个值是否为一个 ref 对象
- isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
- isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
- isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理
10. 新组件,vue2需要div根标签,vue3不需要
(1)、Fragment(片断)
- 在 Vue2 中: 组件必须有一个根标签
- 在 Vue3 中: 组件可以没有根标签, 内部会将多个标签包含在一个 Fragment 虚拟元素中
- 好处: 减少标签层级, 减小内存占用
<template>
<h2>aaaa</h2>
<h2>aaaa</h2>
</template>
(2)、Teleport(瞬移)
- 个人理解:通过
<Teleport ></Teleport to="某个html的标签名称">
标签中的to属性将Teleport 标签中的所有内容挂到某个html的标签里面。 - Teleport 提供了一种干净的方法, 让组件的 html 在父组件界面外的特定标签(很可能是 body)下插入显示ModalButton.vue。
<template>
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal! (My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'modal-button',
setup() {
const modalOpen = ref(false)
return {
modalOpen
}
}
}
</script>
<style>
.modal {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.modal div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: white;
width: 300px;
height: 300px;
padding: 5px;
}
</style>
App.vue
<template>
<h2>App</h2>
<modal-button></modal-button>
</template>
<script lang="ts">
import ModalButton from './ModalButton.vue'
export default {
setup() {
return {}
},
components: {
ModalButton
}
}
</script>
3、Suspense(不确定的)
它们允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验
<template>
<Suspense>
<template v-slot:default>
<AsyncComp />
<!-- <AsyncAddress/> -->
</template>
<template v-slot:fallback>
<h1>LOADING...</h1>
</template>
</Suspense>
</template>
<script lang="ts">
/*
异步组件 + Suspense组件
*/
// import AsyncComp from './AsyncComp.vue'
import AsyncAddress from './AsyncAddress.vue'
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
export default {
setup() {
return {}
},
components: {
AsyncComp,
AsyncAddress
}
}
</script>
AsyncComp.vue
<template>
<h2>AsyncComp22</h2>
<p>{{ msg }}</p>
</template>
<script lang="ts">
export default {
name: 'AsyncComp',
setup() {
// return new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve({
// msg: 'abc'
// })
// }, 2000)
// })
return {
msg: 'abc'
}
}
}
</script>
AsyncAddress.vue
<template>
<h2>{{ data }}</h2>
</template>
<script lang="ts">
import axios from 'axios'
export default {
async setup() {
const result = await axios.get('/data/address.json')
return {
data: result.data
}
}
}
</script>
4、父子之间传参的区别
1、父传子
-
vue2.0
-
vue3.0(含js语法到ts语法的转换优化),下面是子组件接收的写法,父组件写法和2.0一样
父组件传参
2、子传父
- vue2.0
- vue3.0使用defineEmits(js语法)
- ts语法(一般来说vue3.0都写ts语法的)
- vue3.0使用defineEmits(ts语法+v-model语法糖)
5、插槽:useSlots和useAttrs
useSlots使用案例
useSlots和useAttrs一起使用
6、对外暴露属性defineExpose
在vue2.0中,子直接调用父组件属性或方法、或者父调用子的,我们使用this.$parent.event
或者this.$children[0]
、this.$refs.xxx
来直接调用
在vue3.0中,使用defineExpose暴露出去才可获取使用,如下图示例:
示例一:
示例二:
四、vue3.0+typescript实战:后台管理系统
参考博客:vue3.0实战
一、技术栈
语法演变,vue3.0更加简便!
二、功能架构
三、框架搭建
- vite2创建项目
- vite2项目结构如下:vue版本为3.2.xx
- 或者使用vue cli3构建项目,输入命令
vue create xxx
,选择语法为vue3.0 .插件包括 router、vuex
四、安装插件
如果用vue-cli3搭建的时候勾选了这些插件,就不用下面的步骤,手动去安装,可以直接使用
1、路由插件vue-router4安装使用
1、输入命令npm i vue-router@4
安装 vue-router4
2、新建/router/index.ts配置路由
3、路由配置代码可以参考下面的
//router/index.ts代码
//注意,这里不建议使用createWebHistory,这样二级路由跳转的时候引入外部js会失效,
//使用createWebHashHistory会在地址栏加上#号以欺骗浏览器,地址的改变是由于正在进行页内导航 ,避免了js失效的问题。
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/helloworld',
name: 'helloworld',
component: () => import('../components/HelloWorld.vue')
}
]
const router = createRouter({
history: createWebHashHistory(process.env.BASE_URL),
routes
})
export default router
//main.js代码
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
createApp(App).use(store).use(router).mount('#app')
4、使用路由router4,路由跳转实现代码参考下面的(@click=“toPath(menu.path)”):
<!-- -->
<template>
<template v-for="menu in menus" :key="menu.path">
<el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.index">
<template #title>
<el-icon>
<component :is="menu.meta.icon" />
</el-icon>
<span>{{ menu.meta.title }}</span>
</template>
<menu-item :menus="menu.children"></menu-item>
</el-sub-menu>
<el-menu-item v-else :index="menu.index" @click="toPath(menu.path)">
<el-icon>
<component :is="menu.meta?.icon" />
</el-icon>
<span>{{ menu.meta?.title }}</span>
</el-menu-item>
</template>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
import { useRouter } from 'vue-router';
defineProps(['menus'])
const router = useRouter()
const toPath = (item: string) => {
router.push({ path: item });
}
</script>
<style lang="scss" scoped>
.el-sub-menu {
.el-menu-item {
background-color: #312a2a;
}
}
</style>
5、获取路由route的操作如下,比如监听(包括监听函数watch的用法)路由路径变化并获取所有路由信息,具体代码如下(import { useRoute } from ‘vue-router’;)
<!-- 布局:头部 -->
<template>
<div>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">homepage</el-breadcrumb-item>
<el-breadcrumb-item>
<a href="/">promotion management</a>
</el-breadcrumb-item>
<el-breadcrumb-item>promotion list</el-breadcrumb-item>
<el-breadcrumb-item>promotion detail</el-breadcrumb-item>
</el-breadcrumb>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { ref, watch } from 'vue';
const route = useRoute();
const breadcrumb = ref([]);
const getBreadcrumb = () => {
console.log("===>", route.matched);
}
watch(() => route.path, () => {
getBreadcrumb()
})
</script>
<style lang="scss" scoped>
</style>
上面的useRouter和useRoute的区别和vue2.0一样,区别:
this.$router是指所有路由,比如跳转路由地址时可使用
this.$router.push(‘/user’)或this.$router.replace(‘/home’)进行页面跳转.
This.$route是指当前路由(地址栏里的),可获取当前地址上的参数
this.$route.params.xxx
2、vuex4的安装(状态管理库选型①)
- 安装
npm i vuex@4
或者npm i vuex@next
- src下新建store目录,新建index.ts,写入如下内容,里面定义了一个数字类型count来测试使用
import { createStore } from 'vuex'
interface State{
count:number
}
export default createStore<State>({
state() {
return {
count:0
}
},
mutations: {
increment(state){
state.count++
}
},
actions: {
},
modules: {
}
})
- 在main.ts里面全局挂载vuex的store
import store from './store'
app.use(store)
- 组件中使用案例:
<template>
<div class="hello">
<button @click="increment">{{count}}</button>
</div>
</template>
<script setup>
import{useStore} from 'vuex'
const store=useStore()
const count=computed(()=>{
return store.state.count
})
const increment=()=>{
store.commit('increment')
}
</script>
<style scoped>
</style>
3、Pinia(状态管理库选型②)
Pinia是vue的专属状态管理库,它允许你跨组件或页面共享状态
- 安装pinia
npm install pinia
- 在vue应用实例中使用pinia
(1)、在main.ts文件中引入
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)
(2)、在pinia中存储全局数据token
import { defineStore } from "pinia";
import {ref} from 'vue';
/*
defineStore参数描述:
第一个参数:给状态起名,具有唯一性
第二个参数:函数,可以把定义该状态中拥有的内容
defineStore返回值描述:
返回的是一个函数,将来可以调用该函数,得到第二个参数中返回的内容
*/
export const useTokenStore = defineStore('token',()=>{
//1.定义描述token
const token = ref('')
//2.定义修改token的方法
const setToken = (newToken)=>{
token.value = newToken
}
//3.定义移除token的方法
const removeToken = ()=>{
token.value=''
}
return {
token,setToken,removeToken
}
}
)
(3)、在某个组件中获取pinia的token
//导入token状态
import { useTokenStore } from '@/stores/token.js'
//调用useTokenStore得到状态
const tokenStore = useTokenStore();
//tokenStore.token
//tokenStore.setToken()
//tokenStore.removeToken()
注意:不管是vuex还是pinia,手动页面刷新后都会丢失数据,这个时候就需要对pinia的数据进行持久化,解决方案有存session、cookie或者使用Pinia持久化插件persist
。
4、Pinia持久化插件 persist
Pinia默认是内存存储,当刷新浏览器的时候会丢失数据。
Persist插件可以将pinia中的数据持久化的存储
- 安装persist
npm i pinia-plugin-persistedstate
- 在pinia中使用persist,main.ts中加上下面代码
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
- 定义状态Store时指定持久化配置参数
import { defineStore } from 'pinia'
export const useStore = defineStore(
'main',
() => {
const someState = ref('你好 pinia')
return { someState }
},
{
persist: true,
},
)
5、安装sass和element plus
- vite安装步骤,可以参考官方文档:element plus
安装成功后可找到相关版本编号,如下图
- vue-cli安装使用步骤
1、安装插件,输入以下命令npm install element-plus --save
、
npm install sass-loader sass -D
、
npm install -D unplugin-vue-components unplugin-auto-import
2、全局引用element-plus并配置。在main.ts里面写入如下关于element-plus的代码:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import locale from 'element-plus/dist/locale/zh-cn.js'//导入中文包
const app = createApp(App)
for (const name in ElIcons) {
app.component(name, (ElIcons as any)[name])//全局使用icon图标
}
createApp(App).use(store).use(router).use(ElementPlus,{locale}).mount('#app')
在组件中使用,代码和效果如下
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h1>{{ obj.name }}</h1>
<h1>{{ obj.age }}</h1>
<h1>{{ sum }}</h1>
<button @click="increment">{{count}}</button>
<el-button @click="increment">{{count}}</el-button>
</div>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import{useStore} from 'vuex'
const msg = ref(123);
const obj = reactive({ name: "Eric", age: 12 });
const sum=computed(()=>{
return msg.value-obj.age
})
const store=useStore()
const count=computed(()=>{
return store.state.count
})
const increment=()=>{
store.commit('increment')
}
</script>
<style scoped>
h1 {
margin: 40px 0 0;
}
</style>
五、系统开发
1、页面布局
大致位置如下:
开发步骤:
1、将官网的布局代码写入组件
<template>
<div class="common-layout">
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup>
</script>
<style lang="scss">
.common-layout{
display: flex;
height: 100vh;
.el-header, .el-footer {
background-color: #B3C0D1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
background-color: #E9EEF3;
color: #333;
text-align: center;
line-height: 160px;
}
}
</style>
2、启动后如出现报错,Syntax Error: TypeError: this.getOptions is not a function
,则是上面安装sass的版本太高,卸载npm uninstall sass-loader sass -D
后依次安装如下插件即可
npm install node-sass@^7.0.1
npm install sass@^1.49.9
npm install sass-loader@^7.3.1
3、一般为了便于统一管理项目的样式,我们需要定义一些全局变量。如果你需要使用sass/scss语法定义一些全局的内容需要在项目根目录的vue.config.js文件(如果没有这个文件直接创建一个即可)下加上如下代码:注意了旧版本的sass-loader使用的是data字段,新版本的sass-loader使用的是prependData字段,我用的版本是旧版的。
module.exports = {
css: {
loaderOptions: {
sass: {
// 这里的值是你的全局变量文件路径,如果有多个全局变量用逗号分割开即可,如:
// data: `@import"~@/assets/scss/main.scss";@import"~@/assets/scss/mixins/util.scss"`
data: '@import "~@/style/global.scss";'
}
}
}
4、页面效果如下
5、优化页面样式
5-1、去掉App.vue的#app的所有样式
5-2、项目public文件夹下的index.html中加入
<style>
html,body{
margin: 0;
}
</style>
2、侧边栏开发
后面的直接看源码:
源码地址:github的源码地址
3、全局使用动态的icon图标
3-1、 按需引用图标
使用的是如下代码,使用什么引用什么:
<el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<template #title>Navigator Two</template>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon><Document /></el-icon>
<template #title>Navigator Three</template>
</el-menu-item>
import {
Document,
Menu as IconMenu,
Location,
Setting,
} from "@element-plus/icons-vue";
3-2、全局使用动态的icon
3-2-1、安装依赖
npm install @element-plus/icons
3-2-2、main.ts引入使用,代码如下:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElIcons from '@element-plus/icons'
//全局使用动态的icon
const app = createApp(App)
for (const name in ElIcons) {
app.component(name, (ElIcons as any)[name])
}
app.use(store).use(router).use(ElementPlus).mount('#app')
3-2-3、去掉手动引用,直接使用即可
4、全局提示框
类似于vue2.0的this.$message.info()
,使用方式就是下面红框里的代码
效果如下
五、基于mysql+js+webpack的API接口案例
参考源码:gitee