VUE3学习笔记
一、创建vue3项目工程
1.安装步骤
1.安装node.js,nodejs官网下载地址
2.cmd命令创建 npm create vue@latest
3.npm i (安装依赖包,在vs code的终端里执行,执行完重新打开)
4.安装vue插件,系统会自动提示安装,详见vscode=>extensions.json
2.相关知识
Vite 是一种新型的前端构建工具,主要用于构建现代 Web 应用。它与传统的构建工具(如 Webpack)不同,采用了一种基于 ES 模块的开发服务器启动策略,在开发阶段能够提供极快的冷启动速度和热更新速度。
Vue-cli 是一个官方的 Vue.js 项目脚手架工具。它可以帮助开发者快速搭建 Vue 项目的基本结构,包括目录结构、配置文件等。通过命令行工具,能够自动化地创建一个包含了 Webpack 等构建工具的项目模板。
Webpack 是一个功能强大的模块打包工具,用于现代 JavaScript 应用程序的静态模块打包。它可以将各种资源(如 JavaScript、CSS、图片等)看作模块,然后根据模块之间的依赖关系进行打包处理。
以上三个是竞争关系,Vite最快,现在用Vite
二、项目文件解析
1.项目目录
2.文件解释
- .vscode=>extensions.json :vs code的插件清单,打开vs code的时候会读这个json,检查有没有安装相关插件,没有的话会提示安装
- vscode=>setting.json :我也不知道干嘛的,豆包解释:通常是用于配置开发环境相关的设置。这个文件可以控制代码编辑器(如 Visual Studio Code)对 Vue 项目的支持方式,包括语法检查、代码格式化、智能提示等功能。不过需要注意的是,它不是 Vue 项目本身运行所必需的核心文件,而是用于辅助开发过程。
- node_modules :项目所有的依赖,执行过npm i 过后就有这个文件夹了。好像是根据 package-lock.json 下载依赖
- public:公共文件夹,放点公共用的东西,比如ico
- src:全拼是source,可以理解为源代码,就是用来写代码的地方
- components:组件,所有的组件都放这
- src=>App.vue:根组件,可以在这里引用其他组件,形成页面
- main.ts:入口ts文件,index.html引用了这个文件,引用vue创建应用方法,创建一个以app组件为根的应用,挂载到id为app的div上
- .gitigonre:是一个文本文件,用于告诉 Git 版本控制系统哪些文件或目录不需要被纳入版本控制。它在项目开发过程中起到了非常重要的作用,特别是当你想要排除一些临时文件、编译后的文件、敏感信息文件等,以保持仓库的整洁和安全。
- index.html:入口文件,这里引用了main.ts
- package.json:包管理文件,依赖生成文件
- package-lock.json:包管理文件,依赖生成文件
- env.d.ts:ts的类型声明,即让ts认识css、txt、png等常用文件后缀,没有这个文件间就无法互相引用了
- tsconfig.json:ts的配置文件,不用管
- tsconfig.app.json:ts的配置文件,不用管
- tsconfig.node.json:ts的配置文件,不用管
- vite.config.ts:整个工程的配置文件,安装插件、配置代理都可以在这操作
3.一个基础的组件
App.vue(根应用):
<template>
<div class="app">
<Car></Car> <!--引用组件 -->
</div>
</template>
<script lang="ts">
import Car from './components/Car.vue';//引入组件
export default{
name:'App',//组件名
components:{Car}//注册组件
}
</script>
<style>
.app{
color: rgb(25, 126, 203);
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
background-color: burlywood;
font-size: 50px;
}
</style>
Car.vue(组件):
<template>
<h1>{{title}}</h1>
<button @click="ChageTitle">修改标题</button>
</template>
<script lang="ts" setup >
import {ref} from 'vue'
let title=ref('hello world!');
function ChageTitle(){
title.value="new title!";
}
</script>
<style>
</style>
三、Vue3核心语法
1.选项式 API (Options API)与组合式 API (Composition API)
vue2是选项式,vue3是组合式。选项式就是数据、方法等都单独写;组合式就是数据、方法都写在一块。 官方解释与区别
2.setup
<script lang="ts" setup> let a=666;</script>
等价于
<script lang="ts" >
export default{
setup(){
let a=666;
return {a}
}
}</script>
在script里写上setup属性,就等于写了setup(){xxxx,return xxxx},是setup的语法糖
3.响应式数据(ref和reactive)
- 用ref和reactive包起来的是响应式数据,即值改变页面也会跟着变
- ref可以传入基本类型字段,也可以传入对象
- reactive只能传入对象 ref的相应对象,必须使用 .value 访问和修改(使用volar插件,可以自动生成 .value),reactive可以直接访问
- ref里面也是会把传入的参数用reactive包起来 var {a,b}=
- reactive无法直接更新整个对象。可以使用object.assign(obj1,obj2) ,这个可以把obj2拷贝到obj1,直接更新整个响应式对象,不然不生效(因为响应式对象变了)。这个是浅拷贝(问题:什么是浅拷贝?)
- ref可以更新整个value为新的对象
<script lang="ts" >
var a=ref("你好");
</script>
4.toRef和toRefs
解构:直接取一个对象里的字段,代码如下 (问题:对象有多层,怎么解构?)
<script lang="ts" >
var person=ref({name:"帅春",age:26,height:175})
var {name,age}=person
//这样可以直接拿到name和age
</script>
多层解构:
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
// 假设我们有一个嵌套的对象
const nestedObject = reactive({
user: {
name: 'Alice',
address: {
street: '123 Main St',
city: 'Exampleville',
},
contact: {
email: 'alice@example.com',
phone: '123-456-7890'
}
}
});
// 多层解构
const {
user: {
name,
address: { street, city },
contact: { email }
}
} = nestedObject;
// 使用解构后的变量
console.log(name); // 输出 "Alice"
console.log(street); // 输出 "123 Main St"
console.log(city); // 输出 "Exampleville"
console.log(email); // 输出 "alice@example.com"
// 如果需要的话,你也可以保留嵌套结构
const { user: { address } } = nestedObject;
console.log(address.street); // 输出 "123 Main St"
return {
name,
street,
city,
email
};
}
};
</script>
但是以上解构出来的name和age不是响应式的,如果需要解构出来的是响应式的,需要用toRefs
<script lang="ts" >
var person=ref({name:"帅春",age:26,height:175})
var {name,age}=toRefs(person)
//这样可以直接拿到name和age
</script>
toRef 给一个对象里的指定一个属性变成响应式,toRefs给整个对象所有的key、value都变成响应式
<script lang="ts" setup>
var person=ref({name:"帅春",age:26,height:175})
var {name,age}=toRefs(person) //toRefs
var nl=toRef(person,'age') //toRef
</script>
5.computed计算属性
computed可以当fuction用,computed里用到的变量发生了变化,计算的结果就会立马重新计算,而function需要调用才能重新计算
<script lang="ts" setup>
let name=ref("li")
let xing=ref("si")
let fullName=computed({
get(){
return name+xing;//fullname的值为lisi,只有name或者xing发生变化的时候,fullname才会重新计算
},
set(value){
return value;//不写set的话,fullname是只读的,有set就可以修改fullname的值了
}
//如果fullname不需要修改的话,直接简写如下
let fullname=computed(()=>{
return name+xing;//此时fullname为只读的
})
})
</script>
6.watch监视
作用:监听一个东西发生变化时,执行方法
可监听的类型:ref 、recatvite、getter【getter就是返回一个值的函数,类似委托 “()=>变量”=function(){return 变量}】
语法:watch(你要监听的对象,(newvalue,oldvalue)=>{你要干的事},{配置对象【一般不用写,有deep、immediate】})
停止监视:watch函数返回一个函数,调用即可停止,下面例子写的有
(一)举个栗子(监听ref基本类型数据):
<script lang="ts" setup>
import {ref,watch} from 'vue'
let num=ref(0)
watch(num,(newvalue,oldvalue){ //可以只写一个参数,接收的是新的值
console.log(newvalue,oldvalue)//如果走了下面的chageNum(),newvalue输出6,oldvalue=0
})
function chageNum(){
num.value =6
}
//watch函数返回一个函数,调用即可停止
let stop= watch(num,(newvalue,oldvalue){
if(num>6){
stop();//这样就停了
}
})
</script>
(二)举个栗子(监听ref对象类型数据):
<script lang="ts" setup>
import {ref,watch} from 'vue'
let car=ref({name:"赛600",price:52999})
watch(car,(newvalue,oldvalue){
console.log(newvalue,oldvalue);//如果改的对象下面的属性,new、old值都是一样的,因为地址没变
console.log(newvalue,oldvalue);//如果整个对象都改了,new就是新的,old就是原来的
},{deep:true})//deep是开启深度监视的意思,如果不开启深度监视,改对象里面属性的时候,不会触发监听
//immediate 页面开始直接执行一次,没想到应用场景?
function chageShuXing(){
car.value.price=99999 //此时new、old值都是一样的,因为地址没变,都是car对象
}
function changeDuiXiang(){
car.value={name:"赛600 rs",price:32999}//此时new就是新的,old就是原来的
}
</script>
(三)举个栗子(监听reactive对象类型数据):
<script lang="ts" setup>
import {reactive,watch} from 'vue'
let car=reactive({name:"赛600",price:52999})
watch(car,(newvalue,oldvalue){
console.log(newvalue,oldvalue);//调用下面changeshuxing,也会触发监听,reactive对象默认开启deep(深度监视),且无法关闭deep
console.log(newvalue,oldvalue);//
})
function changeShuXing(){
car.price=99999
}
function changeDuiXiang(){
car=Object.assign(car,{name:"赛600 rs",price:32999})//此时new就是新的,old就是原来的
}
</script>
(四)举个栗子(监听对象下的某一个属性):
<script lang="ts" setup>
import {reactive,watch} from 'vue'
let car=reactive({name:"赛600",price:52999})
watch(()=>car.name,(newvalue,oldvalue){//这里不能直接写对象点属性,要用函数式,即变成getter
console.log(newvalue,oldvalue);//只有car的name属性变化了,才会触发
})
let car=reactive({name:"赛600",price:5299,peijian:{fadongji:"4缸",huohuasai:"依金"}})
//监听对象下面的对象
watch(car.peijian,(){}) //这么写监听的是peijian的属性,fadongji、huohuasai变化都会触发监听
watch(()=>car.peijian,(){})//这么写监听的是peijian的地址,只有地址变化了,才会触发监听
watch(()=>car,peijian,(){},{deep:true})//这么写对象的属性值,和地址发生编码都会触发监听
</script>
(五)举个栗子(监听以上所有情况,即监听一个数组)【newvalue返回的也是一个数组】:
<script lang="ts" setup>
import {reactive,watch} from 'vue'
let car=reactive({name:"赛600",price:5299,peijian:{fadongji:"4缸",huohuasai:"依金"}})
//监听对象下面的对象
watch([()=>car.name,()=>car.peijian],(newvalue,oldvalue){
console.log(newvalue)//这里返回的也是一个数组
})
</script>
7.watchEffect监视
说明:不需要参数,直接写监听的回调方法,vue会自动判断方法里用到的响应式参数,只要变了就执行这个回调方法
注意:回调方法页面一打开就会执行一次
<script lang="ts" setup>
import {ref,watchEffect} from 'vue'
let a=ref(0)
watchEffect(()=>{//只要a发生变化了,都会执行这个方法
if(a>0){
console.log('你好,我执行了!')
}
})
</script>
8.标签的ref属性
说明:就是给标签写上ref,可以直接拿到标签
<template>
<h1 ref="title">你好</h1>
<button @click="getTitle">测试</button>
</template>
<script lang="ts" setup >
import {ref} from 'vue'
let title=ref()//这个不能写到方法里面,写到里面就拿不到了
function getTitle(){
console.log(title.value)//这里输出 <h1>你好</h1>
}
</script>
ref也可以直接写到组件上,可以实现父组件用子组件的变量,但是子组件必须使用defineExpose将允许访问的变量指定下,子组件如下:
<template>
<h1 ref="title">你好</h1>
<button @click="getTitle">测试</button>
</template>
<script lang="ts" setup >
import {ref,defineExpose} from 'vue'
let title=ref()//这个不能写到方法里面,写到里面就拿不到了
function getTitle(){
console.log(title.value)//这里输出 <h1>你好</h1>
}
let a=ref(0)
let b=ref(1)
let c=ref(2)
defineExpose({a,b,c})//defineExpose用于把这些字段交出去,别的组件就可以访问了
</script>
父组件如下:
<template>
<div class="app">
<Car ref="che"></Car>
<button @click="getChe">拿车</button>
</div>
</template>
<script lang="ts" setup name="App">
import {ref} from 'vue'
import Car from './components/Car.vue';
let che=ref()
function getChe(){
console.log(che.value.b)//这里可以直接输出1
}
</script>
9.ts中的接口、泛型、自定义类型
说明:接口用来限制对象的,可以使代码更规范
//这是一个ts文件
export interface PersonInterface{
name:string,
age:number,
id:string
}
export type Persons=Array<PersonInterface> //这就是自定义类型,泛型不用看,简单
<template>
你好,ts
</template>
<script lang="ts" setup >
import {type PersonInterface,type Persons} from '@/types'
let person:PersonInterface={
name:"1",
id:"1",
age:22
}
//由于person继承了PersonInterface接口,所以person对象必须包含接口的字段,即name、age、id
let personList:Persons=[{
name:"1",
id:"1",
age:22
}]
</script>
10.组件之间的值传递-props
如何把一个母组件的值传递给子组件
//这是母组件
<template>
<div class="app">
<Car :personList="pList" />// 直接在子组件的标签上写属性传值,“:”这个冒号代表运算符,不写就代表传一个值为“pList”的字符串了
</div>
</template>
<script lang="ts" setup name="App">
import {reactive, ref} from 'vue'
import Car from './components/Car.vue';
import {type Persons} from '@/types' //引入js定义的接口类型
//定义一个变量叫pList继承Person值为一个响应式的Persons对象数组
let pList:Persons=reactive([{
name:"张三",
age:12,
id:1
},{
name:"李四",
age:17,
id:2
}])
</script>
//这是子组件
<template>
<ul>
<li v-for="item in personList" :key="item.id">
{{item.name}}------{{item.id}}
</li>
</ul>
</template>
<script lang="ts" setup >
let dp_data=defineProps(['personList']);//defineProps用来接收母组件传过来的值
defineProps<{personList?:Persons}>();//也可以用泛型来限制接收的对象类型,"?"问号的意思代表允许父组件不传
withDefaults(defineProps<{personList:Persons}>(), {personList:()=>[{name:"张三",age:12,id:1}]})//这个就是给个默认值
console.log(dp_data);
</script>
11.vue的生命周期(先回顾vue2,再看vue3)
vu2的生命周期:组件从创建到销毁的全过程,包含 创建、挂载、更新、销毁,对应8个生命周期函数(钩子):beforeCreate()、created()、beforeMount()、mounted()、beforeUpdate()、updated()、beforeDestroy()、destroyed()
vue3的声明周期:组件从创建到销毁的全过程,包含 创建、挂载、更新、卸载,对应6个生命周期函数(钩子):onBeforeMount()、onMounted()、onBeforeUpdate()、onUpdated()、onBeforeUnMount()、onMounted()
备注:vue3没有创建的函数钩子了,直接写script就完事了,且销毁改成了卸载
12.自定义hooks
说明:hooks类似与后端的类,就是声明一个ts或者js文件,把一个对象的所有数据和方法放到一起,然后再从组件里导入;
hooks命名要用use开头,后面写对象的名,比如订单相关的hooks,就可以命名为 useOrder.ts
//比如这是 useOrder.ts 文件
//要用 exprot default 交去去,也就是写了这个组件里才能导入
export default function(){
import {ref} from 'vue'
let orderNum=ref(000001)
let customerName=ref('梁帅哥')
let invName=ref('赛600')
let money=ref(50000)
function chageMoney(){
money.value+=1000;
}
//这里要return一下,组件里才能访问到
return {customerName,money,changeMoney}
}
//这是组件,展示如何调用hooks里的内容
<template>
客户名称:{{customerName}},订单金额{{money}}
<button @click="changeMoeny">修改订单金额</button>
</template>
<script lang="ts" setup >
import useOrder from '@/hooks/useOrder'
//直接解构
const {customerName,money,changeMoney}=userOrder()
</script>
13.路由
说明:路由就是根据浏览器地址变化,访问对应的组件
1.创建路由器
在src文件夹下创建router文件夹,并创建文件index.ts,以下是index.ts的代码
import { createRouter, createWebHistory } from "vue-router";//引入路由相关的东西
import Home from "@/components/Home.vue";//引入页面组件
import About from "@/components/About.vue";//引入页面组件
import News from "@/components/News.vue";//引入页面组件
//router是路由器
const router= createRouter({
history:createWebHistory(),//指定路由模式,分为history和hash模式,hash的路径带#,history不带更美观
routes:[{
name:"home",//路由的名字
path:"/Home",//路径
component:Home//对应的组件
},{
name:"about",
path:"/About",
component:About
},{
name:"news",
path:"/News",
component:News
}]
})
export default router //将路由器抛出去
2.在母组件创建的时候指定路由器
以下是main.ts的代码
import { createApp } from "vue";
import App from './App.vue'
import router from "./router";//引入路由器
const app= createApp(App)
app.use(router);//使用路由器
app.mount("#app");
3.使用路由
以下是App.vue的代码
<template>
<div class="app" >
<h1>路由学习</h1>
</div>
<div class="nav">
<RouterLink replace to="/Home" activeClass="active">首页</RouterLink> //RouterLink路由链接,这是to的第一种写法
<RouterLink :to="{name:'news'}" activeClass="active">新闻</RouterLink>//直接用name这样路由的地址改了,这就不用改了
<RouterLink :to="{path:'/news'}" activeClass="active">关于</RouterLink>//这是直接指定路径,不推荐使用
</div>
<div>
<RouterView></RouterView>//路由组件展示的区域
</div>
</template>
<script lang="ts" setup name="App">
import { RouterView } from 'vue-router';
</script>
<style>
.app{
color: rgb(25, 126, 203);
box-shadow: 0 0 10px;
border-radius: 10px;
padding: 20px;
background-color: burlywood;
font-size: 50px;
}
.nav{
display: flex;
margin-left: 10px;
justify-content: space-around;
}
.nav a{
width: 90px;
height: 40px;
background-color: aqua;
}
.active{
background-color: aquamarine;
}
</style>
注意:
1.路由的组件一般放在page或者view的文件里
2.路由的切换,会将上一个组件销毁掉
3.如果再routerLink标签里写了replace 属性,那么路由变换后就不允许回退了(浏览器默认是push属性,即访问一个路由网记录里加一个记录,来实现回退,而replace属性代表不加记录,直接替换掉,所以不允许回退了)
14.路由的值传递
路由的值传递有两种方案,一个是url传参(query),一个是params
以下是query传参样例:
<!--以下路由器代码-->
import { createRouter, createWebHistory } from "vue-router";
import Home from "@/views/Home.vue";
import About from "@/views/About.vue";
import News from "@/views/News.vue";
import Detail from "@/views/Detail.vue";
const router= createRouter({
history:createWebHistory(),
routes:[{
name:"home",
path:"/Home",
component:Home
},{
name:"about",
path:"/About",
component:About
},{
name:"news",
path:"/News",
component:News,
children:[{
name:"details",
path:"details/:id/:title/:content",
component:Detail
}]
}]
})
export default router
<!--以下是母组件代码:-->
<template>
<div class="fatherBox">
<div class="title">
<ul>
<!-- <RouterLink v-for="item in newList" :key="item.id" :to="`/News/details?id=${item.id}&title=${item.title}&content=${item.content}`">{{item.content}}</RouterLink> -->
<RouterLink v-for="item in newList" :key="item.id" :to="{name:'details',query:{id:item.id,title:item.title,content:item.content}}">{{item.content}}</RouterLink>
</ul>
</div>
<div class="mainContent">
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup >
import { reactive } from 'vue';
let newList=reactive([{
title:"标题1",
content:"新闻内容1",
id:1
},{
title:"标题2",
content:"新闻内容2",
id:2
},{
title:"标题3",
content:"新闻内容3",
id:3
},{
title:"标题4",
content:"新闻内容4",
id:4
}])
</script>
<!--以下是子组件代码:-->
<template>
<div>
id:{{query.id}}
标题:{{query.title}}
内容:{{query.content}}
</div>
</template>
<script lang="ts" setup>
import { useRoute } from 'vue-router';//这是一个hooks
import { toRefs } from 'vue';
let routeData=useRoute();//这里是一个路由的对象,里面包含了query对象的参数
let {query}=toRefs(routeData);//解构,并用toRefs重新弄成响应式的对象
</script>
以下是params传参样例:
<!--以下路由器代码-->
import { createRouter, createWebHistory } from "vue-router";
import Home from "@/views/Home.vue";
import About from "@/views/About.vue";
import News from "@/views/News.vue";
import Detail from "@/views/Detail.vue";
const router= createRouter({
history:createWebHistory(),
routes:[{
name:"home",
path:"/Home",
component:Home
},{
name:"about",
path:"/About",
component:About
},{
name:"news",
path:"/News",
component:News,
children:[{
name:"details",
path:"details",
component:Detail,
props:true//如果这个设置成true,等于渲染组件标签的时候会把参数当属性,比如<Detail a='' b=''>,这样子组件可以直接用defineProps接收参数,就可以直接用了,比较优雅
}]
}]
})
export default router
<!--以下是子组件代码:-->
<template>
<div>
id:{{params.id}}
标题:{{params.title}}
内容:{{params.content}}
</div>
</template>
<script lang="ts" setup>
import { useRoute } from 'vue-router';
import { toRefs } from 'vue';
let {params}=toRefs(useRoute());
//以上是没在路由器里指定props为true的写法,如果props为true,可以用以下的写法
defineProps(['id','title','content']) //这样上面的插值标签不用写params.id了,可以直接写id了
</script>
<!--以下是母组件代码:-->
<template>
<div class="fatherBox">
<div class="title">
<ul>
<!-- <RouterLink v-for="item in newList" :key="item.id" :to="`/News/details?id=${item.id}&title=${item.title}&content=${item.content}`">{{item.content}}</RouterLink> -->
<RouterLink v-for="item in newList" :key="item.id"
:to="{name:'details',params:{id:item.id,title:item.title,content:item.content}}">{{item.content}}</RouterLink>
</ul>
</div>
<div class="mainContent">
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup >
import { reactive } from 'vue';
let newList=reactive([{
title:"标题1",
content:"新闻内容1",
id:1
},{
title:"标题2",
content:"新闻内容2",
id:2
},{
title:"标题3",
content:"新闻内容3",
id:3
},{
title:"标题4",
content:"新闻内容4",
id:4
}])
</script>
15.路由的跳转
通过代码跳转路由,而不是通过RoterLink组件
<template>
<div class="fatherBox">
<div class="title">
<ul>
<RouterLink v-for="item in newList" :key="item.id" :to="{ name:'details'}">
<button @click="showNew(item)">查看新闻</button>{{item.content}}
</RouterLink>
</ul>
</div>
<div class="mainContent">
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup >
import { reactive } from 'vue';
import { useRouter } from 'vue-router';
let newList=reactive([{
title:"标题1",
content:"新闻内容1",
id:1
},{
title:"标题2",
content:"新闻内容2",
id:2
},{
title:"标题3",
content:"新闻内容3",
id:3
},{
title:"标题4",
content:"新闻内容4",
id:4
}])
const router=useRouter(); //这里调用获取路由器的方法(路由自带的hooks),拿到路由
interface NewInter{
id:number,
content:string,
title:string
}
function showNew(item:NewInter){//任意类型的数据,ts报错(方法不能直接传参数),所以要拿接口规范以下对象的字段和类型
router.replace({ //直接拿路由器push或者replace一个对象就可以里,这个对象的字段和to字段一样,比如都有name和params等
name:"details",
params:{
id:item.id,
title:item.title,
content:item.content
}
});
}
</script>
16.路由重定向
比如一打开网页就指定到某个路由,就可以用到路由的重定向
以下是路由器配置的的代码
import { createRouter, createWebHistory } from "vue-router";
import Home from "@/views/Home.vue";
const router= createRouter({
history:createWebHistory(),
routes:[{
name:"home",
path:"/Home",
component:Home
},{
path:"/",
redirect:"/Home" //这个代码就是当路由等于"/"的时候重定向到"/Home",也就等于一打开代码就跳转到/Home了
}
]
})
export default router
四、Pinia
1.介绍
Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。
官网地址:Pinia官网
2.安装
npm i pinia
在main.ts里创建和使用,代码如下:
import { createApp } from "vue";
import App from './App.vue'
import router from "./router";
import { createPinia } from "pinia";
const app= createApp(App)
const pinia=createPinia()
app.use(router);
app.use(pinia);
app.mount("#app");
3.pinia的使用
先在src下创建一个名叫strore的文件夹(在这里面创建所有pinia相关的hooks),并创建自定义存储相关的东西
import { defineStore } from "pinia";
export const useCountStore=defineStore('count',{
actions:{
add(value:any){
this.sum+=value;
}//如果数据的修改逻辑一致,可以在这里写数据的修改方法,方法直接复用
},
state(){ //这里是真正的用来存储数据的地方
return {
sum:1
}
},
getters:{//这里可以直接对数据加工,可以直接解构拿到bigSum直接用
bigSum():number{
return this.sum*10;
}
}
}
)
自定义存储的使用,及数据修改的3种方法
<template>
<div class="mainContentBox">
<h1>
当前求和为:{{countStore.sum}}
</h1>
<select v-model.number="n">
<option>1</option>
<option>2</option>
<option>3</option>
</select>
<button @click="add">加</button>
<button @click="jian">减</button>
</div>
</template>
<script lang="ts" setup >
import { ref } from 'vue';
import { useCountStore } from '@/store/count'; //引入刚才创建的useCountStore
const countStore=useCountStore();//使用useCountStore ,得到一个专门保存count相关的store
const {sum}=countStore //如果要解构赋值,sum就不是响应式对象了
const {sum}=toRefs(countStore)//所以要使用toRefs将数据变成响应式的,但直接给stroe toRefs了,stroe里所有的东西都会变成ref的,不优雅
const {sum}=storeToRefs(countStore)//所以要这么写,这是pinia提供的api,这个只会给store里的state里的变成ref,优雅
//store的数据修改
//1.直接修改
countStore.sum=2;
//2.使用$patch,如果要同时修改多个字段,建议用这个,性能好些吧
countStore.$patch({
sum:2
})
//3.调用store里的actions修改数据,就是用store里自己写的方法,一般数据的修改逻辑是一样的建议用这种,提高代码的复用
countStore.add(n);//这里的add是store里actions里自己写的方法
let n=ref(1)
function add(){
countStore.sum=countStore.sum+n.value;
}
function jian(){
countStore.sum=countStore.sum-n.value;
}
</script>
4.订阅
import { useCountStore } from '@/store/count';
const countStore=useCountStore();
countStore.$subscribe((mutate,state)=>{ //订阅,会有两个入参,mutate没啥用,state是store里的数据
//数据一变就会进这个方法,想干什么就干什么,比如存个token什么的
localStorage.setItem('token',state.sum.toString())
})
5.store的组合式写法
import { defineStore } from "pinia";
import { ref } from "vue";
import { computed } from "vue";
//以下是选项式的,用来和组合式的比较参考
export const useCountStore=defineStore('count',{
actions:{
add(value:any){
this.sum+=value;
}//如果数据的修改逻辑一致,可以在这里写数据的修改方法,方法直接直接复用
},
state(){
return {
sum:1
}
},
getters:{
bigSum():number{
return this.sum*10;
}
}
}
)
//下面式组合式的
export const useCountStore2=defineStore('count',()=>{
let sum =ref(1);
function add(value:any){
sum.value+=value;
}
let bigSum = computed({
get(){
return sum.value*10
},
set(){}
})
return {sum,add,bigSum}
}
)
五、组件间的值传递
1.props
以下是父组件给子组件传递的例子
//这是父组件
<template>
<div class="content">
<Son :car="car" ></Son>
</div>
</template>
<script lang="ts" setup name="props">
import Son from './Son.vue';
import { ref } from 'vue';
let car=ref({carName:"xiaomi",std:"su7 max"})
</script>
//这是子组件
<template>
<div class="content_son">
你好,我是子组件,父亲给我了一辆{{car.carName}}{{car.std}}
</div>
</template>
<script lang="ts" setup name="props_son">
import { defineProps } from 'vue';
let {car}=defineProps(['car']);
</script>
以下是子组件给父组件传递的例子。
用props方式子给父传值比较麻烦,需要父组件申明一个方法,传给子组件,子组件通过方法把值给父组件
//这是父组件
<template>
<div class="content">
<Son :getData="getData" ></Son>
</div>
</template>
<script lang="ts" setup name="props">
import Son from './Son.vue';
import { ref } from 'vue';
let sonData=ref()
function getData(value:any){
sonData.value=value;//这里可以拿到子组件传过来的值
}
</script>
//这是子组件
<template>
<div class="content_son">
你好,我是子组件,父亲给我了一辆{{car.carName}}{{car.std}}
<button @click="getData(toy)">通过方法,给父亲传值</button>
</div>
</template>
<script lang="ts" setup name="props_son">
import { defineProps } from 'vue';
import { ref } from 'vue';
let {getData}=defineProps(['getData']);
let toy=ref("给父组件的值")
</script>
2.自定义事件传值
一般主要用于子组件给父组件传值
<template>
<div class="content">
你好,我是自定义事件的父组件
<Son @send-data="saveData"></Son> //@后面写的就是自定义事件的名字,svaedata是触发自定义事件执行的方法
</div>
</template>
<script lang="ts" setup name="evnet">
import Son from './Son.vue';
function saveData(value:number){
alert(value)
}
</script>
<template>
<div class="content_son">
你好,我是自定义事件的子组件
<button @click="emit('send-data',6)">点我给父传值</button>//这是自定义事件的触发方法,emit是接到的父组件事件,第一个参数是事件的名字,后面跟的是传递给事件的参数;调用形式不至于用click,可以在任意地方写上emit(),都可以触发
</div>
</template>
<script lang="ts" setup name="evnet">
import { defineEmits } from 'vue';
let emit=defineEmits(['send-data'])//用defineEmits接收父组件自定义的事件
</script>
3.使用mitt传值
说明:mitt就相当于一个中介,传递数据的组件告诉mitt要触发哪个事件和传递什么数据,mitt通知所有订阅该事件的组件并传递数据
使用步骤:
1.安装mitt( npm i mitt)
2.建立tools文件夹,创建emitter.ts
3.在emitter.ts里调用mitt,并暴露出去
4.在需要接收数据的组件里,订阅事件
5.在需要传递数据的组件里,触发事件
6.在订阅事件的组件里用卸载后事件里解绑事件,以免组件被销毁了还占内存
//这是tools文件夹里的emitter.ts
//引入mitt
import mitt from "mitt";
//调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter=mitt();
//暴露emitter
export default emitter;
//这是传递数据的组件
<template>
<div class="content_son">
你好,我是mitt样例的子组件1<br/>
我有一个玩具:{{toy}}<br/>
<button @click="emitter.emit('sent-toy',toy)">点我把玩具传给另一个组件</button>
</div>
</template>
<script lang="ts" setup name="mittSon1">
import { ref } from 'vue';
import emitter from '@/tools/emitter';//引入在tools文件夹里创建的emitter
let toy=ref('奥特曼');
</script>
//这是接收数据的组件
<template>
<div class="content_son">
你好,我是mitt样例的子组件2<br/>
我有一个电脑{{computer}}<br/>
son1组件给我了传了一个{{toy}}
</div>
</template>
<script lang="ts" setup name="mittSon2">
import { ref,onUnmounted } from 'vue';
import emitter from '@/tools/emitter';//引入在tools文件夹里创建的emitter
let computer=ref('redmi book');
let toy=ref('')
emitter.on('sent-toy',(value:any)=>{
toy.value=value;
})
//卸载后事件里解绑订阅,以免占内存
onUnmounted(()=>{
emitter.off('sent-toy');
})
</script>
4.v-model实现原理
用v-model原来里实现父子传值的用法开发中很少用到,这里学习下v-model的原理,可以装逼,或者封装组件也能用上
<template>
<div class="content">
你好,我是v-model的父组件
<input v-model="num" />
<input :value="num" @input="num = (<HTMLInputElement>$event.target).value" />//这一行就等于上面那一行,
//通过:value和input事件实现双向绑定
<Son v-model="num"></Son>//v-model用到组件上
<Son v-model:shuzi="num"></Son>//这是v-model取别名的手法,子表接收的时候需要按照别名接收
<Son :modelValue="num" @update:modelVlue="num=$event" />//这一行等于上面那一行
//通过modelValue是update:modelValue事件实现双向绑定
</div>
</template>
<script lang="ts" setup name="v-model_son">
import Son from './Son.vue';
import {ref} from 'vue'
let num=ref('200')
function showNum(){
alert(num.value)
}
</script>
//如果父组件用v-model传值,子组件以下这么写才能实现双向绑定
<template>
<div class="content_son">
你好,我是v-model的子组件
<input :value="modelValue" @input="emit('update:modelValue',(<HTMLInputElement>$event.target).value)" />
</div>
</template>
<script lang="ts" setup name="v-model_son">
import { defineEmits } from 'vue';
defineProps(['modelValue','shuzi']);//这里的“shuzi”是父组件取的别名
const emit=defineEmits(['update:modelValue','update:shuzi'])//这里的“shuzi”是父组件取的别名
</script>
5.$attrs
学习$attrs前先了解以下知识点:
1.父亲用属性传值传了数据,但是子组件没有使用defineProps接收,那么剩余的数据就会存储到$attrs里
2.$attrs多用于父组件和孙组件传数据
3.v-bind=“{a:1}” 等于 :a=“1”
<template>
<div class="content">
你好,我是attrs的父组件
a:{{a}},b:{{b}},c:{{c}}
<Son v-bind="{a:1}" :b="b" :c="c" :updateA="updateA"></Son>
</div>
</template>
<script lang="ts" setup name="attrs">
import Son from './Son.vue';
import {ref} from 'vue';
let a=ref(0);
let b=ref(1);
let c=ref(2);
function updateA(value:number){
a.value+=value;
console.log(a.value)
}
</script>
<style scoped>
</style>
<template>
<div class="content_son">
你好,我是attrs的子组件
a:{{a}},b:{{b}}
<sunzi v-bind="$attrs"></sunzi>
</div>
</template>
<script lang="ts" setup name="attrs_son">
import sunzi from './Sunzi.vue'
import { defineProps } from 'vue';
defineProps(['a','b'])
</script>
<style scoped>
</style>
<template>
<div class="content_sunzi">
你好,我是attrs的孙组件
c:{{c}}
<button @click="updateA(50)">点我修改爷爷的a的值</button>
</div>
</template>
<script lang="ts" setup name="attrs_sunzi">
import { defineProps } from 'vue';
defineProps(['c','updateA'])
</script>
<style scoped>
</style>
6.$refs和$parents
说明:给子组件用ref写上名字过后,用$refs可以直接拿到所有子组件暴露出的数据,同理子组件里可以直接使用$parent拿到父亲暴露的数据
<template>
<div class="content">
你好,我是$refs、$parent传值的父组件
房产:{{house}}
<button @click="chageBooks($refs)">点我修改子组件的书籍数量</button>
<Son1 ref="son1"></Son1>
<Son2 ref="son2"></Son2>
</div>
</template>
<script lang="ts" setup name="refsAndParent">
import Son1 from './Son1.vue';
import Son2 from './Son2.vue';
import { ref } from 'vue';
let house=ref(6)
function chageBooks(sons:any){
for(let key in sons){
sons[key].book+=1;
}
}
defineExpose({house})
</script>
<style scoped>
.content {
padding: 20px;
width: 1500px;
height: 600px;
background-color: skyblue;
margin-left: 20px;
border-radius: 10px;
}
</style>
<template>
<div class="content_son">
你好,我是$refs、$parent传值的子组件1
玩具:{{ toy }}
书籍:{{ book }}
<button @click="getHouse($parent)">点我从父亲那搞个房子</button>
</div>
</template>
<script lang="ts" setup name="refsAndParent_son1">
import { ref } from 'vue';
let toy = ref('奥特曼')
let book = ref(6)
function getHouse(value: any) {
value.house-=1;
}
defineExpose({ toy, book })
</script>
<style scoped>
.content_son {
padding: 20px;
width: 1400px;
height: 160px;
background-color: pink;
margin-left: 20px;
margin-top: 20px;
border-radius: 10px;
}
input {
border-image: auto;
background-color: aqua;
}
</style>
<template>
<div class="content_son">
你好,我是$refs、$parent传值的子组件2
电脑:{{computer}}
书籍:{{book}}
</div>
</template>
<script lang="ts" setup name="refsAndParent_son2">
import { ref } from 'vue';
let computer=ref('小米')
let book=ref(6)
defineExpose({computer,book})
</script>
<style scoped>
.content_son {
padding: 20px;
width: 1400px;
height: 160px;
background-color: pink;
margin-left: 20px;
margin-top: 20px;
border-radius: 10px;
}
input {
border-image: auto;
background-color: aqua;
}
</style>
7.provide、inject
说明:
provide 写在父组件,用于传递数据,要传递两个参数,分别对应key和value
inject写在子组件用于接收数据
备注:inject接收对象的时候必须写一个默认值,不然飘红
<template>
<div class="content">
你好,我是provide、Inject传值的父组件
房产:{{house}}
存款:{{money}}
<Son ></Son>
</div>
</template>
<script lang="ts" setup name="provideAndInject">
import Son from './Son1.vue';
import { provide, reactive, ref } from 'vue';
let house=ref(2);
let car=reactive({
pinpai:"xiaomi su7 max",
price:300000
})
let money=ref(300000);
function updateMoney(value:number){
money.value+=value;
}
//通过provide给子组件传值,要传两个参数,分别对应key和value
provide('house',house)
provide('car',car)
provide('moenyContext',{money,updateMoney})
</script>
<style scoped>
.content {
padding: 20px;
width: 1500px;
height: 600px;
background-color: skyblue;
margin-left: 20px;
border-radius: 10px;
}
</style>
<template>
<div class="content_son">
你好,我是provide、Inject传值的子组件
<br/>
父亲的房子:{{ house }}
父亲的车:{{car.pinpai}},价格:{{car.price}}
父亲的存款:{{money}}
<button @click="updateMoney(222)">点我修改父亲的存款</button>
</div>
</template>
<script lang="ts" setup name="provideAndInject_son">
import { inject, ref } from 'vue';
//通过key直接获取父组件的数据
let house=inject('house')
//通过key获取父组件的对象数据,但是必须要在inject的第二个参数协商默认值,否则飘红
let car =inject('car',{pinpai:'待定',price:0})
//解构获取父组件的数据和方法
let {money,updateMoney}=inject('moenyContext',{money:0,updateMoney:(value:number)=>{}});
</script>
<style scoped>
.content_son {
padding: 20px;
width: 1400px;
height: 160px;
background-color: pink;
margin-left: 20px;
margin-top: 20px;
border-radius: 10px;
}
input {
border-image: auto;
background-color: aqua;
}
</style>
8.pinia
直接看第四章,有详细说明
9.插槽slot
说明:插槽其实就是占位的意思,在组件里用slot标签占位,父组件调用组件的时候,可以在组件标签里写上其他html,这样子组件渲染的时候,就会把html替换到slot标签里
9.1 默认插槽
以下案例展示了同一个组件,分别展示列表、图片、视频的案例
<template>
<div class="content">
<MsgBox title="游戏列表">//这是一个子组件
<ul>//这里的ul标签会渲染到子组件的slot里
<li v-for="item in games" >{{item}}</li>
</ul>
</MsgBox>
<MsgBox title="图片展示">
<img :src="tupian">
</MsgBox>
<MsgBox title="视频展示">
</MsgBox>
</div>
</template>
<script lang="ts" setup name="provideAndInject">
import MsgBox from './MsgBox.vue';
import { provide, reactive, ref } from 'vue';
let games=reactive(['地下城与勇士','英雄联盟','和平经营','王者荣耀'])
let tupian=ref('http://img.sharpnet.vip/demoList/0001.png')
let video=ref('xxxx.mp4')
</script>
<style scoped>
.content {
padding: 20px;
width: 1500px;
height: 600px;
background-color: skyblue;
margin-left: 20px;
border-radius: 10px;
display: flex;
justify-content: space-evenly;
}
img{
width: 100%;
}
</style>
<template>
<div class="content_son">
<h2>{{title}}</h2>
<slot>内容加载中...</slot>
</div>
</template>
<script lang="ts" setup name="provideAndInject_son">
import { ref } from 'vue';
defineProps(['title'])
</script>
<style scoped>
.content_son {
padding: 20px;
width: 400px;
height: 400px;
background-color: pink;
margin-left: 20px;
margin-top: 20px;
border-radius: 10px;
}
h2{
background-color: orange;
text-align: center;
}
</style>
9.2 具名插槽
说明:就是给插槽取名字,根据名字渲染到不同的slot标签里
<MsgBox title="游戏列表">//调用子组件
<template v-slot:title> //这里可以直接简写成 #title
<h2>游戏列表</h2>
</template>
<template v-slot:content>//这里可以直接简写成 #content
<ul>
<li v-for="item in games">{{ item }}</li>
</ul>
</template>
</MsgBox>
<template>
<div class="content_son">
<slot name="title">内容加载中...</slot>
<slot name="content">内容加载中...</slot>
</div>
</template>
9.3 作用域插槽
说明:数据在子组件,但slot标签要替换的内容在父组件,这种情况要用到作用域插槽
v-solt:xxx 是具名插槽,xxx是名字
v-solt="xxx"是作用域插槽,xxx是数据
v-solt:xxx="data"这是往名字叫xxx的插槽里插数据,并使用他的data数据,可以简写为 #xxx=“data”
<template> //这是父组件
<div class="content">
<MsgBox >
<template v-slot:title>
<h2>游戏列表</h2>
</template>
<template v-slot:content="{gameData}">
<ul>
<li v-for="item in gameData">{{ item }}</li>
</ul>
</template>
</MsgBox>
<MsgBox >
<template v-slot:title>
<h2>有序游戏列表</h2>
</template>
<template #content="{gameData}">
<ol>
<li v-for="item in gameData">{{ item }}</li>
</ol>
</template>
</MsgBox>
</div>
</template>
<script lang="ts" setup name="provideAndInject">
import MsgBox from './MsgBox.vue';
</script>
六、其他API
1.shallowRef与shallowReactive
说明:就是用shallowRef或shallowReactive声明的变量只有第一层是响应式的,提升性能用的
官方说明:和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。
shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。
const state = shallowRef({ count: 1 })
// 不会触发更改
state.value.count = 2
// 会触发更改
state.value = { count: 2 }
2.readonly与shallowReadonly
说明:readonly是让整个对象变成只读的,shallowReadonly只让对象的顶层是只读的
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// 更改状态自身的属性会失败
state.foo++
// ...但可以更改下层嵌套对象
isReadonly(state.nested) // false
// 这是可以通过的
state.nested.bar++
3.toRaw和markRaw
toRaw:让一个响应式对象变成普通对象
官方说明:toRaw() 可以返回由 reactive()、readonly()、shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。
这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
markRaw:让一个对象永远不会是响应式
官方说明:将一个对象标记为不可被转为代理。返回该对象本身。
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
4.customRef
说明:自定义ref数据,就是数据发生变化的时候,可以拦截操作一下再返回
官方说明:创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象。
一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。
写一个hooks,实现用这个hooks定义的响应式数据,在接收到值后延迟1s再更新,并支持防抖
import { customRef } from "vue";
export default function useMsgRef(initMsg:string,time:number){
let timer:number
let msg = customRef((track, trigger) => {
return {
get() {
track();//追踪一下initMsg有没有发生更新
return initMsg;
},
set(value) {
clearTimeout(timer)
timer= setTimeout(() => {
initMsg=value;
trigger();//告诉vue initMsg发生了更新
}, time);
return initMsg;
}
}
})
return msg
}
在组件中使用
<template>
<div class="content">
<h2>{{ msg }}</h2>
<input v-model="msg" />
</div>
</template>
<script lang="ts" setup name="customRef">
import { ref, customRef } from 'vue'
import useMsgRef from './useMsgRef'
//let msg = ref('你好')
let msg=useMsgRef('你好',1000);
</script>
5.Teleport
说明:Teleport可以将包裹起来的标签放到指定位置
官方说明:Teleport 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。
https://cn.vuejs.org/guide/built-ins/teleport.html
有时我们可能会遇到这样的场景:一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。
这类场景最常见的例子就是全屏的模态框。理想情况下,我们希望触发模态框的按钮和模态框本身是在同一个组件中,因为它们都与组件的开关状态有关。但这意味着该模态框将与按钮一起渲染在应用 DOM 结构里很深的地方。这会导致该模态框的 CSS 布局代码很难写。
6.Suspense
说明:如果在子组件的setup中有异步的请求,则父组件可以使用Suspense标签来控制加载内容和具体展示的内容
<Suspense>
<template v-slot:default>//异步操作返回结果了,展示这个插槽的内容
<child/>
</template>
<template v-slot:fallback>//当异步操作没返回的时候,展示这个插槽的内容
加载中...
</template>
</Suspense>
官方说明:
Suspense 组件有两个插槽:#default 和 #fallback。两个插槽都只允许一个直接子节点。在可能的时候都将显示默认插槽中的节点。否则将显示后备插槽中的节点。