使用create-vue创建项目

vue3官网:https://cn.vuejs.org/

  1. 前提环境条件:node版本必须在16.0以上
  2. 创建Vue3的项目
1
2
3
npm init vue@latest

pnpm create vue

这一指令将会安装并执行 create-vue,本质底层会采用vite脚手夹进行创建

初始vue3项目中的关键文件

组合式API

setup 选项的写法和执行时机

setup 函数执行时机比Vue组件中的生命周期函数执行时期还要早

原始写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
export default {
setup () {
console.log('setup函数执行')

// 数据
const message = 'hello vue3'

// 函数
const logMessage = () => {
console.log(message)
}

// 所写的数据和函数必须return,不然template中无法调用
return {
message,
logMessage
}
},
created () {
console.log('created函数执行')
}
}
</script>

<template>
<div>{{ message }}</div>
<button @click="logMessage">按钮</button>
</template>

语法糖写法:

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
const message = 'hello vue3'
const logMessage = () => {
console.log(message)
}
</script>

<template>
<div>{{ message }}</div>
<button @click="logMessage">按钮</button>
</template>

语法糖的底层还是将script中的数据和方法return了出去,只是为了方便开发者编写代码

setup 函数中的this指向undefined

reactive 和 ref 函数的使用

reactive()

作用:接收对象类型数据的参数传入并返回一个响应式的对象

步骤:

  1. 从 vue 包中导入 reactive 函数
  2. 中执行 reactive 函数并传入类型为对象的初始值,并使用变量接收返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0
})
const addCount = () => {
state.count++
}
</script>

<template>
<div>{{ state.count }}</div>
<button @click="addCount">按钮</button>
</template>

ref()

ref函数可以接收复杂类型和简单类型,底层还是用的raective函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { ref } from 'vue'
// ref的底层是在数据的外面包了一层,数据放在value属性下
// 1.在脚本中需要使用对象名.value访问
// 2.在模板中则是直接使用对象名即可访问,底层是将外面包的那一层对象给删去了
const count = ref(0)
const addCount = () => {
count.value++
}
</script>

<template>
<div>{{ count }}</div>
<button @click="addCount">按钮</button>
</template>

计算属性 computed

  1. 从 vue 包中导入 computed
  2. 将要返回的数据写在 computed 函数的参数中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { computed, ref } from 'vue'
const state = ref([1,2,3,4,5,6])
const computedList = computed(() => state.value.filter(item => item > 2))
const updateList = () => {
state.value.push(666)
}
</script>

<template>
<div>原始数据:{{ state }}</div>
<div>修改后:{{ computedList }}</div>
<button @click="updateList">按钮</button>
</template>

一般不推荐通过计算属性的方法去修改ref中数据的值,如果要修改则推荐采用get和set语法

计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>

监听 watch

基本语法

当监听的数据发生变化时,立即执行回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script setup>
import {ref, watch} from 'vue'
const number = ref(0)
const nickname= ref('张三')

const updateNumber = () => {
number.value++
}

const updateNickname = () => {
nickname.value = '李四'
}

// 监视单个数据
// watch(number, (newValue, oldValue) => {
// console.log(newValue, oldValue)
// })

// 监视多个数据
watch([number, nickname], (newValue, oldValue) => {
console.log(newValue, oldValue)
})

</script>

<template>
<div>{{number}}</div>
<button @click="updateNumber">修改数字</button>
<div>{{nickname}}</div>
<button @click="updateNickname">修改昵称</button>
</template>

immediate 属性

immediate属性为true时,进入页面立即调用该回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script setup>
import {ref, watch} from 'vue'
const number = ref(0)
const nickname= ref('张三')

const updateNumber = () => {
number.value++
}

const updateNickname = () => {
nickname.value = '李四'
}

// immediate属性为true时,进入页面立即调用该回调函数
watch(number, (newValue, oldValue) => {
console.log(newValue, oldValue)
}, {
immediate: true
})

