Vue3 学习实践笔记

注:以下代码片段需要结合你的实际项目结构进行实现,部分功能需要安装对应依赖

该vue3部署样例网站:yamds.github.io

项目链接:https://github.com/Yamds/yamds.github.io,欢迎点个star QAQ

一、学习路径

二、核心要点

一. 组件通信方式对比

1.1 事件总线(Mitt

实现原理

Mitt 是一个微型事件发射器(200字节),通过发布-订阅模式实现组件间通信。适合处理父子组件或兄弟组件的简单通信。

典型场景

// event-bus.ts
import mitt from 'mitt'
type Events = { 'search': string }
export const emitter = mitt<Events>()

// 导入包一个发送 一个接收就行了,可以有多个接收
// 组件A
emitter.emit('search', query)

// 组件B
emitter.on('search', (query) => {
  // 处理搜索逻辑
})

优缺点:

✓ 轻量

✓ 适合简单场景

✗ 事件难以追溯

✗ 全局污染风险

1.2 🍍Pinia 🍍🍍状态管理🍍🍍

特点:🍍

  • 逻辑关注点集中,🍍相关代码组织在一起

  • 🍍天然支持 TypeScript 类型推断🍍

使用样例

// stores/useCounterStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // 状态定义
  const count = ref(0)

  // Getter(计算属性)
  const doubleCount = computed(() => count.value * 2)

  // Action
  function increment(step = 1) {
    count.value += step
    updateTimestamp()
  }

  function reset() {
    count.value = 0
    updateTimestamp()
  }

  return { 
    count,
    doubleCount,
    increment,
    reset
  }
})
// components/Counter.vue

<script setup>
import { useCounterStore } from '@/stores/useCounterStore'
  let counter = useCounterStore()
</script>

<template>
  <div>
    <p>当前计数: {{ counter.count }}</p>
    <p>双倍计数: {{ counter.doubleCount }}</p>
    
    <button @click="counter.increment()">增加</button>
    <button @click="counter.reset()">重置</button>
  </div>
</template>

优势

🍍类型安全🍍

🍍模块化设计🍍

🍍服务端渲染友好🍍

二、数据处理

1.使用场景

  1. 有一个对象包含"name": "xxx", "age": "18",如何通过对象的name拿到age?

  2. 给一个由人能看懂的字符串"方案25 项链 树皮5x*3 兽核5x*1",然后附上装备格式.json,如何将这一字符串解析为一个机器可读装备对象?或者反之应该如何做。

  3. (各种需要解析数据的场景)......

2.常用的数组函数方法

.split()

  • 不改变原数组

  • 以参数为断点,切片字符串,每一片化为字符串数组的一个元素

  • 逗号可替换为其他间隔符号

字符串 ==> 数组

let str = "hi hello 你好啊 awa"
console.log(str.split(" "))  // 以空格符作为断点
// ["hi", "hello", 你好啊", "awa"]

.splice(a, b)

  • 改变原数组

  • 从字符串特定下标a取b个值出来

  • 第二个参数可省略,意为取特定下标a之后的全部内容

字符串 ==> 字符串

let str1 = "aabbccdd"
let str2 = "aabbccdd"
console.log(str1.splice(3, 4))
console.log(str2.splice(3))
// bccd
// bccdd

.map()

  • 不改变原数组

  • 以我粗浅的理解,可以看似类似迭代器的用法,但是返回值是一个新数组,用item将数组遍历了一遍。

  • 参数也可以写为(item,index,arr),表示为正在操作的元素、操作元素的下标,被操作的这个数组整体。

数组 ==> 数组

let arr = [1, 2, 3, 4, 5]
let arr2 = arr.map((item) => {
  return item*2
})
console.log(arr2)
// [2, 4, 6, 8, 10]

.find()

  • 不改变原数组,返回数组的其中一个元素

  • 作用是返回满足条件的第一个元素

数组 ==> 元素

let arr = [[0, 1], [0, 2], [1, 1], [1, 2]]
let findElem = arr.find((elem) => {
  return elem[0] === 1
})
console.log(findElem)
// 返回[1, 1],即第三个元素数组,可以看到第四个其实也符合,但是只返回第一个符合的

.forEach()

  • 是否会修改原数组?

  • 当遍历的是基本类型,比如数字,则不会改变原数组

  • 当遍历的是对象,而且直接拿新的对象去覆盖,也不会改变原数组

  • 当遍历的是对象,但是使用item.keys = xxx去修改对象的某一个元素,则改变了原数组

  • 如果想对原数组进行操作,可以使用(item, index, arr)来操作,item为遍历的对象,arr是被操作的原数组,可以直接对arr进行操作,一些操作条件之类的,则可以使用item或index

数组 ==> 数组

