Vue3 学习实践笔记
注:以下代码片段需要结合你的实际项目结构进行实现,部分功能需要安装对应依赖
该vue3部署样例网站:yamds.github.io
项目链接:https://github.com/Yamds/yamds.github.io,欢迎点个star QAQ
一、学习路径
b站尚硅谷 vue3课程,尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程
学校课程,老师也有录播视频,【Vue快速入门】0基础轻松入门Vue,从入门到实战
官方文档精读 (组合式API + TypeScript支持),简介 | Vue.js
二、核心要点
一. 组件通信方式对比
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.使用场景
有一个对象包含"name": "xxx", "age": "18",如何通过对象的name拿到age?
给一个由人能看懂的字符串"方案25 项链 树皮5x*3 兽核5x*1",然后附上装备格式.json,如何将这一字符串解析为一个机器可读装备对象?或者反之应该如何做。
(各种需要解析数据的场景)......
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里的内容,可以参考以下:
首先导入包,创建一个看不见的textarea,{{ copyData }}是vue里的变量,放你需要复制的内容。
在你的复制按钮上,使用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(