</script>

<template>
<div>{{number}}</div>
<button @click="updateNumber">修改数字</button>
<div>{{nickname}}</div>
<button @click="updateNickname">修改昵称</button>
</template>

深度监视 deep

当监视数据为对象时,如果不开启深度监视,只有当对象的地址发生变化时才会触发回调函数,deep 属性为true时,当对象中的属性值发生变化也会触发回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script setup>
import {ref, watch} from 'vue'
const userInfo = ref({
name: '张三',
age: 18
})

const setUserInfo = () => {
userInfo.value.age++
}

// deep 属性为true时,当对象中的属性值发生变化也会触发回调函数
watch(userInfo, (newValue) => {
console.log(newValue)
}, {
deep: true
})
</script>

<template>
<div>{{userInfo}}</div>
<button @click="setUserInfo">修改年龄</button>
</template>

精确监听复杂类型的某个属性

开启深度监视,监听复杂类型时,当复杂类型中的任意一个属性发生变化都会触发回调函数。如果想要只监听复杂类型的某个属性,可以将第一个参数写成函数的形式,并且返回值为要监听的复杂类型的某个属性即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script setup>
import {ref, watch} from 'vue'

const userInfo = ref({
name: '张三',
age: 18
})

const setUserInfo = () => {
userInfo.value.age++
}

// 不开启深度监视,精确监听对象的某个属性
watch(() => userInfo.value.age,
(newValue) => {
console.log(newValue)
})
</script>

<template>
<div>{{ userInfo }}</div>
<button @click="setUserInfo">修改年龄</button>
</template>

Vue3的生命周期API(选项式和组合式)

选项式API 组合式API
beforeCreate/created setup
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount(vue2中是beforeDestroy) onBeforeUnmount
unmounted(vue2中是destroyed) onUnmounted

其中beforeCreate/created函数在组合式API中直接写在脚本中并调用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup>
import {onMounted} from "vue";

function getData () {
setTimeout(() => {
console.log('请求获取数据')
}, 1000)
}

// created函数在组合式API中直接调用
getData()

// mounted函数在组合式API中则是取名为onMounted,并且可以调用多次,依次执行
onMounted (() => {
console.log('onMounted函数触发-逻辑1')
})

onMounted (() => {
console.log('onMounted函数触发-逻辑2')
})
</script>

<template>

</template>

父子通信

父传子

  1. 父组件中给子组件的属性绑定值
  2. 子组件通过props接收父组件传递的值

由于使用组合式API,故而不能直接使用props,而是采用编译器宏函数 defineProps 来自动编译props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { ref } from "vue";
import Son from '@/components/son.vue'
const cart = ref('宝马车')
const money = ref(100)

function addMoney () {
money.value += 10
}

</script>

<template>
<div>
<h5>
父组件 - {{ money }}
<button @click="addMoney">挣钱</button>
</h5>
<Son cart="cart" :money="money"></Son>
</div>
</template>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup>
// 通过编译器宏函数 defineProps 接收数据
const props = defineProps({
cart: String,
money: Number
})

// 脚本调用传递的参数,需要使用对象名.props
console.log(props.cart)
</script>

<template>
<div class="box">
<!-- 模板调用则直接使用参数名即可 -->
我是子组件 - {{ cart }} - {{ money }}
</div>
</template>

<style scoped>
.box {
border: 1px solid black;
padding: 30px;
}
</style>

子传父

  1. 父组件中给子组件标签通过@绑定事件
  2. 子组件内部通过emit方法触发事件

由于使用组合式API,故而不能直接使用emit,而是采用编译器宏函数 defineEmits 来自动编译emit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script setup>
import { ref } from "vue";
import Son from '@/components/son.vue'
const cart = ref('宝马车')
const money = ref(100)

function addMoney () {
money.value += 10
}

