Vue3-项目实战
Vue3-项目实战
Vue3 的优势
-
优势
优势
项目初始化
-
环境
node 16+
-
项目初始化
1
2# create-vue 是 Vue 官方新的脚手架工具,
底层切换到了 vite(下一代前端工具链), 为开发提供极速响应
npm init vue@latest选项 -
项目启动
-
安装依赖
1
2npm config set registry https://registry.npmmirror.com
npm install -
项目启动
1
npm run dev
-
熟悉项目目录和关键文件
- 关键文件
vite.config.js
: 项目的配置文件基于vite
的配置 package.json
: 项目包文件核心依赖项变成了 vue3x
和 vite main.js
: 入口文件,createApp
函数构建应用实例 app.vue
根组件 SFC
单文件组件 script-template-style
- 变化一: 脚本
script
和模板 template
顺序调整 - 变化二: 模板
template
不再要求唯一根元素 - 变化三: 脚本
script
添加 setup
标识支持组合式 API
- 变化一: 脚本
index.html
: 单页入口提供 id 为 app
的挂载点
组合式 API
setup
-
写法和执行时机
执行时机 -
时机验证
1
2
3
4
5
6
7
8
9
10<script>
export default {
setup() {
console.log("setUp....");
},
beforeCreate() {
console.log("beforeCreate...");
}
}
</script> -
基础使用测试
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>
export default {
setup() {
console.log("setUp....");
// 定义变量
const message = "Hello Vue3.....";
// 定义函数
const sayHello = () => {
console.log("hello methods.......");
}
// 导出
return {
message,
sayHello
}
},
beforeCreate() {
console.log("beforeCreate...");
}
}
</script>
<template>
<div class="container">
{{ message }}
</div>
<button @click="sayHello">测试点击按钮 </button>
</template> -
setup
的语法糖 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<!-- 不用再导出 -->
<script setup>
console.log("setUp....");
// 定义变量
const message = "Hello Vue3.....";
// 定义函数
const sayHello = () => {
console.log("hello methods.......");
}
</script>
<template>
<div class="container">
{{ message }}
</div>
<button @click="sayHello">测试点击按钮 </button>
</template> -
总结
-
setup
选项的执行时机是什么? beforeCreate
钩子之前, 自动执行 -
setup
写代码的特点是什么? 定义数据 + 函数,
然后以对象方式 return
-
<script setup>
解决了什么问题? 经过语法糖的封装更简单的使用组合式
API
-
setup
中的 this
还指向组件实例吗? 指向
undefined
-
reactive 和 ref 函数
-
reactive()
-
作用:
接受对象类型数据的参数
并返回一个响应式的对象
-
使用的核心步骤
1
2
3
4
5
6
7<script setup>
// 1. 从 vue 包中导入 reactive 函数
import { reactive } from 'vue';
// 2. 在
const state = reactive(对象类型数据)
</script>-
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<script setup>
// 1. 导入
import { reactive } from 'vue';
// 执行函数 传入参数 变量接受
const state = reactive({
count: 0
})
const setCount = () => {
state.count++
}
</script>
<template>
<button @click="setCount">
{{ state.count }}
</button>
</template>
-
-
-
ref()
-
作用: 接受
简单类型或者对象类型的数据
传入并返回一个响应式的对象
-
使用的核心步骤
1
2
3
4
5
6
7<script setup>
// 1. 从 vue 包中导入 ref 函数
import { ref } from 'vue';
// 2. 在
const count = ref(简单类型或者复杂类型的数据)
</script> -
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<script setup>
// 1. 从 vue 包中导入 ref 函数
import { ref } from 'vue';
// 2. 在
const count = ref(1)
const setCount = () => {
// 脚本区域修改 ref 产生的响应式对象数据 必须通过.value 属性
count.value++
}
</script>
<template>
<div class="container">
<button @click="setCount">
{{ count }}
</button>
</div>
</template> -
总结
-
reactive
和 ref
函数的共同作用是什么 用函数调用的方式生成响应式数据
-
reactive vs ref
reactive
不能处理简单类型的数据 ref
参数类型支持更好但是必须通过.value 访问修改 ref
函数的内部实现依赖于 reactive
函数
-
在实际工作中推荐使用那个?
推荐使用
ref
函数, 更加灵活。
-
-
computed
-
computed
计算属性函数 -
计算属性基本思想和
vue2
完全一致, 组合式 API
下的计算属性 只是修改了写法
-
使用的核心步骤
1
2
3
4
5
6
7
8
9<script setup>
// 1. 从 vue 包中导入 computed 函数
import { computed } from 'vue';
// 2. 执行函数在回调参数中 return 基于响应式数据做计算的值,用变量接受
const computedState = computed(() => {
return 基于响应式数据做计算后的值
})
</script> -
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<script setup>
// 1. 从 vue 包中导入 computed 函数
import { ref, computed } from 'vue';
const list = ref([1, 2, 3, 4, 5, 6]);
// 2. 执行函数在回调参数中 return 基于响应式数据做计算的值,用变量接受
const computedState = computed(() => {
return list.value.filter(item => item > 2)
})
</script>
<template>
<div class="container">
<p> 原始响应数组: {{ list }}</p>
<p> 计算属性后的数组: {{ computedState }}</p>
</div>
</template>计算属性的使用 -
总结
-
计算属性中不应该有
副作用
比如: 异步请求
/ 修改 dom
-
避免直接修改计算属性的值
计算属性应该是只读的
-
-
watch
-
watch
函数 -
基本使用-侦听单个数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<script setup>
// 1. 从 vue 包中导入 computed 函数
import { ref, watch } from 'vue';
const count = ref(0)
const setCount = () => {
count.value++
}
// 2. 执行 watch 函数传入要侦听的响应式数据(ref 对象) 和回调函数
watch(count, (newValue, oldValue) => {
console.log(`count 发生了变化,老值为 ${oldValue}, 新值为: ${newValue}`);
})
</script>
<template>
<div class="container">
<p> 原始响应数组: {{ count }}</p>
<button @click="setCount">改变数据 </button>
</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<script setup>
// 1. 从 vue 包中导入 computed 函数
import { ref, watch } from 'vue';
const count = ref(0)
const cp = ref('cp')
const setCount = () => {
count.value++
cp.value = 'cpdd++'
}
// 2. 执行 watch 函数传入要侦听的响应式数据(ref 对象) 和回调函数
watch([count, cp], ([newCount, newCp], [oldCount, oldCp]) => {
console.log(`count 发生了变化,老值为 ${oldCount}, 新值为: ${newCount}`);
console.log(`cp 发生了变化,老值为 ${oldCp}, 新值为: ${newCp}`);
})
</script>
<template>
<div class="container">
<p> 原始响应: {{ count }}</p>
<p> 原始响应: {{ cp }}</p>
<button @click="setCount">改变数据 </button>
</div>
</template>
-
-
立即执行
说明: 在侦听器创建时
立即触发回调
,响应式数据变化后继续执行回调 1
2
3
4const count = ref(0)
watch(count,()=>{},{
immediate: true
}) -
deep
默认机制: 通过
watch
监听的 ref
对象默认是 浅层监听的,
,直接修改嵌套的对象属性不会触发回调执行 需要开启 deep
选项 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<script setup>
// 1. 从 vue 包中导入 computed 函数
import { ref, watch } from 'vue';
const state = ref({ count: 0 })
const changeStateByCount = () => {
// 直接修改属性 => 不会触发回调
state.value.count++
}
watch(state, () => {
console.log("发生了变化......");
})
</script>
<template>
<div class="container">
<p> 原始响应: {{ state.count }}</p>
<button @click="changeStateByCount">改变数据 </button>
</div>
</template>1
2
3
4
5
6
7watch(state,
() => {
console.log("发生了变化......");
},
{ 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
25
26
27
28
29
30
31
32
33<script setup>
// 1. 从 vue 包中导入 computed 函数
import { ref, watch } from 'vue';
const state = ref({ count: 0, name: 'zs' })
const changeCount = () => {
// 直接修改属性 => 不会触发回调
state.value.count++
}
const changeName = () => {
// 直接修改属性 => 不会触发回调
state.value.name = 'lisi'
}
// TODO 实现精确监听
watch(
() => state.value.name, // 精确监听的字段
() => {
console.log("name 发生了变化...."); // 回调
}
)
</script>
<template>
<div class="container">
<p> 原始响应: {{ state.count }}</p>
<p> 原始响应: {{ state.name }}</p>
<button @click="changeCount">改变 Count</button>
<button @click="changeName">改变 Name</button>
</div>
</template> -
总结
-
作为
watch
函数的第一个参数, ref
对象需要添加 .value
吗? 不需要,
watch
会自动读取 -
watch
只能侦听单个数据吗? 单个或者多个
-
不开启
deep
,直接修改嵌套属性能触发回调吗?不能,
默认是浅层监听 -
不开启
deep
,想在某个层次比较深的属性变化时执行回调该怎么做?可以把第一个参数写成函数的写法,
返回要监听的具体属性
-
生命周期函数
-
vue2
与 vue3 对比 选项式 (vue2) API(vue3) beforeCreate/create
setup
beforeMount
onBeforeMount
mounted
onMounted
beforeUpdate
onBeforeUpdate
updated
onUpdated
beforeUnmount
onBeforeUnmount
unmounted
onUnmount
-
生命周期函数的基本使用
1
2
3
4
5
6
7
8
9
10<script setup>
// 1. 从 vue 包中导入 onMounted 函数
import { onMounted } from 'vue';
// 2. 执行函数, 传入回调
onMounted(() => {
// 自定义逻辑
console.log("onMounted.....");
})
</script> -
生命周期函数的多次执行
生命周期函数是可以执行多次的,
多次执行时传入的回调会在 时机成熟时依次执行
-
总结
-
组合式
API
中生命周期函数的格式是什么? on + 生命周期名字
-
组合式
API
中可以使用 onCreated
吗? 没有这个钩子函数,
直接写入到 setup
中 -
组合式
API
中组件卸载完毕时执行哪个函数 onUnmounted
-
父子组件通信
Props
-
通信结果
父子组件通信 -
实现
-
父组件
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<script setup>
// 1. 导入子组件
import User from '@/components/User.vue'
</script>
<template>
<div class="container">
Father Component
<!-- 2. 对子组件绑定属性和值 -->
<user
message="father data to son"
count="10">
</user>
</div>
</template>
<style>
.container {
border: 2px solid red;
padding: 15px;
font-family: "ER Kurier 1251";
width: 40%;
}
</style> -
子组件
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<script setup>
import {ref} from "vue";
// 3. 通过 defineProps "编译器宏",接受父组件传递的数据
defineProps({
message: String,
count: Number
})
</script>
<template>
<div class="user">
Son Component
<p>
{{message}}
</p>
<button>{{ count }}</button>
</div>
</template>
<style>
.user {
border: 2px solid #000;
padding: 5px;
}
</style>-
子组件修改父组件的数据
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} from "vue";
// 3. 通过 defineProps "编译器宏",接受父组件传递的数据
const props = defineProps({
message: String,
count: Number
})
let num = ref(props.count)
const setCount = ()=>{
num.value++
}
</script>
<template>
<div class="user">
Son Component
<p>
{{message}}
</p>
<button @click="setCount">{{ num }}</button>
</div>
</template>
<style>
.user {
border: 2px solid #000;
padding: 5px;
}
</style>
-
-
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<script setup>
// 1. 引入子组件
import User from '@/components/User.vue'
import {ref} from "vue";
let msg = ref('')
const getMessage = (data) => {
console.log(data)
msg.value = data
}
</script>
<template>
<div class="container">
Father Component => {{ msg }}
<!-- 绑定自定义事件 -->
<user @get-message="getMessage"></user>
</div>
</template>
<style>
.container {
padding: 15px;
border: 2px solid red;
font-family: "ER Kurier 1251";
}
</style> -
子组件
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 {ref} from "vue";
let count = ref(100)
// 通过 defineEmits 编译器宏生成 emit 方法
const emit = defineEmits(['get-message'])
const sendMsg = () => {
// 触发自定义事件
emit('get-message', 'this is send son data to father....')
}
</script>
<template>
<div class="user">
Son Component
<button @click="sendMsg">发送数据 </button>
</div>
</template>
<style>
.user {
padding: 5px;
border: 2px solid #000;
}
</style>-
渲染
未获取数据前 触发自定义事件并渲染
-
组件通信总结
-
父传子
-
父传子的过程中通过什么方式接受
props
definePros({属性名: 类型})
-
setup
语法糖中如何使用父组件传过来的数据 const props = defineProps({属性名: 类型})
-
-
子传父
-
子传父的过程中通过什么方式得到
emit
方法 defineEmits(['事件名称'])
-
模板引用
-
概念: 通过
ref
标识获取真实的 dom
对象或者组件实例对象 -
具体使用
-
调用
ref
函数生成一个 ref
对象 -
通过
ref
标识绑定 ref
对象到标签 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";
// 1. 获取 ref 对象
const h1Ref = ref(null)
// 组件挂载完毕之后才能获取对象
onMounted(() => {
console.log(h1Ref.value)
})
</script>
<template>
<div class="container">
<h1 ref="h1Ref">测试 ref 的使用 </h1>
Father Component
</div>
</template>
<style>
.container {
padding: 15px;
line-height: 2;
border: 2px solid red;
font-family: "ER Kurier 1251";
}
</style>
-
-
组件实例
-
子组件
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} from "vue";
const msg = ref('点击获取修改后的数据...')
const changeMsg = () => {
msg.value = '修改后的新数据'
}
</script>
<template>
<div class="user">
Son Component
<button @click="changeMsg">{{ msg }}</button>
</div>
</template>
<style>
.user {
padding: 5px;
line-height: 2;
border: 2px solid #000;
}
</style> -
父组件
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 setup>
import {onMounted, ref} from "vue";
import User from "@/components/User.vue";
// 1. 获取 ref 对象
const compRef = ref(null)
// 组件挂载完毕之后才能获取对象
onMounted(() => {
console.log(compRef.value)
})
</script>
<template>
<div class="container">
Father Component
<user ref="compRef"></user>
</div>
</template>
<style>
.container {
padding: 15px;
line-height: 2;
border: 2px solid red;
font-family: "ER Kurier 1251";
}
</style>问题 -
获取不到子组件内部的
msg
的问题说明 默认情况下在
script setup>
语法糖下组件内部的属性和方法是不开放
给父组件访问的,可以通过 defineExpose
编译宏 指定那些属性和方法允许访问
-
-
总结
-
获取模板引用的时机是什么?
组件
挂载完毕
-
defineExpose
编译宏的作用是什么 显示暴露组件内部的属性和方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<!-- 组件内 -->
<script setup>
import {ref} from "vue";
const msg = ref('点击获取修改后的数据...')
const changeMsg = () => {
msg.value = '修改后的新数据'
}
// 显示的暴露出去
defineExpose({
msg,
changeMsg
})
</script>1
2
3
4
5
6
7<!-- 暴露出去后的使用方式 -->
// 组件挂载完毕之后才能获取对象
onMounted(() => {
console.log('aaa')
// 调用函数
compRef.value.changeMsg()
})
-
provide 和 inject
-
跨组件数据传递
跨组件数据传递 -
provide 和 inject
作用: 顶层组件
向任意的组件传递数据和方法
,实现跨组件通信
-
跨层传递普通数据
-
实现步骤
- 顶层组件通过
provide
函数提供数据 - 底层组件通过
inject
函数获取数据
- 顶层组件通过
-
细节实现
-
App.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 setup>
import RoomMsgItem from "@/components/RoomMsgItem.vue";
import {provide, ref} from "vue";
// 传递静态数据
provide('data-key', 'this is root page data....')
// 传递响应式数据
const count = ref(0)
provide('count-key', count)
setTimeout(() => {
count.value = 100
}, 3000)
</script>
<template>
<div class="container">
顶层组件
<room-msg-item/>
</div>
</template>
<style>
.container {
padding: 15px;
line-height: 2;
border: 2px solid red;
font-family: "ER Kurier 1251";
}
</style> -
RoomMsgItem(中间组件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<script setup>
import RoomMsgComment from "@/components/RoomMsgComment.vue";
</script>
<template>
<div class="room-msg-item">
中间组件
<room-msg-comment/>
</div>
</template>
<style scoped>
.room-msg-item {
padding: 20px;
border: 1px solid green;
}
</style> -
RoomMsgComment(最内部组件)
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<script setup>
import {inject} from "vue";
const roomData = inject('data-key')
const countData = inject('count-key')
</script>
<template>
<div class="comment">
底层组件
<div class="content">
<div>来自顶层组件中的数据为: {{ roomData}}</div>
<div>来自顶层组件中的响应式数据为: {{ countData }} </div>
</div>
</div>
</template>
<style scoped>
.comment{
padding: 20px;
border: 1px solid blue;
}
.content{
padding: 10px;
}
</style>跨组件数据传递
-
-
-
跨层传递方法
顶层组件可以向底层组件传递方法,
底层组件调用方法修改顶层组件中的数据 1
2
3
4
5
6// 顶层组件传递方法
const setCount = () => {
count.value = 101;
}
provide('setCount-key', setCount)1
2
3
4
5
6
7
8// 内部组件调用
<script setup>
const setCountData = inject('setCount-key')
</script>
<template>
<button @click="setCountData"> 修改顶层组数据 </button>
</template> -
总结
-
provide
和 inject
的作用是什么 跨组件通信
-
如何在传递的过程中保持数据响应式
第二个参数传递
ref
对象 -
底层组件想要通知顶层组件做修改,
如何做 传递方法,
底层组件调用方法 -
一个组件书只有一个顶层或底层组件吗
相对概念,
存在多个顶层和底层关系
-
Pinia
基本概念
-
是什么
Pinia
是 vue
的专属最新的 状态管理库
,是 Vuex
状态管理工具的替代品
优势
-
优势
- 提供更加简单的
API(去掉了 mutation)
- 提供符合组合式风格的
API(和 Vue3
新语法统一) - 去掉了
modules
的概念, 每一个 store
都是一个独立的模块 - 搭配
TypeScript
一起使用提供可靠的类型推断
- 提供更加简单的
-
安装
1
npm install pinia
安装和基础配置
-
使用
-
配置
1
2
3
4
5
6
7
8
9
10
11
12import {createApp} from 'vue'
// 1. 导入 createPinia
import {createPinia} from 'pinia'
// 2. 执行方法得到实例
const pina = createPinia()
const app = createApp(App)
// 3. 把 pinia 实例加入到 app 应用中
app.use(pina)
app.mount('#app') -
案例: 使用
Pinia
实现计数器案例 -
创建文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14import {defineStore} from "pinia";
import {ref} from "vue";
// counter: 是一个标识(自定义的,任意的)
export const useCounterStore = defineStore('counter', () => {
// 数据(state)
const count = ref(0)
// 修改数据的方法
const increment = () => {
count.value++
}
// 以对象形式返回
return {count, increment}
}) -
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<script setup>
// 1. 导入 useCounterStore 方法
import {useCounterStore} from "@/store/counters";
// 2. 执行方法得到 counterStore 对象
const counterStore = useCounterStore()
</script>
<template>
<div class="container">
// 调用它的方法: counterStore.increment
<button @click="counterStore.increment">{{ counterStore.count }}</button>
</div>
</template>
-
-
Getters 实现
-
getters
实现 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import {defineStore} from "pinia";
import {computed, ref} from "vue";
export const useCounterStore = defineStore('counter', () => {
// 数据(state)
const count = ref(0)
// 修改数据的方法(action 同步 + 异步的方式)
const increment = () => {
count.value++
}
// getter 的定义
const doubleCount = computed(() => count.value * 2)
// 以对象形式返回
return {count, increment,doubleCount}
})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<script setup>
// 1. 导入 useCounterStore 方法
import {useCounterStore} from "@/store/counters";
// 2. 执行方法得到 counterStore 对象
const counterStore = useCounterStore()
console.log(counterStore)
</script>
<template>
<div class="container">
<button @click="counterStore.increment">{{ counterStore.count }}</button>
<!-- 使用 getter -->
{{ counterStore.doubleCount }}
</div>
</template>
<style>
.container {
padding: 15px;
line-height: 2;
border: 2px solid red;
font-family: "ER Kurier 1251";
}
</style>
异步 Action
-
action
如何实现异步 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import {defineStore} from "pinia";
import {computed, ref} from "vue";
import axios from "axios";
const API_URL = 'http://geek.itheima.net/v1_0/channels'
export const useCounterStore = defineStore('counter', () => {
// 准备数据
const list = ref([])
// 异步action
const loadList = async () => {
const {data: res} = await axios.get(API_URL)
list.value = res.data.channels
}
// 以对象形式返回
return {list, loadList}
})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>
// 1. 导入 useCounterStore 方法
import {useCounterStore} from "@/store/counters";
import {onMounted} from "vue";
// 2. 执行方法得到 counterStore 对象
const counterStore = useCounterStore()
onMounted(() => {
counterStore.loadList()
})
</script>
<template>
<div class="container">
<ul>
<li v-for="item in counterStore.list">
{{ item.name }}
</li>
</ul>
</div>
</template>
<style>
.container {
padding: 15px;
line-height: 2;
border: 2px solid red;
font-family: "ER Kurier 1251";
}
</style>
storeToRefs
-
storeToRefs
使用
storeToRefs
函数可以辅助保持数据 ( state + getter
)的响应式解构 -
丢失响应式问题
1
2
3
4
5
6// 直接解构赋值
(响应式丢失)
const {count} = counterStore
// 响应式丢失,数据无法发生改变
<button @click="counterStore.increment">{{ count }}</button>-
如何解决
1
2// 通过 storeToRefs 包裹即可
const {count} = storeToRefs(counterStore)
-
-
方法的解构
1
2// 方法直接从原来的 counterStore 中解构赋值
const { increment } = counterStore
总结
-
Pinia
是用来做什么的? 集中状态管理工具,
新一代的 vuex
-
Pinia
中还需要 mutation
吗? 不需要,
action
既支持同步也支持异步 -
Pinia
如何实现 getter
?computed
计算属性函数 -
Pinia
产生的 Store
如何解构赋值数据保持响应式 storeToRefs
Vue-Rabbit
-
项目初始化
初始化 -
目录调整
目录调整 -
配置别名路径联想提示
-
作用
在编写代码的过程中,
一旦 输入 @/
,Vscode
会立刻 联想出 src
,下的所有子目录和文件 统一文件路径访问不会出错 -
具体配置
-
在项目的
根目录下
新增jsconfig.json
文件 -
添加
json
格式的配置项 1
2
3
4
5
6
7
8
9
10{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
}
}
-
-
Element-UI Plus
-
安装
1
2
3
4
5
6
7
8# NPM
$ npm install element-plus --save
# Yarn
$ yarn add element-plus
# pnpm
$ pnpm install element-plus -
按需自动导入配置
1
npm install -D unplugin-vue-components unplugin-auto-import
-
vite
中添加如下配置 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
29import {fileURLToPath, URL} from "node:url";
import {defineConfig} from "vite";
import vue from "@vitejs/plugin-vue";
// TODO: 配置Element-UI 按需自动导入
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import {ElementPlusResolver} from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// TODO: 添加 element-ui的插件
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
}); -
主题定制
-
如何定制:
scss
变量替换方案 -
安装依赖
1
npm i sass -D
-
创建文件
1
styles/element/index.scss
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/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
// 主色
'base': #27ba9b,
),
'success': (
// 成功色
'base': #1dc779,
),
'warning': (
// 警告色
'base': #ffb302,
),
'danger': (
// 危险色
'base': #e26237,
),
'error': (
// 错误色
'base': #cf4444,
),
)
); -
通知
Element
采用 scss
语言, 导入定制 scss
文件覆盖 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
37import {fileURLToPath, URL} from "node:url";
import {defineConfig} from "vite";
import vue from "@vitejs/plugin-vue";
// 配置Element-UI 按需自动导入
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import {ElementPlusResolver} from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
// 1. 配置 elementPlus 采用 sass 样式配色系统
resolvers: [ElementPlusResolver({importStyle: "sass"})],
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
css: {
preprocessorOptions: {
scss: {
// 自动导入定制化样式文件进行样式覆盖
additionalData: `@use "@/styles/element/index.scss" as *;`,
}
}
}
});
-
-
-
如何验证成功替换主题色
将导入的测试按钮如何发现色系已更改则为替换成功
Axios
-
安装
1
npm i axios
-
配置基础实例
(统一接口配置) 统一接口配置 -
基础封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import axios from 'axios'
// 创建axios 实例
const httpInstance = axios.create({
baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
timeout: 5000
})
// 拦截器
// 请求拦截器
httpInstance.interceptors.request.use(config => {
return config;
}, error => {
Promise.reject(error)
})
// 响应拦截器
httpInstance.interceptors.response.use(
res => res.data,
error => {
return Promise.reject(error)
})
export default httpInstance;1
2
3
4
5
6
7
8// 封装请求
import http from "@/utils/http";
export function getCategory() {
return http({
url: 'home/category/head'
})
}1
2
3
4
5
6
7
8
9// 使用该请求
<script setup>
import {RouterView} from 'vue-router'
import {getCategory} from '@/apis/test/testHttp'
getCategory().then(res => {
console.log(res)
})
</script>
路由设计
-
一级路由与二级路由
路由设计原则:找内容切换的区域,
如果是 页面整体切换
,则为一级路由 -
路由文件默认内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue')
}
]
})
export default router -
总结
-
路由设计的依据是什么?
依据: 内容切换的方式
-
默认二级路由如何进行设置?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// path 配置项置空
routes: [
{
path: '/',
component: Layout,
children: [
{
// TODO: 小技巧,默认为空是, 会自动显示二级路由
path: "",
component: Home
},
{
path: "category",
component: Category
}
]
},
{
path: '/login',
component: Login
}
]
-
静态资源初始化
- 图片资源和样式资源
- 资源说明
- 实际工作中的图片资源通常由
UI
设计师提供, 常见的图片格式有 png,svg
等都是由 UI
切图交给前端 - 样式资源通常是指项目初始化的时候进行样式重置,
常见的比如开源的 normalize.css
或者手写
- 实际工作中的图片资源通常由
- 资源操作
- 图片资源-把
images
文件夹放到 assets
目录系啊 - 样式资源-把
common.scss
文件放到 style
目录下
- 图片资源-把
- 资源说明
scss-自动导入
-
为什么需要自动导入
在项目里一些组件共享的色值会以
scss
变量的方式统一放到一个名为 var.scss
的文件中, 正常组件中使用,需要 先导入 scss 文件,
,再使用内部的变量 比较繁琐,自动导入 可以免去手动导入的步骤,
直接使用内部的变量 -
配置
-
定义一些色值变量
1
2
3
4
5$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444; -
vite.config.js
中做出配置 1
2
3
4
5
6
7
8
9
10
11css: {
preprocessorOptions: {
scss: {
// 自动导入定制化样式文件进行样式覆盖
additionalData:
`@use "@/styles/element/index.scss" as *;
@use "@/styles/var.scss" as *;
`,
}
}
}
-
吸顶导航
-
吸顶交互: 浏览器再上下滚动的过程中,
如果距离顶部的滚动距离大于 78px
,吸顶导航显示,小于 78px
隐藏 -
实现核心思路分析
核心思路分析 -
vueuse
1
npm i @vueuse/core
-
使用
1
2
3
4
5
6
7<script setup>
// 1. 导入
import {useScroll} from "@vueuse/core";
const {y} = useScroll(window)
</script>
-
Pinia-优化重复请求
-
结论: 两个导航中的列表是完全一致的,
但是要发送两次网络请求, 存在浪费。通过 Pinia
集中管理数据, 再把数据给组件使用 -
如何优化
优化分析 -
定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import {ref} from 'vue'
import {defineStore} from 'pinia'
import {getCategoryAPI} from '@/apis/layout'
export const useCategoryStore = defineStore('category', () => {
// 存储列表(state)
const categoryList = ref([])
// 发送请求: getCategory 再次使用函数,可以对发送请求前后的数据进行处理 (异步 action)
const getCategory = async () => {
const res = await getCategoryAPI()
// 数据绑定
categoryList.value = res.result
}
return {categoryList, getCategory}
})使用的时候只在
Layout
组件触发 action
Home
-
页面解构分析
页面解构分析 -
组件封装
-
组件封装解决了什么问题?
- 复用问题
- 业务维护问题
-
组件封装的核心思路
把可复用的解构只写一次,
把 可能发生变化的部分抽象成组件参数
(props/ 插槽) -
实现步骤
- 不做任何抽象,
准备静态模板 - 抽象可变的部分
- 主标题和副标题是
纯文本
,可以抽象成prop
传入 - 主体内容是
复杂的模板
,抽象成 插槽
传入
- 主标题和副标题是
- 不做任何抽象,
-
总结
-
纯展示类组件通用封装思路总结
-
搭建纯静态的部分,
不管可变的部分 -
抽象可变的部分为组件参数
非复杂的模板抽象成
props
,复杂的解构模板抽象为插槽
-
-
-
图片懒加载指令实现
-
场景和指令用法
-
场景
场景: 电商网站的首页通常会很长,
用户不一定能访问到 页面靠下面的图片
,这类图片通过懒加载优化手段可以做到, 只有进入视口区域才发送图片请求 -
指令用法
1
<img v-img-lazy="item.picture">
在图片
img
身上绑定指令, 该图片只有在正式进入到视口区域时才会发送图片网络请求
-
-
核心原理:
-
插件定义与使用
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// 定义懒加载插件 【src/directives/index.js】
import {useIntersectionObserver} from "@vueuse/core";
export const lazyPlugin = {
install(app) {
// 自定义全局指令
app.directive('img-lazy', {
mounted(el, binding) {
// el: 指定绑定那个元素的 img
// binding: binding.value 指令等于好后面绑定的表达式的值,图片 url
// vueuse插件中的一个函数获取视口
const {stop} = useIntersectionObserver(
el,
([{isIntersecting}]) => {
if (isIntersecting) {
// 进入视口区域
el.src = binding.value
// 在监听的图片第一次完成加载之后就停止监听,避免内存浪费
stop()
}
},
)
}
})
}
}1
2
3
4// main.js 全局指令注册
import {lazyPlugin} from "@/directives";
app.use(lazyPlugin)
激活状态显示
-
是针对
RouterLink
组件默认支持激活样式显示的类名, 只需要给 active-class
属性设置对应的类名即可 -
使用
1
<RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
-
类定义
1
2
3
4
5// 激活时使用的类名
.active {
color: $xtxColor;
border-bottom: 1px solid $xtxColor;
}
路由缓存
-
什么是路由缓存问题
使用带有参数的路由时需要注意的是,
当用户从 /users/johnny
导航到 /users/jolyne
时, 相同的组件实例将被重复使用。因为两个路由都渲染同一个组件, 比起销毁再创建, 复用则显得更加高效。不过, 这也意味着组件的生命周期钩子不会被调用。 -
问题复现
一级分类的怯寒正好满足上面的条件,
组件实例复用, 导致分类数据无法更新 -
解决问题的思路
- 思路一: 让组件实例不复用,
强制销毁创建 - 思路二: 监听路由变化,
变化之后执行数据更新操作
-
方案一: 给
router-view
添加 key
以当前路由完成路径为
key
的值, 给 router-view
组件绑定 1
2<!-- 二级路由出口: 添加 key 破坏复用机制 强制销毁重建 -->
<RouterView :key="$route.fullPath"/> -
方案二: 使用
beforeRouterUpdate
导航钩子 beforeRouterUpdate
钩子函数可以在每次路由更新之前执行,在 回调中执行需要数据更新的业务逻辑
即可1
2
3
4
5
6
7
8import { onBeforeRouteUpdate } from 'vue-router'
// 目标: 路由参数变化的时候,可以把分类数据接口重新发送
onBeforeRouteUpdate((to) => {
// 存在问题: 使用最新的路与参数请求最新的分类数据
getCategory(to.params.id)
})
- 思路一: 让组件实例不复用,
-
总结
-
路由缓存问题产生的原因是什么?
路由只有参数变化时,
会复用组件实例 -
两种方案都可以解决路由缓存问题,
如何选择? - 在意性能问题时,
选择 onBeforeRouteUpdate
,精细化控制 - 不在意性能问题时,
选择 key
,简单粗暴
- 在意性能问题时,
-
使用逻辑函数拆分业务
-
概念
基于逻辑函数拆分业务是指把
同一个组件中独立的业务代码通过函数做封装处理
,提升 代码的可维护性
使用逻辑函数拆分业务 -
具体的实现步骤
-
按照业务声明以
use
打头的逻辑函数 -
把
独立的业务逻辑
封装到各个函数内部 -
函数内部把组件中需要用到的数据或者方法
return
出去 -
在
组件中调用函数
把数据或者方法组合回来使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import {onMounted, ref} from "vue";
import {getBannerAPI} from "@/apis/home";
// 1.按照业务声明以`use`打头的逻辑函数
export function useBanner() {
const bannerList = ref([])
const getBanner = async () => {
const res = await getBannerAPI({
distributionSite: '2'
});
bannerList.value = res.result
}
onMounted(() => getBanner())
return {
// 3. 函数内部把组件中需要用到的数据或者方法`return`出去
bannerList
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import {onMounted, ref} from "vue";
import {onBeforeRouteUpdate, useRoute} from "vue-router";
import {findTopCategoryAPI} from "@/apis/category";
// 2. 把`独立的业务逻辑`封装到各个函数内部
export function useCategory() {
const categoryData = ref({})
const route = useRoute()
const getCategory = async (id = route.params.id) => {
// 如何在setup 中获取路由参数 useRoute() -> route 等价于 this.$route
const res = await findTopCategoryAPI(id)
categoryData.value = res.result
}
onMounted(() => getCategory(route.params.id))
// 目标: 路由参数变化的时候,可以把分类数据接口重新发送
onBeforeRouteUpdate((to) => {
// 存在问题: 使用最新的路与参数请求最新的分类数据
getCategory(to.params.id)
})
return {categoryData}
}1
2
3
4
5
6
7
8
9
10
11<script setup>
import GoodsItem from "@/views/Home/components/GoodsItem.vue";
import {useCategory} from './composeable/useCategory'
import {useBanner} from './composeable/useBanner'
// 4. 在`组件中调用函数`把数据或者方法组合回来使用
const {categoryData} = useCategory()
const {bannerList} = useBanner()
</script>
-
列表的无限加载
-
核心实现逻辑
使用
elementuiPlus
提供的 v-infinite-scroll
指令 监听是否满足触底条件
,满足加载条件时让 页数参数加一获取下一页数据,
做新老数据拼接渲染 -
实现步骤
分析 -
详细使用在
elementuiPlus
官网
定制路由 scrollBehavior
-
在不同路由切换的时候,
可以 自动滚动到页面的顶部
,而不是停留在原先的位置问题复现 滚动条未还原 -
解决方案
vue-router
支持 scrollBehavior
配置项, 可以指定路由切换的滚动位置 -
配置
1
2
3
4
5
6
7
8
9
10
11const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 路由滚动行为定制
scrollBehavior() {
return {
top: 0
}
},
// 路由映射
...
}