let numArr = [1, 2, 3]
numArr.forEach((num) => {
  num = num*2 
})
console.log(numArr)
// [1, 2, 3], 基本类型的数组没有发生改变
let objArr = [{name: "yamds", age: 18}, {name: "kumiko", age: 17}]
objArr.forEach((num) => {
  num = num*2 
})
console.log(numArr)
// [1, 2, 3], 基本类型的数组没有发生改变
let numArr = [1, 2, 3]
numArr.forEach((num) => {
  num = num*2 
})
console.log(numArr)
// [1, 2, 3], 基本类型的数组没有发生改变

.filter()

  • 不改变原数组,返回新数组

  • 顾名思义,以一定条件过滤原数组

数组 ==> 数组

let arr = [1, 2, 3, 4, 5]
let arr2 = arr.filter((item) => {
  return item>2
})
console.log(arr2)
// [3, 4, 5]

.reduce()

  • 功能强大的聚合计算,将每一次计算的结果存放在sum里。

  • 像求和,出现次数,去重等功能都可以用它实现,可以代替很多需要写循环的代码(逼格更高awa)

  • 可选参数(index, arr, init),index和arr同上map(),init参数作为计算结果的初始值

数组 ==> (根据需求)

// 求和
let arr = [1, 2, 3, 4, 5]
let numSum = arr.reduce((sum, num) => {
  return sum+num
})
console.log(numSum)
// 15
// 求n次方,并且初始值为1
let arr = [1, 2, 3, 4, 5]
let numSum = arr.reduce((sum, num) => {
  return sum*num
}, 1) // 初始值变量
console.log(numSum)
// 15
// 去重
let arr = [1, 5, 2, 5, 2]
let arr2 = arr.reduce((newList, index, num) => {
  if(newList.indexOf(num) === -1) {  // 逻辑: indexOf未在新数组找到要添加的数,则添加。如果发现添加过就跳过
    newList.push(num)
  }
  return newList
}, []) // 初始值为空数组
console.log(arr2)
// [1, 5, 2]

3. 常用的对象函数方法

Object.entries()

将对象的键值对转换为键值对数组

对象 ==> 数组

let obj = {
  name: "yamds"
  age: 18
}
console.log(Object.entries(obj))
// [["name", "yamds"], ["age", 18]]

Object.keys()

将对象的键名组合成数组

对象 ==> 数组

let obj = {
  name: "yamds"
  age: 18
}
console.log(Object.keys(obj))
// ["name", "age"]

Object.values()

将对象的每个键对应的值组合成数组

对象 ==> 数组

let obj = {
  name: "yamds"
  age: 18
}
console.log(Object.keys(obj))
// ["yamds", 18]

Object.assign(target, s1, s2...)

  • 将s1、s2...的可枚举属性拷贝到target中,如果有重复键则覆盖,返回target

  • 相当于把对象插进对象,但是当两个对象键是一样时,和直接用等于号并不相同,
    例如当使用vue3的reactive对象时,使用Object.assign()不会取消原对象的响应式状态。

const target = { a: 1, b: 2 };
const s1 = { b: 4, c: 5 };
const s2 = { b: 6, c: 7 };
const obj = Object.assign(target,s1,s2);
console.log(obj); // (a: 1, b: 6, c: 7)

4. 用法举例

将"布匹3x*3 兽核5x*1 残骸4x*2"转换为材料对象

// 选中的装备材料,还需要附带星级属性
// "布匹3x*3 兽核5x*1 残骸4x*2"
export interface SelectedArmorMaterialInter {
    material: string,
    star: number
}

let str = "布匹3x*3 兽核5x*1 残骸4x*2"
let strArr1 = str.split(" ")
// ["布匹3x*3", "兽核5x*1", "残骸4x*2"]
let strArr2 = strArr1.map((item) => {
  return item.split("*")
})
// [["布匹3x", "3"], ["兽核5x, "1"], ["残骸4x", "2"]]
let strArr3 = strArr2.map((item) => {
  let star = parseInt(item[0].slice(-2, -1)); // 提取倒数第二个字符作为star
  let material = item[0].slice(0, -2)  // 提取材料名称
  return {"material": meterial, "star": star}
}}
// [[{"material": "布匹", "star": 3}, "3"], 
//  [{"material": "兽核", "star": 5}, "1"], 
//  [{"material": "残骸", "star": 4}, "2"]]
let strArr4 = strArr3.reduce((newList, item) => {
  // 将item[1]里的次数转换为数组个数,再将对象填充进去
  newList.push(Array(parseInt(item[1])).fill({ "material": material, "star": star }))
}, [])
// 最终创建出来的数组,嵌套数组里只剩一个对象元素,所以使用flat扁平化数组,只保留一层
console.log(strArr4.flat())
// [{"material": "布匹", "star": 3}, 
//  {"material": "布匹", "star": 3},
//  {"material": "布匹", "star": 3},
//  {"material": "兽核", "star": 5},
//  {"material": "残骸", "star": 4},
//  {"material": "残骸", "star": 4},
//  {"material": "残骸", "star": 4}]
// 拿到想要的数据!
// 层层结构, 想一次处理也可以比如item.split().map((i) => {return i.reduce()}),只不过看着费劲,需要写好注释

三. 交互功能实现

剪贴板功能

