Vue 3 进阶-组件通信
今日目标:
1️⃣能够熟练掌握 Vue3
组件通信的方式
2️⃣能够独立写出 Vue3
版本的 TodoList
3️⃣能够熟练使用 Pinia
进行状态的管理
4️⃣能够独立写出 Pinia
版本的 TodoList
5️⃣了解 Vue 3
其他组件通讯的方式
01.组件通讯-父往子通信
知识点:
在使用 <script setup>
的单文件组件中,props 可以使用 defineProps()
宏来声明组件使用者传递给组件内部的数据。
<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>
在没有使用 <script setup>
的组件中,prop 可以使用 props
选项来声明
export default {
props: ['foo'],
setup(props) {
// setup() 接收 props 作为第一个参数
console.log(props.foo)
}
}
📌 Tip:
error 'defineProps' is not defined no-undef
如果提示以上错误,说明没有启用宏编译器环境,需要手动开启
.eslintrc.js:
module.exports = { env: { 'vue/setup-compiler-macros': true } }
参考文档:
落地代码:
➡️ App.vue
<template>
<div>
<childComponent name="亚瑟" />
</div>
</template>
<script lang="ts" setup>
import childComponent from './components/childComponent.vue'
</script>
➡️components/child.vue
<template>
<div>
<h2>{{ name }}</h2>
</div>
</template>
<script lang="ts" setup>
// 使用字符串数组来声明 prop
// const props = defineProps(['name'])
// console.log(props.name)
// 使用对象的形式来声明 prop
// defineProps({
// name: String
// })
// 使用对象的形式来声明 prop 其他配置项
// defineProps({
// name: {
// type: String,
// requidred: true,
// default: '安琪拉'
// }
// })
// defineProps<{
// name: string
// }>()
// 当使用基于类型的声明时,我们失去了为 props 声明默认值的能力。这可以通过 withDefaults 编译器宏解决:
withDefaults(
defineProps<{
name: string
}>(),
{
name: '安琪拉'
}
)
</script>
02.组件通讯-子往父通信
知识点:
在组件的模板表达式中,可以直接使用 $emit
方法触发自定义事件 ,父组件通过 v-on
(缩写为 @
) 来监听事件
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>
<MyComponent @some-event="callback" />
组件要触发的事件可以显式地通过 defineEmits()
宏来声明
<script setup>
defineEmits(['inFocus', 'submit'])
</script>
我们在 <template>
中使用的 $emit
方法不能在组件的 <script setup>
部分中使用,但 defineEmits()
会返回一个相同作用的函数供我们使用
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
在 <script setup>
中,emit
函数的类型标注也可以通过运行时声明或是类型声明进行
<script setup lang="ts">
// 运行时
const emit = defineEmits(['change', 'update'])
// 基于类型
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
落地代码:
➡️ App.vue
<template>
<div>
<childComponent name="亚瑟" @sendData="getData" />
</div>
</template>
<script lang="ts" setup>
import childComponent from './components/childComponent.vue'
const getData = (data: number) => {
console.log('事件被触发了', data)
}
</script>
➡️components/child.vue
<template>
<div>
<button @click="sendParent">传值给父组件</button>
</div>
</template>
<script lang="ts" setup>
// 基于运行时给 emit 函数的类型标注
// const emit = defineEmits(['sendData'])
// 基于类型声明给 emit 函数的类型标注
const emit = defineEmits<{
(e: 'sendData', data: number): void
}>()
const sendParent = () => {
emit('sendData', 1)
}
</script>
<style lang="scss" scoped></style>
03.TodoList-使用 Vite 创建项目
基于 Vite 的创建的项目:
✔ npm init vue@latest
这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / (Yes)
✔ Add JSX Support? … (No) / Yes
✔ Add Vue Router for Single Page Application development? … (No) / Yes
✔ Add Pinia for state management? … (No) / Yes
✔ Add Vitest for Unit testing? … (No) / Yes
✔ Add Cypress for both Unit and End-to-End testing? … (No) / Yes
✔ Add ESLint for code quality? … No / (Yes)
✔ Add Prettier for code formatting? … No / (Yes)
##04.TodoList-拆分TodoList组件
知识点:
根据准备的 TodoList
素材将文件拆分成 4 个组件:
Header/index.vue
List/index.vue
Item/index.vue
Footer/index.vue
其中 Item
组件需要在 List
组件中引入和使用
Header
、List
、Footer
组件需要在 App.vue
中引入和使用
落地代码:
➡️ App.vue
<template>
<div class="todo-container">
<div class="todo-wrap">
<!-- 头部组件 -->
<Header />
<!-- 列表区域 -->
<List />
<!-- 脚本区域 -->
<Footer />
</div>
</div>
</template>
<script lang="ts" setup>
import Header from './components/Header/index.vue'
import List from './components/List/index.vue'
import Footer from './components/Footer/index.vue'
</script>
<style lang="scss">
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
</style>
➡️ Header/index.vue
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" />
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
➡️ List/index.vue
<template>
<ul class="todo-main">
<Item />
</ul>
</template>
<script lang="ts" setup>
import Item from '../Item/index.vue'
</script>
<style lang="scss" scoped>
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
➡️ Item/index.vue
<template>
<li>
<label>
<input type="checkbox" />
<span>xxxxx</span>
</label>
<button class="btn btn-danger" style="display: none">删除</button>
</li>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
</style>
➡️ Footer/index.vue
<template>
<div class="todo-footer">
<label>
<input type="checkbox" />
</label>
<span> <span>已完成0</span> / 全部2 </span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
05.TodoList-展示页面数据
知识点:
-
在
App.vue
中定义数据,同时将数据通过属性绑定传递给List
组件 -
在
List
组件中通过defineProps
接收App.vue
中传递的数据 -
在
Item
组件中通过defineProps
接收List
中传递的数据,并进行渲染
落地代码:
➡️ App.vue
<template>
<div class="todo-container">
<div class="todo-wrap">
<!-- 头部组件 -->
<Header />
<!-- 列表区域 -->
<List :list="state.todoList" />
<!-- 脚本区域 -->
<Footer />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
import Header from './components/Header/index.vue'
import List from './components/List/index.vue'
import Footer from './components/Footer/index.vue'
interface ITodoItem {
id: number
title: string
done: boolean
}
const initList = [
{ id: 1, title: '吃饭', done: false },
{ id: 2, title: '睡觉', done: true },
{ id: 3, title: '学习', done: false },
{ id: 4, title: '打豆豆', done: true }
]
let TodoList = reactive<{
todo: ITodoItem[]
}>({
todo: initList
})
</script>
➡️ List/index.vue
<template>
<ul class="todo-main">
<Item v-for="item in list" :key="item.id" :todo="item" />
</ul>
</template>
<script lang="ts" setup>
import Item from '../Item/index.vue'
interface ITodoItem {
id: number
title: string
done: boolean
}
defineProps<{
list: ITodoItem[]
}>()
</script>
➡️ Item/index.vue
<template>
<li>
<label>
<input type="checkbox" />
<span>{{ todo.title }}</span>
</label>
<button class="btn btn-danger">删除</button>
</li>
</template>
<script lang="ts" setup>
interface ITodoItem {
id: number
title: string
done: boolean
}
defineProps<{
todo: ITodoItem
}>()
</script>
06.提取类型以及配置 @ 路径
知识点:
在开发的过程中,目前存在两个问题:
- 定义的类型散落在每个
SFC
中,对于每个组件中都应用的类型,不利于类型复用 - 路径以相对路径的方式进行引入,不方便组件的复用
对于第一个问题,需要修改 vite.config.js
以及 tsconfig.json
文件,详细文档:alias 别名
➡️ vite.config.js
作用: 配置 src
路径别名 @
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
+ import path from 'path'
export default defineConfig({
plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src')
+ }
+ }
})
➡️ tsconfig.json
作用: 让 ts
识别 @
路径
{
"compilerOptions": {
// coding...
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
对于第二个问题,可以在 src
目录下创建 types/todos.ts
文件,将公共的类型都放到该文件即可
落地代码:
➡️ types/todos.ts
export interface ITodoItem {
id: number
title: string
done: boolean
}
<script lang="ts" setup>
import type { ITodoItem } from '@/types/todos'
// ...
</script>
07.TodoList-新建一个任务
知识点:
-
在
Header.vue
组件中给Input
输入框绑定键盘弹起事件addTodoFunc
,同时绑定.enter
修饰符 -
在
addTodoFunc
事件中获取用户输入的值,组装成todo
数据格式,通过emit
发送给父组件App.vue
-
在
App.vue
中向任务列表TodoList
中使用unshift
逆向添加属性
落地代码:
➡️ App.vue
<template>
<div class="todo-container">
<div class="todo-wrap">
<!-- 头部组件 -->
+ <Header @add-todo="addTodo" :list="state.todoList" />
<!-- 列表区域 -->
<List :list="TodoList" />
<!-- 脚本区域 -->
<Footer />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
import type { ITodoItem } from './types/todos'
import Header from './components/Header/index.vue'
import List from './components/List/index.vue'
import Footer from './components/Footer/index.vue'
// 初始化数据
const initList = [
{ id: 1, title: '吃饭', done: false },
{ id: 2, title: '睡觉', done: true },
{ id: 3, title: '学习', done: false },
{ id: 4, title: '打豆豆', done: true }
]
// 任务项列表
const TodoList = reactive<ITodoItem[]>(initList)
+ // 新增 todo 任务
+ const addTodo = (newTodo: ITodoItem) => {
+ state.todoList.unshift(newTodo)
+ }
</script>
➡️ Header/index.vue
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" @keyup.enter="addTodoFunc" />
</div>
</template>
<script lang="ts" setup>
import type { ITodoItem } from '../../types/todos'
const props = defineProps<{
list: ITodoItem[]
}>()
// 定义组件要触发的事件
const emit = defineEmits<{
(e: 'addTodo', todo: ITodoItem): void
}>()
// 添加 todo
const addTodoFunc = (e: Event) => {
// 获取用户输入的值
const { value } = e.target as HTMLInputElement
const target = e.target as HTMLInputElement
// 计算需要添加的 id
const ids = props.list.map((todo) => todo.id)
// 如果 ids 长度为 0 ,说明 todos 没有任何数据,那么新增项的 id = 1,否则求最大值 + 1
let maxId = ids.length === 0 ? 1 : Math.max.apply(null, ids) + 1
// 判断用户是否输入了任务名称
if (value.trim() !== '') {
const params = {
id: maxId,
title: value,
done: false
}
emit('addTodo', params)
target.value = ''
} else {
alert('请输入你的任务名称')
}
}
</script>
08.TodoList-删除一个任务
知识点:
使用 PubSubJS
消息订阅的方式实现删除一个任务
使用步骤:
- 安装
PubSubJS
库:npm install pubsub-js
- 使用
PubSubJS
库提供的publish
、subscribe
、unsubscribe
方法 - 点击删除按钮,给删除按钮绑定点击事件,通过
publish
发布事件 - 在
App
组件onMounted
生命周期函数中订阅事件 - 在
App
组件onBeforeUnmount
生命周期函数中订阅事件
落地代码:
➡️ Item/index.vue
<template>
<li>
<label>
<input type="checkbox" />
<span>{{ todo.title }}</span>
</label>
+ <button class="btn btn-danger" @click="delTodoFunc(todo.id)">删除</button>
</li>
</template>
<script lang="ts" setup>
+ import PubSub from 'pubsub-js'
import { ITodoItem } from '../../types/todos'
defineProps<{
todo: ITodoItem
}>()
+ const emit = defineEmits<{
+ (e: 'delTodo'): void
+ }>()
+ const delTodoFunc = (id: number) => {
+ if (confirm('您确定删除该任务吗?')) {
+ PubSub.publish('delTodo', id)
+ }
}
</script>
➡️ App.vue
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, reactive } from 'vue'
import PubSub from 'pubsub-js'
// coding...
let delTodo: string
// 订阅事件
// 删除任务
onMounted(() => {
delTodo = PubSub.subscribe('delTodo', (msg, id) => {
// 获取需要删除的 todo 索引
const index = state.todoList.findIndex((todo) => todo.id === Number(id))
// 使用变更方法进行删除
state.todoList.splice(index, 1)
})
})
// 组件销毁的时候取消订阅
onBeforeUnmount(() => {
PubSub.unsubscribe(delTodo)
})
</script>
09.TodoList-单个任务状态修改
知识点:
使用 PubSubJS
消息订阅的方式实现删除一个任务
- 给单个复选框绑定
change
事件,同时传递id
和 事件对象 - 在
change
事件的事件处理程序中,通过publish
发布事件,同时传递参数 - 在
App
组件onMounted
生命周期函数中订阅事件 - 在
App
组件onBeforeUnmount
生命周期函数中订阅事件
落地代码:
➡️ Item/index.vue
<template>
<li>
<label>
+ <input type="checkbox" :checked="todo.done" @change="changeTodoFunc(todo.id, $event)" />
<span>{{ todo.title }}</span>
</label>
<button class="btn btn-danger" @click="delTodoFunc(todo.id)">删除</button>
</li>
</template>
<script lang="ts" setup>
import PubSub from 'pubsub-js'
import { ITodoItem } from '../../types/todos'
defineProps<{
todo: ITodoItem
}>()
const emit = defineEmits<{
(e: 'delTodo'): void
+ (e: 'changeTodo'): void
}>()
// 删除 todo
// coding......
+ // 更新 todo 状态
+ const changeTodoFunc = (id: number, e: Event) => {
+ const { checked } = e.target as HTMLInputElement
+ console.log(checked)
+ PubSub.publish('changeTodo', { id, checked })
+ }
</script>
➡️ App.vue
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, reactive } from 'vue'
import PubSub from 'pubsub-js'
// coding......
let changeTodo: string
// 切换单个任务
onMounted(() => {
changeTodo = PubSub.subscribe('changeTodo', (msg, { id, checked }) => {
const todo = state.todoList.find((todo) => todo.id === Number(id))
if (todo) {
todo.done = checked
}
})
})
// 取消订阅
onBeforeUnmount(() => {
PubSub.unsubscribe(delTodo)
+ PubSub.unsubscribe(changeTodo)
})
</script>
10.TodoList-批量删除已完成任务
知识点:
- 给清除已完成任务按钮绑定点击事件
clearDoneTodo
- 在
clearDoneTodo
事件的事件处理程序中,通过emit
触发自定义事件clearTodo
- 在
App
组件Footer
组件标签上监听自定义事件clearTodo
- 在对应的事件处理程序中将已完成的任务进行清除
落地代码:
➡️ App.vue
<!-- 脚本区域 -->
<Footer @clearTodo="clearTodo" />
<script lang="ts" setup>
// coding......
// 初始化数据
const initList = [
{ id: 1, title: '吃饭', done: false },
{ id: 2, title: '睡觉', done: true },
{ id: 3, title: '学习', done: false },
{ id: 4, title: '打豆豆', done: true }
]
// 任务项列表
let state = reactive<{
todoList: ITodoItem[]
}>({
todoList: initList
})
// coding......
// 清除已经完成的任务
const clearTodo = () => {
state.todoList = state.todoList.filter((todo) => todo.done === false)
}
</script>
➡️ Footer/index.vue
<template>
<div class="todo-footer">
<label>
<input type="checkbox" />
</label>
<span> <span>已完成0</span> / 全部2 </span>
<button class="btn btn-danger" @click="clearDoneTodo">清除已完成任务</button>
</div>
</template>
<script lang="ts" setup>
// 定义组件要触发的事件
const emit = defineEmits<{
(e: 'clearTodo'): void
}>()
// 清除已完成任务
const clearDoneTodo = () => {
if (confirm('确定删除已完成任务?')) {
emit('clearTodo')
}
}
</script>
##11.TodoList-全部任务和已完成任务数量
知识点:
在 App.vue
中,利用计算属性计算出全部任务和已完成任务的数量,通过属性绑定传递给 Footer
组件
在 Footer
组件中, 通过 props
接收传递的数量
落地代码:
➡️ App.vue
<template>
<div class="todo-container">
<div class="todo-wrap">
<!-- 头部组件 -->
<Header @add-todo="addTodo" :list="state.todoList" />
<!-- 列表区域 -->
<List :list="state.todoList" />
<!-- 脚本区域 -->
<Footer @clearTodo="clearTodo" :doneTotal="doneTotal" :total="total" />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, reactive } from 'vue'
// coding......
// 总条数
const total = computed(() => {
return state.todoList.length
})
// 已完成总数量
const doneTotal = computed(() => {
return state.todoList.filter((item) => item.done).length
})
</script>
➡️ Footer/index.vue
<template>
<div class="todo-footer">
<label>
<input type="checkbox" />
</label>
<span>
<span>已完成 {{ doneTofal }}</span> / 全部 {{ total }}
</span>
<button class="btn btn-danger" @click="clearDoneTodo">清除已完成任务</button>
</div>
</template>
<script lang="ts" setup>
// 接收传递的总数量和已完成个数
defineProps<{
doneTofal: number | string
total: number | string
}>()
// coding......
</script>
12.TodoList-全选和反选任务
知识点:
- 每个任务都被勾选时,全选按钮需要选中,利用计算属性判断
total
和doneTotal
是否相等即可 - 监听全选按钮状态是否改变,如果改变获取状态
checked
通过自定义事件传递给父级 App
中监听触发的自定义事件,在事件处理程序中将全部状态进行改变
落地代码:
➡️ Footer/index.vue
<template>
<div class="todo-footer">
<label>
+ <input type="checkbox" :checked="selectAll" @change="changeSelectFunc" />
</label>
<span>
<span>已完成 {{ doneTotal }}</span> / 全部 {{ total }}
</span>
<button class="btn btn-danger" @click="clearDoneTodo">清除已完成任务</button>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
const props = defineProps<{
doneTotal: number | string
total: number | string
}>()
// 定义组件要触发的事件
const emit = defineEmits<{
(e: 'clearTodo'): void
+ (e: 'changeSelect', checked: boolean): void
}>()
// 清除已完成任务
// coding......
// 全选与全不选
+ const changeSelectFunc = (e: Event) => {
+ const { checked } = e.target as HTMLInputElement
+ emit('changeSelect', checked)
+ }
+ // 计算是否全选
+ const selectAll = computed(() => {
+ return props.doneTotal === props.total
+ })
</script>
➡️ App.vue
<Footer
:doneTotal="doneTotal"
:total="total"
@clearTodo="clearTodo"
@changeSelect="changeSelect"
/>
<script lang="ts" setup>
// coding......
// 全选与反选
const changeSelect = (status: boolean) => {
state.todoList.forEach((item) => (item.done = status))
}
</script>
13.TodoList-任务的持久化存储
知识点:
在 App
中使用 watch
监听数据 todoList
是否改变
如果数据发生变化,利用本地存储 localStorage
实现数据的持久存储
落地代码:
➡️ App.vue
<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue'
// coding......
// 初始化数据
+ const initList = (JSON.parse(localStorage.getItem('todos') as string) as ITodoItem[]) || []
// 任务项列表
let state = reactive<{
todoList: ITodoItem[]
}>({
todoList: initList
})
// coding......
+ watch(
+ () => state.todoList,
+ (newList) => {
+ localStorage.setItem('todos', JSON.stringify(newList))
+ },
+ {
+ deep: true
+ }
+ )
+ watch(state.todoList, (newList) => {
+ localStorage.setItem('todos', JSON.stringify(newList))
+ })
</script>
14.Pinia-初始 Pinia 状态管理工具
14.1 Pinia 介绍
Pinia
是 Vue
新一代的状态管理器,作用类似于 Vuex
,是 Vue
的另一种状态管理方案,由 Vue.js
团队中核心成员所开发的,目前 Pinia
已经被纳入官方正式的管理库, Pinia
因此也被认为是 下一代的 Vuex
Pinia
的设计试图尽可能地接近 Vuex
的理念。它的设计是为了测试 Vuex
的下一次迭代的建议,它是成功的,因为我们目前有一个开放的 RFC,用于 Vuex 5
,其 API
与 Pinia
使用的非常相似。我对这个项目的个人意图是重新设计使用全局 Store
的体验,同时保持 Vue
的平易近人的理念。我保持 Pinia
的 API
与 Vuex
一样接近,因为它不断向前发展,使人们很容易迁移到 Vuex
,甚至在未来融合两个项目(在 Vuex
下)
Pinia
支持 Vue2
和 Vue3
两个版本,尤其在 Vue3
的项目中备受推崇
14.2 Pinia 优点
- 使用简单上手快: 更容易组织代码的结构,如果你使用 Vuex 基础,基本上没有任何的学习成本
- 支持 TypeScript : 完美支持 TypeScript,通过自动类型推断让代码更加安全
- Vue2\Vue3都支持 : 除了初始化配置,API 的使用是相同的,(Vuex 4 用于 Vue3、Vuex 3 用于 Vue 2)
- 关联 Vue Devtools : 能够跟踪时间线,提供更好地开发体验
- 支持插件拓展功能: 响应 store 的变化,从而可以使用插件拓展 Pinia 功能
- 模块化设计思想 : 构建多个 store 模块,打包时自动化代码分隔
- 体积小,极其轻量 : 只有1.5 kb,甚至可以忽略它的存在
14.3 Pinia Vs Vuex
Pinia
剔除了Mutations API
,Mutations
的设计一直是诟病- 模块没有更多的嵌套结构,没有命名空间模块,可以声明多个
Store
- 可以直接获取
Getters
数据 - 更好的
TypeScript
支持,创建自定义的复杂包装器来支持TypeScript
,所有内容都是类型化的,并且API
的设计方式尽可能地利用TS
类型推断 - 不再需要引入魔法字符(复杂的
API:commit/dispatch
),导入方法然后调用,享受自动补全效果
对比 | Pinia | Vuex 4 |
---|---|---|
TypeScript 支持 | ✅ | ❌(不友好) |
Vue DevTools 支持 | ✅ | ✅ |
##15.Pinia-创建项目并初始化 Pinia
知识点:
- 使用
Vite
新建项目:npm create vite@latest pinia-todo -- --template vue-ts
- 安装
pinia
:npm install pinia
- 在入口文件
main.js
中导入并配置pinia
- 从
pinia
中导入createPinia
方法 - 创建
pinia
实例,将pinia
实例挂载到应用程序上
- 从
落地代码:
➡️ main.js
import { createApp } from 'vue'
import App from './App.vue'
// 导入 pinia
import { createPinia } from 'pinia'
const app = createApp(App)
// 创建 store 实例
const pinia = createPinia()
// 将 pinia 实例挂载到应用程序上
app.use(pinia)
app.mount('#app')
##16.Pinia-理解并创建 Store 仓库
知识点:
Store
是保存状态和业务逻辑的实体,它没有绑定到组件树。换句话说,它承载全局状态。它有点像一个总是存在的组件,每个人都可以读取和写入。它有三个核心概念,State
、Getters
和 Actions
,可以想当然地认为这些概念等同于组件中的 data
、computed
和 methods
State
→ 类似组件的data
,用来存储全局状态Getters
→ 类似组件的computed
,根据已有的state
封装派生数据,也有缓存特性Actions
→ 类似组件的methods
,用来封装业务逻辑,同步异步都可以
Store
包含可以在整个应用程序中访问的数据,通过 defineStore
方法来创建一个 store
,接收两个参数:
- 第一个参数为:
store
的id
,应保证其唯一性,Pania
使用它来将store
连接到devtools
- 第二个参数为:初始化配置,包含
state
,getters
,actions
等可选项,各项都类似Vuex
state
为一个函数,返回一个对象,类似于Vue
的Options API
中的data
选项getters
,actions
分别是一个对象
落地代码:
➡️ store/count.ts
// 导入 defineStore 方法
import { defineStore } from 'pinia'
// 通过 defineStore 方法来创建一个 store
export const useCounterStore = defineStore('counter', {
state: () => ({
counter: 0
}),
actions: {},
getters: {}
})
17.Pinia-使用初始数据并修改数据
知识点:
state
用来存储全局状态,定义为一个返回初始 state 的函数
state
为一个函数,返回一个对象,类似于 Vue
的 Options API
中的 data
选项
defineStore
创建的 Store
模块,本质上一个方法,调用这个方法会发现。Store
其实是一个用 reactive
包装的对象,这意味着不需要在 getter
后面写 .value
,但是,就像 setup
中的属性一样,我们不能对它进行解构,如果组件中使用数据时进行解构,会失去响应式能力,这时候我们可以使用 Pinia
提供的 storeToRefs
方法
使用数据:
- 直接导入通过
defineStore
创建的Store
模块import { useCounterStore } from './stores/counter'
- 将导入的模块当成方法进行调用
const counterStore = useCounterStore()
- 通过返回值能够获取到初始化的数据,直接使用即可
const { counter } = storeToRefs(counterStore)
修改数据:
- 方法 1:对导入的数据进行进行操作,例如:
counter += 1
- 方法 2:通过导入的模块调用
$patch
,传入对象,例如:countStore.$patch({ xxxx })
- 方法 2:通过导入的模块调用
$patch
,传入方法,例如:countStore.$patch((state) => {})
落地代码:
➡️ App.vue
<template>
<div class="container">
<h2>{{ counter }}</h2>
<button @click="addCounter">+1</button>
<button @click="subCounter">-1</button>
<button @click="rideCounter">*2</button>
<button>async +1</button>
</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from './stores/counter'
// 读取数据
// const counterInfo = useCounterStore()
// console.log(counterInfo.counter)
// 读取数据,并使用 storeToRefs 方法让结构出来的数据具有响应式
const counterStore = useCounterStore()
const { counter } = storeToRefs(counterStore)
// 加 1:修改数据的方法 1
const addCounter = () => {
counter.value += 1
}
// 减 1:修改数据的方法 1
const subCounter = () => {
counterStore.$patch({
counter: (counter.value -= 1)
})
}
// 乘 2:修改数据的方法 1
const rideCounter = () => {
counterStore.$patch((state) => {
state.counter *= 2
})
}
</script>
18.Pinia-Pinia 中的 Getters
知识点:
getters
和 vue
中的 computed
选项类似,依赖 state
中的数据并返回一个新的值
getters
的使用和 state
的使用方式一样
- 导入使用
defineStore
创建的Store
模块 - 将模块当成方法调用,返回的值就包含
Getters
落地代码:
➡️ store/count.ts
// 导入 defineStore 方法
import { defineStore } from 'pinia'
// 通过 defineStore 方法来创建一个 store
export const useCounterStore = defineStore('counter', {
state: () => ({
counter: 0
}),
actions: {},
+ getters: {
+ newCounter(state) {
+ return state.counter * 2
+ }
+ }
})
➡️ App.vue
<template>
<div class="container">
<h2>{{ counter }}</h2>
+ <h2>Getters: {{ newCounter }}</h2>
<button @click="addCounter">+1</button>
<button @click="subCounter">-1</button>
<button @click="rideCounter">*2</button>
<button>async +1</button>
</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from './stores/counter'
// 读取数据
const counterStore = useCounterStore()
// 使用 storeToRefs 方法让结构出来的数据具有响应式
+ const { counter, newCounter } = storeToRefs(counterStore)
// coding......
</script>
19.Pinia-使用 Actions 处理数据变更和异步
知识点:
Actions
相当于组件中的 methods
在 actions
对象中定义方法,
在组件中通过导入的 Store
,直接调用方法即可
落地代码:
➡️ store/count.ts
// 导入 defineStore 方法
import { defineStore } from 'pinia'
// 通过 defineStore 方法来创建一个 store
export const useCounterStore = defineStore('counter', {
state: () => ({
counter: 0
}),
+ actions: {
+ // 更新 counter
+ updateCounter() {
+ setTimeout(() => {
+ this.counter += 1
+ }, 1000)
+ }
+ },
getters: {
newCounter(state) {
return state.counter * 2
}
}
})
➡️ App.vue
<template>
<div class="container">
<h2>{{ counter }}</h2>
<h2>Getters: {{ newCounter }}</h2>
<button @click="addCounter">+1</button>
<button @click="subCounter">-1</button>
<button @click="rideCounter">*2</button>
+ <button @click="asyncAddCounter">async +1</button>
</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from './stores/counter'
// 读取数据
const counterStore = useCounterStore()
// 使用 storeToRefs 方法让结构出来的数据具有响应式
const { counter, newCounter } = storeToRefs(counterStore)
// coding......
+ // 异步 + 1
+ const asyncAddCounter = () => {
+ counterStore.updateCounter()
+ }
</script>
20.组件通讯-消息订阅库的使用
知识点:
Vue 3
中移除了 $on
、$off
、 $once
这几个实例方法,但可以通过使用实现事件发射器接口的第三方库来替换事件总线。例如:PubSubJs or mitt
使用步骤:
- 安装
PubSubJS
库:npm install pubsub-js
- 使用
PubSubJS
库提供的publish
、subscribe
、unsubscribe
方法
落地代码:
➡️ App.vue
<script lang="ts" setup>
import { onMounted } from 'vue'
import PubSub from 'pubsub-js'
onMounted(() => {
PubSub.subscribe('xxxx', (msg, id) => {
console.log('自定义事件的名称 ' + msg)
console.log('传递的数据 ' + id)
})
})
</script>
➡️ children.vue
<script lang="ts" setup>
import PubSub from 'pubsub-js'
const emit = defineEmits<{
(e: 'xxxx'): void
}>()
const delTodoFunc = (id: number) => {
PubSub.publish('xxxx', id)
}
</script>
##21.组件通讯-$attr 的使用
##22.组件通讯-$parent 的使用