// value就是子组件传递的值
const changeFn = (value) => {
money.value -= value
}

</script>

<template>
<div>
<h5>
父组件 - {{ money }}
<button @click="addMoney">挣钱</button>
</h5>
<Son
cart="cart"
:money="money"
@changeMoney="changeFn"
></Son>
</div>
</template>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script setup>
// 通过编译器宏函数 defineProps 接收数据
const props = defineProps({
cart: String,
money: Number
})

// 通过编译器宏函数 defineEmits 发送数据
// 使用emit触发父组件中绑定的事件,需要先声明事件名
const emit = defineEmits(['changeMoney'])

const buy = () => {
// 通过emit给父组件传递数据
emit('changeMoney', 5)
}

// 脚本调用传递的参数,需要使用对象名.props
console.log(props.cart)
</script>

<template>
<div class="box">
<!-- 模板调用则直接使用参数名即可 -->
我是子组件 - {{ cart }} - {{ money }}
<button @click="buy">花钱</button>
</div>
</template>

<style scoped>
.box {
border: 1px solid black;
padding: 30px;
}
</style>

模板引用

概念:通过ref标识获取真实的dom对象或者组件实例对象

步骤:

  1. 通过ref函数创建ref对象
  2. 在组件标签或者html标签中绑定ref属性
  3. 通过ref对象名.value访问元素或组件

子组件默认不向父组件暴露自己的属性和方法,可以通过编译器宏函数 defineExpose 暴露对应的方法和属性。这样才能在父组件中应用子组件中的方法和属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script setup>
import {onMounted, ref} from "vue";
import Test from '@/components/test.vue'

// 1.通过ref函数创建ref对象
const inp = ref(null)
const test = ref(null)

// 注意:必须等dom渲染完,才能拿到
onMounted(() => {
// 3.通过ref对象名.value访问元素或组件
console.log(inp.value)
inp.value.focus()
})

const getTest = () => {
console.log(test.value.count)
test.value.logMessage()
}
</script>

<template>
<!-- 2.在组件标签或者html标签中绑定ref属性 -->
<input ref="inp" type="text">
<Test ref="test"></Test>
<button @click="getTest">获取组件元素</button>
</template>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
const count = 999
const logMessage = () => {
console.log('hello vue3')
}

// 子组件默认不向父组件暴露自己的属性和方法
// 可以通过编译器宏函数 defineExpose 暴露对应的方法和属性
defineExpose({
count,
logMessage
})
</script>

<template>
我是子组件 - {{count}}
</template>

<style scoped>

</style>

provide 和 inject 实现跨层级传递数据或方法

在根组件使用provide传递数据或方法,在后代组件中使用inject接收数据或方法,一般可以传递以下数据和方法:

  1. 跨层级传递普通数据
  2. 跨层级传递响应数据
  3. 跨层级传递方法
1
2
3
// 两者key必须一致
provide(key, 数据或方法)
const 对象名 = inject(key)

即两组件之间可以相隔多个组件传递数据和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script setup>
import {provide, ref} from "vue";
import Son from '@/components/son.vue'

// 传递普通数据
provide('count', 100)

// 传递响应式数据
const money = ref(200)
provide('money', money)

setTimeout(() => {
money.value = 500
}, 2000)

// 传递方法
const updateMoney = (newValue) => {
money.value = newValue
}
provide('updateMoney', updateMoney)
</script>

<template>
<h1>
我是根组件
</h1>
<Son></Son>
</template>

<style scoped>

</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import Grandson from '@/components/grandson.vue'
</script>

<template>
<h2>
我是son组件
</h2>
<Grandson></Grandson>
</template>

<style scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
import {inject} from "vue";

const count = inject('count')
const money = inject('money')
const updateMoney = inject('updateMoney')

</script>

<template>
<h3>
我是grandson组件 - {{ count }} - {{ money }}
<button @click="updateMoney(50)">修改金额</button>
</h3>
</template>