这里使用的clipboard库,需要注意这个库只能复制可输入框的内容,input、textarea...

直接复制输入库自不必说,如果需要复制例如span或div里的内容,可以参考以下:

  1. 首先导入包,创建一个看不见的textarea,{{ copyData }}是vue里的变量,放你需要复制的内容。

  2. 在你的复制按钮上,使用data-clipboard-target绑定上文文本域的id。

import ClipboardJS from 'clipboard';
// --------
<div class="checkbox-click" data-clipboard-target="#copy-id">
  <span>复制至剪切板</span>
</div>
<textarea id="copy-id" style="position: absolute; left: -9999px;">{{ copyData }}</textarea>

可编辑文本

几个要点:

  • 使用一个变量editable = false来控制文本框和div互相切换

  • div可通过点击事件来控制变量,input则使用失焦事件和捕获回车

editable true ==> div隐藏,input出现

editable false ==> div出现,input隐藏

  • input.value与某一个变量相连,每当input触发失焦和捕获,则将这个变量同步进div里的文本

  • 只有一个可输入框可以让input和直接和div的文本相连,如果是多个输入框则必须采用下面同步的方式,或使用数组input[]

<template>
  <div>
    <span v-if="!editable" @click="editable = true">{{ text }}</span>
    <input v-else v-model="input" @blur="saveText" @keyup.enter="saveText">
  </div>
</template>

<script setup>
  import { ref } from 'vue'
  let text = ref('')
  let input = ref('')

  function saveText() {
    editable = false
    text.value = input.value
    input.value = ''
  }
</script>

四. 弹窗组件实现

不涉及具体样式

在弹窗组件内,有一个数组存放所有的弹窗信息对象

弹窗信息包括: id, type(error, warning, info...),title, desc

// Tips.vue
<template>
    <div class="tips-container">
        <!-- 循环放置tips数组里的弹窗对象 -->
        <!-- 通用样式tips,再根据不同的type添加不同的样式,例如背景颜色 -->
        <!-- 根据tip在列表里的index,设置tip的高度,实现添加新的tip可以自动下移,可添加动画 -->
        <div v-for="(item, index) in tips" :key="item.id" :class="['tips', item.type]" :style="{ top: `${index * 93 + 20}px` }">
            <!-- 根据type设置图标 -->
            <i :class="['bi', iconClass(item.type)]"></i>
            <!-- 标题和内容 -->
            <div class="content">
                <div class="title">{{ item.title }}</div>
                <div class="message">{{ item.message }}</div>
            </div>
           <!-- 关闭按钮 -->
            <button @click="removetips(item.id)">×</button>
        </div>
    </div>
</template>

<script setup name="Tips">
import { ref } from 'vue'
let idCounter = 0
// <>泛型固定tips属性,创建新列表
let tips = ref<{ 
    id: number, 
    title: string, 
    message: string, 
    type: string,
}[]>([])

function addTips(title: string, message: string, type: string = 'info') {
    let id = idCounter++
    tips.value.push({ id, title, message, type })
    // tip创建时添加一个计时器用于销毁tip
    setTimeout(() => removetips(id), 5000)
    // 大于7条tip直接remove
    if (tips.value.length > 7) {
        removetips(tips.value[0].id)
    }
}

let removetips = (id: number) => {
    // 根据tip.id,找到tip在数组里的index
    let index = tips.value.findIndex(n => n.id === id)
    if (index !== -1) {
      // 使用filter过滤数组中这个id 的tip
      tips.value = tips.value.filter(n => n.id !== id)
    }
}

// 将添加函数暴露给父组件App.vue,通过App来创建
defineExpose({ addTips })
</script>
// App.vue
<template>
    <div id="app">
        <Tips ref="tips" />
    </div>
</template>

<script lang="ts" setup name="App">
import { ref, onMounted } from 'vue'
import emitter from './utils/emitter'

const tips = ref<any>(null)

// 提供全局调用方法
onMounted(() => {
    // 使用上文所述的emitter,从全局组件接收tip参数,在这里添加新tip
    emitter.on('send-tips', (value: any) => {
        tips.value?.addTips(value.title, value.message, value.type)
    })
})
</script>
// test.vue
<template>
    <div id="test">
        <div @click="addTip">test</div>
    </div>
</template>

<script lang="ts" setup name="Test">
import emitter from './utils/emitter'

function addTip() {
    emitter.emit('send-tips', {
    title: '提示',
    message: '列表已清空~',
    type: 'info'
  })
}
</script>

五. GitHub Pages路由问题

给文件根目录添加一个404.html,既然github page可以自定义404页面那就好说,直接通过404跳转到index。

<!-- 404.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script>
    sessionStorage.redirect = location.href;
  </script>
  <meta http-equiv="refresh" content="0;URL='/'">
  <title>404</title>
</head>
<body>
</body>
</html>

没有涩话time(


Vue3 学习实践笔记
https://blog.yamds.fun/archives/vue3-note
作者
Yamds
发布于
2025年03月03日
更新于
2025年03月03日
许可协议