vue3 早期学习 进阶篇

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 组件中引入和使用

HeaderListFooter 组件需要在 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-展示页面数据

知识点:

  1. App.vue 中定义数据,同时将数据通过属性绑定传递给 List 组件

  2. List 组件中通过 defineProps 接收 App.vue 中传递的数据

  3. 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.提取类型以及配置 @ 路径

知识点:

在开发的过程中,目前存在两个问题:

  1. 定义的类型散落在每个 SFC 中,对于每个组件中都应用的类型,不利于类型复用
  2. 路径以相对路径的方式进行引入,不方便组件的复用

对于第一个问题,需要修改 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-新建一个任务

知识点:

  1. Header.vue 组件中给 Input 输入框绑定键盘弹起事件 addTodoFunc,同时绑定 .enter 修饰符

  2. addTodoFunc 事件中获取用户输入的值,组装成 todo 数据格式,通过 emit 发送给父组件 App.vue

  3. 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 消息订阅的方式实现删除一个任务

使用步骤:

  1. 安装 PubSubJS 库:npm install pubsub-js
  2. 使用 PubSubJS 库提供的 publishsubscribeunsubscribe 方法
  3. 点击删除按钮,给删除按钮绑定点击事件,通过 publish 发布事件
  4. App 组件 onMounted 生命周期函数中订阅事件
  5. 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 消息订阅的方式实现删除一个任务

  1. 给单个复选框绑定 change 事件,同时传递 id 和 事件对象
  2. change 事件的事件处理程序中,通过 publish 发布事件,同时传递参数
  3. App 组件 onMounted 生命周期函数中订阅事件
  4. 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-批量删除已完成任务

知识点:

  1. 给清除已完成任务按钮绑定点击事件 clearDoneTodo
  2. clearDoneTodo 事件的事件处理程序中,通过 emit 触发自定义事件 clearTodo
  3. App 组件 Footer 组件标签上监听自定义事件 clearTodo
  4. 在对应的事件处理程序中将已完成的任务进行清除

落地代码:

➡️ 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-全选和反选任务

知识点:

  1. 每个任务都被勾选时,全选按钮需要选中,利用计算属性判断 totaldoneTotal 是否相等即可
  2. 监听全选按钮状态是否改变,如果改变获取状态 checked 通过自定义事件传递给父级
  3. 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 介绍

PiniaVue 新一代的状态管理器,作用类似于 Vuex,是 Vue 的另一种状态管理方案,由 Vue.js 团队中核心成员所开发的,目前 Pinia 已经被纳入官方正式的管理库, Pinia 因此也被认为是 下一代的 Vuex

Pinia 的设计试图尽可能地接近 Vuex 的理念。它的设计是为了测试 Vuex 的下一次迭代的建议,它是成功的,因为我们目前有一个开放的 RFC,用于 Vuex 5 ,其 APIPinia 使用的非常相似。我对这个项目的个人意图是重新设计使用全局 Store 的体验,同时保持 Vue 的平易近人的理念。我保持 PiniaAPIVuex 一样接近,因为它不断向前发展,使人们很容易迁移到 Vuex,甚至在未来融合两个项目(在 Vuex 下)

Pinia 支持 Vue2Vue3 两个版本,尤其在 Vue3 的项目中备受推崇

Pinia 官网地址

14.2 Pinia 优点

  1. 使用简单上手快: 更容易组织代码的结构,如果你使用 Vuex 基础,基本上没有任何的学习成本
  2. 支持 TypeScript : 完美支持 TypeScript,通过自动类型推断让代码更加安全
  3. Vue2\Vue3都支持 : 除了初始化配置,API 的使用是相同的,(Vuex 4 用于 Vue3、Vuex 3 用于 Vue 2)
  4. 关联 Vue Devtools : 能够跟踪时间线,提供更好地开发体验
  5. 支持插件拓展功能: 响应 store 的变化,从而可以使用插件拓展 Pinia 功能
  6. 模块化设计思想 : 构建多个 store 模块,打包时自动化代码分隔
  7. 体积小,极其轻量 : 只有1.5 kb,甚至可以忽略它的存在

14.3 Pinia Vs Vuex

  1. Pinia 剔除了 Mutations APIMutations 的设计一直是诟病
  2. 模块没有更多的嵌套结构,没有命名空间模块,可以声明多个 Store
  3. 可以直接获取 Getters 数据
  4. 更好的 TypeScript 支持,创建自定义的复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能地利用 TS 类型推断
  5. 不再需要引入魔法字符(复杂的API:commit/dispatch),导入方法然后调用,享受自动补全效果
对比PiniaVuex 4
TypeScript 支持❌(不友好)
Vue DevTools 支持

##15.Pinia-创建项目并初始化 Pinia

知识点:

  1. 使用 Vite 新建项目:npm create vite@latest pinia-todo -- --template vue-ts
  2. 安装 pinianpm install pinia
  3. 在入口文件 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 是保存状态和业务逻辑的实体,它没有绑定到组件树。换句话说,它承载全局状态。它有点像一个总是存在的组件,每个人都可以读取和写入。它有三个核心概念,StateGettersActions,可以想当然地认为这些概念等同于组件中的 datacomputedmethods

  1. State → 类似组件的 data,用来存储全局状态
  2. Getters → 类似组件的 computed,根据已有的 state 封装派生数据,也有缓存特性
  3. Actions → 类似组件的 methods,用来封装业务逻辑,同步异步都可以

Store 包含可以在整个应用程序中访问的数据,通过 defineStore 方法来创建一个 store,接收两个参数:

  1. 第一个参数为:storeid,应保证其唯一性,Pania 使用它来将 store 连接到 devtools
  2. 第二个参数为:初始化配置,包含 stategettersactions 等可选项,各项都类似 Vuex
    • state 为一个函数,返回一个对象,类似于 VueOptions API 中的 data 选项
    • gettersactions 分别是一个对象

落地代码:

➡️ 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 为一个函数,返回一个对象,类似于 VueOptions API 中的 data 选项

defineStore 创建的 Store 模块,本质上一个方法,调用这个方法会发现。Store 其实是一个用 reactive 包装的对象,这意味着不需要在 getter 后面写 .value,但是,就像 setup 中的属性一样,我们不能对它进行解构,如果组件中使用数据时进行解构,会失去响应式能力,这时候我们可以使用 Pinia 提供的 storeToRefs 方法

使用数据:

  1. 直接导入通过 defineStore 创建的 Store 模块
    • import { useCounterStore } from './stores/counter'
  2. 将导入的模块当成方法进行调用
    • const counterStore = useCounterStore()
  3. 通过返回值能够获取到初始化的数据,直接使用即可
    • const { counter } = storeToRefs(counterStore)

修改数据:

  1. 方法 1:对导入的数据进行进行操作,例如:counter += 1
  2. 方法 2:通过导入的模块调用 $patch,传入对象,例如:countStore.$patch({ xxxx })
  3. 方法 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

知识点:

gettersvue 中的 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

使用步骤:

  1. 安装 PubSubJS 库:npm install pubsub-js
  2. 使用 PubSubJS 库提供的 publishsubscribeunsubscribe 方法

落地代码:

➡️ 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 的使用

23.组件通讯-v-model结合组件使用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值