<style scoped>

</style>

defineOptions

在vue3.3之前的版本,由于中只能写setup函数中的属性或者方法,此时就没法通过name属性定义组件名。故而在之后的版本引入了编译器宏函数 defineOptions

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
defineOptions({
name: 'loginIndex'
})
</script>

<template>

</template>

<style scoped>

</style>

defineModel

在Vue3中,自定义组件上使用v-model,相当于传递一个modelValue属性,同时触发 update:modelValue 事件

1
2
3
<Child v-moel="isVisible"></Child>
相当于
<Child :modelValue="isVisible" @update:modelValue="isVisible"></Child>

此时如果是父子通信,需要在子组件中定义props和emit。如果要修改此值,还需要手动调用 emit 函数。会很麻烦,故而引入 defineModel 函数,可以简化子组件代码

原始写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import Son from '@/components/son.vue'
import {ref} from "vue";
const count = ref('100')
</script>

<template>
<Son v-model="count"></Son>
{{ count }}
</template>

<style scoped>

</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
</script>

<template>
<input
:value="modelValue"
@input="e => emit('update:modelValue', e.target.value)"
type="text"
>
</template>

<style scoped>

</style>

使用defineModel 函数的写法(父组件无变化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
const modelValue = defineModel()
</script>

<template>
<input
:value="modelValue"
@input="e => modelValue = e.target.value"
type="text"
>
</template>

<style scoped>

</style>

此时可直接修改,无需emit发送

Pinia

定义

Pinia 是 Vue 的最新的状态管理工具,是 Vuex 的替代品

区别:

  1. 提供更加简单的API(去掉了 mutation)
  2. 提供符合组合式风格的API(和 Vue3 新语法统一)
  3. 去掉了 modules 的概念,每一个 store 都是一个独立的模块
  4. 配合 TypeScript 更加友好,提供可靠的类型判断

手动安装

  1. 新建vue3项目
1
npm create vue@latest
  1. 安装Pinia软件包
1
2
3
npm install pinia

yarn add pinia
  1. 导入Pinia(在main.js文件中)
1
2
3
4
5
6
7
8
9
10
11
12
import { createApp } from 'vue'
// 导入createPinia函数
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
// 通过函数创建pinia对象
const pinia = createPinia()

// 使用pinia
app.use(pinia)
app.mount('#app')

使用Pinia

  1. 在src/store/文件名.js中导出对应的创建store对象的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import {defineStore} from "pinia"
import {computed, ref} from "vue";

// 通过defineStore创建store对象
// 语法:defineStore('仓库名', 回调函数)
export const useCounterStore = defineStore('counter', () => {
// 定义数据 相当于 state
const count = ref(100)

// 定义操作数据的方法 相当于 actions
const addCount = () => {
count.value++
}
const subCount = () => {
count.value--
}

// 定义基于数据的计算属性 相当于 computed
const result = computed(() => count.value * 2)

// 定义数据 相当于 state
const msg = ref('hello pinia')

// 定义方法和属性必须return才能被外部使用
return {
count,
msg,
addCount,
subCount,
result
}
})

  1. 在组件的脚本中导入该函数,并通过函数创建store对象
  2. 在组件的模板中调用该对象中的属性和方法即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import {useCounterStore} from '@/store/counter.js'
const counterStore = useCounterStore()
</script>

<template>
<h4>
我是son2.vue - {{ counterStore.count }}
<button @click="counterStore.subCount()">-</button>
</h4>
</template>

<style scoped>

</style>

在Pinia中异步操作数据

  1. 导出创建store对象的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {defineStore} from 'pinia'
import {ref} from "vue";
import axios from "axios"

export const useChannelStore = defineStore('channel', () => {
const channelList = ref([])

// 方法中直接支持异步操作
const getList = async () => {
const {data: {data}} = await axios.get('http://geek.itheima.net/v1_0/channels')
channelList.value = data.channels
}

return {
channelList,
getList
}
})

  1. 导入使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import {useChannelStore} from '@/store/channel.js'
const channelStore = useChannelStore()
</script>

<template>
<div>
<button @click="channelStore.getList()">获取频道数据</button>
<ul>
<li v-for="item in channelStore.channelList" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>

<style scoped></style>

解构对象获取属性和方法

直接解构得到的属性是失去响应式的,使用storeToRefs函数可以防止失去响应式

store中的方法是可以直接解构的,因为方法不需要响应式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script setup>
import Son1Com from '@/componets/Son1Com.vue'
import Son2Com from '@/componets/Son2Com.vue'
import {useCounterStore} from '@/store/counter.js'
import {useChannelStore} from '@/store/channel.js'
import {storeToRefs} from "pinia";

const counterStore = useCounterStore()
const channelStore = useChannelStore()

// 直接解构得到的属性是失去响应式的
// const { count, msg, result } = counterStore
// 使用storeToRefs函数可以防止失去响应式
const { count, msg, result } = storeToRefs(counterStore)
// store中的方法是可以直接解构的,因为方法不需要响应式
const {getList} = channelStore
</script>

<template>
<div>
<h3>我是App.vue - {{ count }} - {{ msg }}</h3>
{{ result }}
<Son1Com></Son1Com>
<Son2Com></Son2Com>
<button @click="getList()">获取频道数据</button>
<ul>
<li v-for="item in channelStore.channelList" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>

<style scoped></style>

持久化存储

官网:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/

  1. 下载插件
1
npm i pinia-plugin-persistedstate
  1. 导入使用插件(在main.js中)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createApp } from 'vue'
