vue3笔记

vue3 笔记

基本知识

因为是引用类型,语法上可以修改,并会影响父组件,只不过 eslint 校验不通过,此时只需要使用 vscode 修复下错误

1
2
// eslint-disable-next-line vue/no-mutating-props

1-vite 基本使用

vue 项目脚手架工具,需要自己额外配置 ,所以在单纯学习 vue3 语法会使用它

vite 基本使用:

  • 先下载 yarn create vite
  • 创建项目 yarn create vite-app 项目名称
  • 安装依赖 yarn
  • 启动项目 yarn dev

总结:使用 vite 创建项目学习 vue3 语法,使用 vue-cli 创建项目正式开发。

vue2 使用选项 API

vue3 使用组合组合 API

2-setup 函数

组合 API 的起点,生命周期在 vue2 的 beforeCreate 之前执行,模板所需要的数据函数,需要在 setup 返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="container">
<h1 @click="say()">{{msg}}</h1> //模板使用
</div>
</template>

export default {
setup () {
const msg = 'hi vue3' // 数据
const say = () => { // 函数
console.log(msg)
}
return { msg , say} //return返回
},

总结:setup 组件初始化之前执行,它返回的数据和函数可在模版使用。

3-生命周期

vue3.0 生命周期钩子函数

  • setup 创建实例前
  • onBeforeMount 挂载 DOM 前
  • onMounted 挂载 DOM 后
  • onBeforeUpdate 更新组件前
  • onUpdated 更新组件后
  • onBeforeUnmount 卸载销毁前
  • onUnmounted 卸载销毁后

总结: 组合 API 的生命周期钩子有 7 个,可以多次使用同一个钩子,

4-reactive 函数

reactive 是一个函数,它只能定义一个复杂数据类型(数组和对象),成为响应式数据。

reactive 将数组值置空

1
2
reactive([])
list.length = 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div>{{obj.name}}</div>
<div>{{obj.age}}</div>
<button @click="updateName">修改数据</button>

<script>
import { reactive } from 'vue' //按需导入

setup () {
const obj = reactive({
name: 'ls',
age: 18
})

// 修改名字
const updateName = () => {
console.log('updateName')
obj.name = 'zs'
}

return { obj ,updateName}
}

5-toRef 函数

toRef 是函数,转换响应式对象某个属性为单独响应式数据,并且值是关联的

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
<template>
<div class="container">{{ name }} <button @click="updateName">修改数据</button></div>
</template>
<script>
import { reactive, toRef } from 'vue'
export default {
name: 'App',
setup() {
// 1. 响应式数据对象
const obj = reactive({
name: 'ls',
age: 10,
})
console.log(obj)
// 2. 模板中只需要使用name数据
// 注意:从响应式数据对象中解构出的属性数据,不再是响应式数据
// let { name } = obj 不能直接解构,出来的是一个普通数据
const name = toRef(obj, 'name')
// console.log(name)
const updateName = () => {
console.log('updateName')
// toRef转换响应式数据包装成对象,value存放值的位置
name.value = 'zs'
}

return { name, updateName }
},
}
</script>
<style scoped lang="less"></style>

使用场景:有一个响应式对象数据,但是模版中只需要使用其中一项数据。

6-toRefs 函数

toRefs 是函数,转换响应式对象中所有属性为单独响应式数据,对象成为普通对象,并且值是关联的

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
36
37
<template>
<div class="container">
<div>{{ name }}</div>
<div>{{ age }}</div>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { reactive, toRef, toRefs } from 'vue'
export default {
name: 'App',
setup() {
// 1. 响应式数据对象
const obj = reactive({
name: 'ls',
age: 10,
})
console.log(obj)
// 2. 解构或者展开响应式数据对象
// const {name,age} = obj
// console.log(name,age)
// const obj2 = {...obj}
// console.log(obj2)
// 以上方式导致数据就不是响应式数据了
const obj3 = toRefs(obj)
console.log(obj3)

const updateName = () => {
// obj3.name.value = 'zs'
obj.name = 'zs'
}

return { ...obj3, updateName }
},
}
</script>
<style scoped lang="less"></style>

使用场景:剥离响应式对象(解构|展开),想使用响应式对象中的多个或者所有属性做为响应式数据。

7-ref 函数

ref 函数,常用于简单数据类型定义为响应式数据

  • 再修改值,获取值的时候,需要.value
  • 在模板中使用 ref 申明的响应式数据,可以省略.value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="container">
<div>{{ name }}</div>
<div>{{ age }}</div>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
// 1. name数据
const name = ref('ls')
const updateName = () => {
name.value = 'zs'
}
// 2. age数据
const age = ref(10)

return { name, age, updateName }
},
}
</script>

使用场景:

  • 当你明确知道需要的是一个响应式数据 对象 那么就使用 reactive 即可
  • 其他情况使用 ref

8-computed 函数

定义计算属性,计算属性不能修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="container">
<div>今年:{{ age }}岁</div>
<div>后年:{{ newAge }}岁</div>
</div>
</template>
<script>
import { computed, ref } from 'vue'
export default {
name: 'App',
setup() {
// 1. 计算属性:当你需要依赖现有的响应式数据,根据一定逻辑得到一个新的数据。
const age = ref(16)
// 得到后年的年龄
const newAge = computed(() => {
// 该函数的返回值就是计算属性的值
return age.value + 2
})
return { age, newAge }
},
}
</script>

给 computed 传入函数,返回值就是计算属性的值

高级用法:

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
<template>
<div class="container">
<div>今年:{{ age }}岁</div>
<div>后年:{{ newAge }}岁</div>
<!-- 使用v-model绑定计算属性 -->
<input type="text" v-model="newAge" />
</div>
</template>
<script>
import { computed, ref } from 'vue'
export default {
name: 'App',
setup() {
// 1. 计算属性:当你需要依赖现有的响应式数据,根据一定逻辑得到一个新的数据。
const age = ref(16)

// 计算属性高级用法,传人对象
const newAge = computed({
// get函数,获取计算属性的值
get() {
return age.value + 2
},
// set函数,当你给计算属性设置值的时候触发
set(value) {
age.value = value - 2
},
})
return { age, newAge }
},
}
</script>

目的:让计算属性支持双向数据绑定。 给 computed 传入对象,get 获取计算属性的值,set 监听计算属性改变。

9-watch 函数

watch 函数,是用来定义侦听器的

基本用法

1
2
3
watch(count, (newVal,oldVal)=>{
console.log(newVal,oldVal)
})
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
36
37
38
39
40
41
42
43
44
45

const obj = reactive({
name: 'ls',
age: 10,
brand: {
id: 1,
name: '宝马'
}
})
const updateName = () => {
obj.name = 'zs'
}
const updateBrandName = () => {
obj.brand.name = '奔驰'
}
// 2. 监听一个reactive数据
watch(obj, ()=>{
console.log('数据改变了')
})

watch(()=>obj.brand, ()=>{
console.log('brand数据改变了')
},{
// 5. 需要深度监听
deep: true,
// 6. 想默认触发
immediate: true
})

// 3. 监听多个数据的变化
// watch([count, obj], ()=>{
// console.log('监听多个数据改变了')
// })


// 4. 此时监听对象中某一个属性的变化 例如:obj.name
// 需要写成函数返回该属性的方式才能监听到
// watch(()=>obj.name,()=>{
// console.log('监听obj.name改变了')
// })

return {count, add, obj, updateName, updateBrandName}
}
}
</script>

10-ref 属性

掌握使用 ref 属性绑定 DOM 或组件

1-先定义一个空的响应式数据 ref 定义的
2- setup 中返回该数据,你想获取那个 dom 元素,在该元素上使用 ref 属性绑定该数据即可。

1
2
3
4
5
6
<div ref="dom">我是box</div>

const dom = ref(null)
onMounted(()=>{
console.log(dom.value) // <div ref="dom">我是box</div>
})

获取 v-for 遍历的元素

定义一个空数组,接收所有的 LI

定义一个函数,往空数组 push DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 被遍历的元素 -->
<ul>
<li v-for="i in 4" :key="i" :ref="setDom">第{{i}}LI</li>
</ul>

const domList = []
const setDom = (el) => {
domList.push(el)
}
onMounted(()=>{
console.log(domList)
})
return {dom, setDom}

11-父子通讯

掌握使用 props 选项和 emits 选项完成父子组件通讯

1.父传子:

1
2
3
<Son :money="money" />

setup () { const money = ref(100) return { money } }

子组件:

1
2
export default { name: 'Son', // 子组件接收父组件数据使用props即可 props: { money: { type: Number,
default: 0 } }, setup (props) { // 获取父组件数据money console.log(props.money) }

2.子传父:

1
2
3
4
5
6
7
8
<h1>子组件</h1>
<p>{{money}}</p>
<button @click="changeMoney">花50元</button>

// 子组件接收父组件数据使用props即可 props: { money: { type: Number, default: 0 } }, // props
父组件数据 // emit 触发自定义事件的函数 setup (props, {emit}) { // 获取父组件数据money
console.log(props.money) // 向父组件传值 const changeMoney = () => { // 消费50元 //
通知父组件,money需要变成50 emit('change-money', 50) } return {changeMoney} } }

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<h1>父组件</h1>
<p>{{money}}</p>
<hr>
<Son :money="money" @change-money="updateMoney" />

setup () {
const money = ref(100)
const updateMoney = (newMoney) => {
money.value = newMoney
}
return { money , updateMoney}
}
}

在 vue3.0 的时候,使用 v-model:money="money" 即可

1
2
<!-- <Son :money="money" @update:money="updateMoney" /> -->
<Son v-model:money="money" />

总结:

  • 父传子:在 setup 种使用 props 数据 setup(props){ // props就是父组件数据 }
  • 子传父:触发自定义事件的时候 emit 来自 setup(props,{emit}){ // emit 就是触发事件函数 }
  • 在 vue3.0 中 v-model.sync 已经合并成 v-model 指令

12-依赖注入

目标:掌握使用 provide 函数和 inject 函数完成后代组件数据通讯

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<h1>父组件 {{money}}
<button @click="money=1000">发钱</button>
<Son />

setup () {
const money = ref(100)

const changeMoney = (saleMoney) => {
console.log('changeMoney',saleMoney)
money.value = money.value - saleMoney
}
// 将数据提供给后代组件 provide
provide('money', money)
// 将函数提供给后代组件 provide
provide('changeMoney', changeMoney)

return { money }
}

子组件

1
2
3
4
5
<h2>子组件 {{money}}</h2>
<GrandSon />

import { inject } from 'vue' setup () { // 接收祖先组件提供的数据 const money = inject('money')
return { money } }

孙组件

1
2
3
4
5
6
<h3>孙组件 {{money}} <button @click="fn">消费20</button></h3>

import { inject } from 'vue' setup () { const money = inject('money') //
孙组件,消费50,通知父组件App.vue组件,进行修改 //
不能自己修改数据,遵循单选数据流原则,大白话:数据谁定义谁修改 const changeMoney =
inject('changeMoney') const fn = () => { changeMoney(20) } return {money, fn}

总结:

  • provide 函数提供数据和函数给后代组件使用
  • inject 函数给当前组件注入 provide 提供的数据和函数
  • inject 拿到的数据为 ref,所以要加 value

13-v-model 语法糖

目标:掌握 vue3.0 的 v-model 语法糖原理

父组件

1
2
3
4
<!-- <Son :modelValue="count" @update:modelValue="count" /> -->
<Son v-model="count" />

setup () { const count = ref(10) return { count } }

子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<h2>子组件 {{modelValue}} <button @click="fn">改变数据

props: {
modelValue: {
type: Number,
default: 0
}
},
setup (props, {emit}) {
const fn = () => {
// 改变数据
emit('update:modelValue', 100)
}
return { fn }
}

总结: vue3.0 封装组件支持 v-model 的时候,父传子:modelValue 子传父 @update:modelValue

组合式 API(setup)

1-侦听器

属性:{ immediate: true } 立即执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const user = reactive({ name: 'tom' })
// ✅ 方法一,传递响应对象
watch(user, (value) => {
console.log(value) // 监听成功,输出 { name: 'jake' }
})
// ❌ 方法二:传递响应对象下的属性
watch(user.name, (value) => {
console.log(value) // 监听失败,没输出
})
// ✅ 方法三:传递函数,函数返回响应对象属性
watch(() => user.name, (value) => {
console.log(value); // 监听成功,输出 jake
})
// 对响应对象重新赋值
user.name = 'jake'

第三参数:立即执行
{ immediate: true }

2-计算属性

1
2
3
4
5
6
7
8
// 构建指示点的数组
const indicatorData = computed(() => {
const arr = []
for (let i = 0; i < props.sliders.length; i++) {
arr.push(i)
}
return arr
})

3-注册插件

components/library/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import XtxSkeleton from './xtx-skeleton.vue'

export default {
install(app) {
// 在app上进行扩展,app提供 component directive 函数
// 如果要挂载原型 app.config.globalProperties 方式
// 注册全局组件
app.component('xtx-skeleton', XtxSkeleton)
app.component('xtx-carousel', XtxCarousel)
app.component('xtx-more', XtxMore)

// 自定义指令
defineDirective(app)
}
}

4-路由(重点)

1
2
3
当前路由路径: route.path
当前路由参数: route.params
当前路由查询参数: route.query

1-路由配置

router/index.js

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
// 导入函数等
import { createRouter, createWebHashHistory } from 'vue-router'
import store from '@/store'

// 1-导入组件 (实现按需加载)
const Layout = () => import('@/views/Layout')
// 2-路由规则
const routes = [
{
path: '/',
name: 'Layout',
component: Layout,
// redirect:'/home' 重定向
// 子级
children: [
{
path: '/',
name: 'home',
component: home
}
]
}

// 3-创建路由实例
// 4-路由导航守卫

export default router // 最后导出

3-创建路由实例

挂载路由规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const router = createRouter({
/ 模糊匹配,只要路由第一层相同就会加上active样式
// linkActiveClass: 'active',
// 精准匹配:访问的路由跟当前的路由完成一致
linkExactActiveClass: 'active',
// 使用hash方式实现路由
history: createWebHashHistory(),
// 配置路由规则,写法和之前一样
routes,
// 路由切换时滚动条的滚动行为
scrollBehavior() {
return {
left: 0,
top: 0
}
}
})

路由切换时 控制滚动条 scrollBehavior()

4-路由导航守卫

使用场景:做页面权限 某些页面需要做登录

1
2
3
4
5
6
7
8
9
10
11
12
13
// 路由导航守卫
router.beforeEach((to, from, next) => {
// 凡是以 member开头的路由地址都需要做登录
if (!store.state.user.profile.token && to.path.startsWith('/member')) {
next({
path: '/login',
query: {
redirectUrl: encodeURIComponent(to.fullPath),
},
})
}
next()
})

encodeURIComponent() 转换 uri 编码

解码:decodeURIComponent(游览器会自动解析,一般不需要手动解码)

2-封装 axios 请求

  1. 创建一个新的 axios 实例

  2. 请求拦截器,如果有 token 进行头部携带

  3. 响应拦截器:1. 剥离无效数据 2. 处理 token 失效

  4. 导出一个函数,调用当前的 axsio 实例发请求,返回值 promise

创建utils/request.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import axios from 'axios'
import store from '@/store'
import router from '@/router'

// 导出基准地址,原因:其他地方不是通过axios发请求的地方用上基准地址
export const baseURL = 'https://apipc-xiaotuxian-front.itheima.net/'

// 1-创建一个新的axios实例
const request = axios.create({
// axios 的一些配置,baseURL timeout
baseURL,
timeout: 5000
})

2- 请求拦截器,如果有token进行头部携带

3. 响应拦截器:1. 剥离无效数据 2. 处理token失效

4. 导出一个函数,调用当前的axsio实例发请求,返回值promise

2– 请求拦截器

添加 token 请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
request.interceptors.request.use(
(config) => {
// 1. 获取用户信息对象
const { profile } = store.state.user

// 2. 判断是否有token
if (profile.token) {
// 3. 添加token到请求头
config.headers.Authorization = `Bearer ${profile.token}`
}
return config
},
(err) => {
return Promise.reject(err) // 为了调用接口时可以捕获错误
}
)

3–响应拦截器

处理权限不够保存当前地址,并跳转到登录页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 清空无效用户信息
// 2. 加码传递参数
// 3. 跳转并传参(当前路由地址)给登录页码
request.interceptors.response.use(
(res) => res.data,
(err) => {
// 401 状态码(未授权),进入该函数
if (err.response && err.response.status === 401) {
// 1. 清空无效用户信息
store.commit('user/setUser', {})
// 2 加码传递参数
// encodeURIComponent 转换uri编码,防止解析地址出问题
const fullPath = encodeURIComponent(router.currentRoute.value.fullPath)

// 3. 跳转并传参(当前路由地址)给登录页码
router.push('/login?redirectUrl=' + fullPath)
}
return Promise.reject(err)
}
)

router.currentRoute.value.fullPath 当前路由地址

(router.currentRoute 是 ref 响应式数据)

加码 encodeURIComponent( )

4-导出一个函数,调用当前的 axsio 实例发请求,返回值 promise

1
2
3
4
5
6
7
8
9
// 请求工具函数
export default (url, method, submitData) => {
// 负责发请求:请求地址,请求方式,提交的数据
return request({
url,
method,
[method.toLowerCase() === 'get' ? 'params' : 'data']: submitData,
})
}

5–使用

1
2
3
4
5
6
1 - 导入
import http from '@/utils/request'

export const getSpecsAndSkus = (skuId) => {
return http(`/goods/sku/${skuId}`, 'get')
}

使用场景二

1
2
3
4
5
// 封装的api下带https的会自动替换原始地址

export const findCommentsList = (params, id) => {
return http(`https://mock.boxuegu.com/mock/1175/goods/${id}/evaluate/page`, 'get', params)
}

5-动态组件

1
2
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>

在上面的例子中,被传给 :is 的值可以是以下几种:

  • 被注册的组件名
  • 导入的组件对象

当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过强制被切换掉的组件仍然保持“存活”的状态。

6-KeepAlive

缓存动态组件

7-Transition 组件

过渡样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<Transition name="down">
<div>包裹要加的样式</div>
</Transition>

<style scoped lang="less">
// 动画的样式
.down {
&-enter {
&-from {
transform: translateY(-75px);
opacity: 0;
}

&-active {
transition: all 0.5s;
}

&-to {
transform: none;
opacity: 1;
}
}
}

8-vuex

store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createStore } from 'vuex'
// 自动化本地存储
import createPersistedstate from 'vuex-persistedstate'
// 导入模块
import cate from './modules/cate'

// 创建vuex仓库并导出
export default createStore({
modules:{
cate
},
plugins:[
// 需要localstorage存储起来的模块
createPersistedstate({
storage: window.localStorage,
paths: ['app','user ,'permission']
}),
// 需要sessionStorage存储起来的模块
createPersistedstate({
storage: window.sessionStorage,
paths: ['platform', 'upload']
})
]

store/modules下的各个模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default createStore({
state: {
// 数据
username: 'zs'
},
mutations: {
// 同步
getData(state, payload) {
},
actions: {
// 异步
},
getters: {
// 计算属性
}

功能

1-less 封装样式函数

src/assets/style/mixins.less

1
2
3
4
5
6
7
8
9
// 鼠标经过移阴影动画
.hoverShadow() {
transition: all .5s;

&:hover {
transform: translate3d(0, -3px, 0);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
}
}

使用

1
2
3
4
5
6
7
8
全局使用
li {
width: 306px;
height: 406px;
background: #f0f9f4;
// 使用mixinLess中的函数
.hoverShadow();
}

2-数据懒加载(滚动加载)

hook/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// hooks 封装逻辑,提供响应式数据。
import { useIntersectionObserver } from '@vueuse/core'

/**
* 监听dom元素进行入到可视区后的行为
* @param {*} target 目标dom
* @param {*} callback 处理的回调函数
*/

export const useLazyData = (target, callback) => {
const { stop } = useIntersectionObserver(
target, // 要监听的目标dom
([{ isIntersecting }]) => {
// 已经到达可视区
if (isIntersecting) {
// 不需要继续监听
stop()

// 要处理的回调函数
callback && callback()
}
}
)
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
导入ref
import { ref } from 'vue'
导入api
import { findNew } from '@/api/home'
导入数据懒加载函数
import { useLazyData } from '@/hook'

1-要监听的dom对象
const newGoodsRef = ref(null)
<div class="home-new" ref="newGoodsRef">

useLazyData(newGoodsRef, () => {
findNew().then(({ result }) => {
newList.value = result
console.log(result)
})
})

3-组件插槽

1
2
3
4
5
6
7
8
9
10
创建组件
<div class="container">
<div class="head">
<h3>{{ props.title }}<small>{{ props.subTitle }}</small></h3>
<!-- 具名插槽->
<slot name="right" />
</div>
<!-- 内容部分默认插槽 -->
<slot />
</div>

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import HomePanel from '../com/home-panel.vue' 导入

<HomePanel title="新鲜好物" subTitle="新鲜出炉 品质靠谱">
<!-- 顶部的右边插槽 -->
<template #right>
<xtx-more></xtx-more>
</template>
<Transition name="fade">
<!-- 默认插槽 -->
<ul class="goods-list" v-if="newList && newList.length">
<li v-for="item in newList" :key="item.id">
<RouterLink :to="`/product/${item.id}`">
<img :src="item.picture" alt="">
<p class="name ellipsis">{{ item.name }}</p>
<p class="price">&yen;{{ item.price }}</p>
</RouterLink>
</li>
</ul>
<!-- 骨架屏组件 -->
<HomeSkeleton v-else bg="#f0f9f4"></HomeSkeleton>
</Transition>
</HomePanel>


总结具名插槽使用<template #right>或