// 导入createPinia函数
import { createPinia } from 'pinia'
// 导入持久化插件
import persist from 'pinia-plugin-persistedstate'
import App from './App.vue'

const app = createApp(App)
// 通过函数创建pinia对象
const pinia = createPinia()
// 使用持久化插件
pinia.use(persist)

// 使用pinia
app.use(pinia)
app.mount('#app')

  1. 在store模块中配置该插件即可实现该模块中所有state的持久化存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import {defineStore} from "pinia"
import {computed, ref} from "vue";

// 通过defineStore创建store对象
// 语法:defineStore('唯一标识', 回调函数)
export const useCounterStore = defineStore('counter', () => {
// 定义数据 相当于 state
const count = ref(100)

// 定义方法 相当于 actions
const addCount = () => {
count.value++
}
const subCount = () => {
count.value--
}

// 定义基于数据的计算属性 相当于 computed
const result = computed(() => count.value * 2)

// 定义数据 相当于 state
const msg = ref('hello pinia')

// 定义方法和属性必须return才能被外部使用
return {
count,
msg,
addCount,
subCount,
result
}
}, {
persist: true // 实现该模块的持久化
})

Prettier

Eslint 用于代码纠错,Prettier 用于美化代码,用于格式化代码风格

路由

路由初始化

  1. 创建路由示例由 createRouter 实现
  2. 路由模式
  • hsitory 模式使用 createWebHistory()
  • hash 模式使用 createWebHashHistory()
  • 参数是基础路径,默认是/,其中import.meta.env.BASE_URL表示的是 vite.config.js 中的 base 配置的值
1
2
3
4
5
6
7
8
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL)
routes: []
})

export default router

组件中使用router跳转路径,使用route获取路径中的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import {UseRoute, useRouter} from 'vue-router'
const router = UserRouter
const route = UseRoute

const goList = () => {
router.push('/list')
console.log(router, route)
}
</script>

<template>
<button @click='goList'>跳转1</button>
<button @click='$router.push('/list')'>跳转2</button>
</template>

可以在vite.config.js中配置base的值,这样可以改变路由的前缀路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'

// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
// 配置路由前缀前缀
base: '/api',
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})