vue3学习笔记
原文:尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程_哔哩哔哩_bilibili
1.环境安装
nodejs 验证命令node,有node 才有npm这个命令
npm 是 JavaScript 世界的包管理工具,并且是 Node.js 平台的默认包管理工具。通过 npm 可以安装、共享、分发代码,管理项目依赖关系。
npm 由三个独立的部分组成:
- 网站
- 注册表(registry)
- 命令行工具 (CLI)
- 01 - npm 是什么? | npm中文文档 | npm中文网
>npm create vue@latest
只是使用typescript
通过vscode打开目录
注意起重一定要有package.json文件,如果没有就另建项目重新npm create vue@latest
工程介绍
1).vscode-->extensions.json
.vscode存放从商店中拿到的插件
比如
Vue - Official(代替了volar)
2)env.d.ts
从node modules引入
如果没有则飘红
npm i
报错npm WARN saveError ENOENT: no such file or directory, open 'D:\src\ebook\package.json'
原因是没有package.json,需要重新运行项目或者考过来pacakge.json
之后会出现node_modules文件夹
vite网址Vite中文网
3)index.html
入口文件
入口文件不是main.js也不是main.ts
vite.config.ts:工程配置文件
3)main.ts
import './assets/main.css' //引入css文件
import { createApp } from 'vue' //类似于养花的花盆,创建应用
import App from './App.vue' //类似于养花的花根
createApp(App).mount('#app'):
createApp(App).把花插在花盆里 创建应用,每个应用都有个根组件
mount: 成果摆在#app里面
4)删掉src
vue文件三个标签
template script style
推荐使用
2.setup
新配置项,里面不能用this,this是undefined,vue3弱化this
setup比beforecreate还在执行前面
返回对象
setup(){ // console.log("@@@@@@@@@@@@@@"+this) this是undefined // 数据,直接用let不是响应式的,也就是变化不会引发页面自动更新 let name="杜甫"; let age=21; let tel='123455' // 方法 function changeName(){ console.log(1) name='李白' console.log(name) } function changeAge(){ console.log(2) age+=1; console.log(age) } function showTel(){ console.log(3) alert(tel) } return{ name,age,changeAge,changeName,showTel } }上述返回的是对象
返回函数
return function(){ return 'haha' }页面上直接出现
因为绝不会有this,改造成箭头函数
return ()=>{ return 'haha2' }箭头函数在语法上比普通函数简洁多。箭头函数就是采用箭头=>来定义函数,省去关键字function。
函数的参数放在=>前面的括号中,函数体跟在=>后的花括号中
①如果箭头函数没有参数,写空括号
②如果箭头函数有一个参数,也可以省去包裹参数的括号
③如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中。
箭头函数的函数体
①如果箭头函数的函数体只有一句执行代码,简单返回某个变量或者返回一个简单的js表达式,可以省去函数体花括号{ }及return
箭头函数没有原型prototype,因此箭头函数没有this指向
箭头函数没有自己的this指向,它会捕获自己定义所处的外层执行环境,并且继承这个this值。箭头函数的this指向在被定义的时候就确定了,之后永远都不会改变。(!永远)
箭头函数进一步
return ()=>"haha3"
语法糖
可以不写setup 及里面的return
let a='666'
let a='666'
一般vue3都有两个setup
一个用来定义名字,不然就等同文件名
另外一个写数据和方法
配置插件
为了支持把以上两个setup写在一起:
let a='666'
npm i vite-plugin-vue-setup-extend -D
更改vite.config.ts
之后重启就使用了
从输出来看 右侧数据被单独放在一块,其余放在一块
响应式数据
不同于vue2用data(){return{}} 里面的数据自动是响应式
ref:基本类型数据
引入ref,在需要变成响应式的前面加ref
import {ref} from 'vue' let name=ref("libai"); let a='666'; console.log(name); console.log(a); function change(){ }从输出可以看到
打开,带下划线的都是系统自身用的
可见name是一个对象,使用时应该用name.value获取其值,但是
注意如下2个位置不同:template中不用写.value,自动添加
reactive:只能对象类型数据
let car=reactive({brand:'Benz',price:100})
没包裹叫原对象
包裹之后变为响应式对象
ref2:对象类型数据
{{ car.brand }}{{ car.city }}- {{item.id }} {{item.name }}
reactive和Ref
RefImpl都是由ref定义得来
Proxy都是由reactive得来
Ref遇到对象时用Reactive
避免ref后面.value每次都要写的设置
左下角齿轮-settings
选中 AutoInsertDotvalue
reactive所指元素被重新分配对象
失去响应式
以下function不能更改页面
let car=reactive({brand:'aodi',city:'shanghai'})
function changeCar(){
car={brand:'保时捷',city:'tianjin'}
}
但是以下是可以的
function changeCar(){
car.brand='a'
car.csity='gz'
}
解决方法1
to refs结构对象
左边相当于右面
修改为 let {name,age}=toRefs(person)
把reactive对象所定义的每一组key value变成ref对象
并且name就是person.name ,改变name同时也改变了person.name
toRef:只改变某个元素
let nl=toRef(person,'age')
console.log("nl"+nl.value)
3.计算属性
{ fullName }} { fullName2() }} 全名 {{ fullName2() }} -- import {ref,computed} from 'vue' let fName=ref('zhang') let lName=ref("big") // 下面定义的fullName只读,不可修改 // let fullName=computed(()=>{ // // slice(0,1)截取从0开始到1的1个数据 ,slice(1) 截取从1到最后的所有数据 // return (fName.value.slice(0,1).toUpperCase()+fName.value.slice(1)+lName.value) // }) // 下面定义的fullName 可读写 let fullName=computed( { get(){ return (fName.value.slice(0,1).toUpperCase()+fName.value.slice(1)+"-"+lName.value)}, set(val){ const [str1,str2]=val.split('-') fName.value=str1 lName.value=str2 } } ) console.log(fullName) function fullName2(){ console.log("function2") return (fName.value.slice(0,1).toUpperCase()+fName.value.slice(1)+lName.value) } function changeFullName(){ fullName.value="li-si" } .person{ background-color: cadetblue; }
计算属性是有缓存的,发现其构成没变,则即使其它地方使用也不会再计算
方法则没有缓存 {{方法名()}}
如上定义计算属性是只读的
要想该则需要设置get set
set(val) 中的val就是fullname="li-si"中被赋予的值
let fullName=computed( { get(){ return (fName.value.slice(0,1).toUpperCase()+fName.value.slice(1)+"-"+lName.value)}, set(val){ const [str1,str2]=val.split('-') fName.value=str1 lName.value=str2 } } )4.watch
watch在vue3是一个函数
watch(监视谁,回调函数)
监视基本类型
注意:监视sum不需要写.value
let sum=ref(0); watch(sum,(newVal,oldVal)=>{ console.log("sum变化了"+newVal+"旧值"+oldVal) })结束监视
监视函数的返回值就是结束监视函数
import {ref,computed,watch} from 'vue' let sum=ref(0); function addOne(){ sum.value++ } const stopWatch=watch(sum,(newVal,oldVal)=>{ console.log("sum变化了"+newVal+"旧值"+oldVal) if(newVal>10){ stopWatch(); } }) console.log(stopWatch)stopWatch就是
监视Ref定义的对象类型数据
监视对象地址值
若需监视对象值,需开启深度监视,使用监视函数第三个值
watch(person,(newVal,oldVal)=>{ console.log("Person被修改了"+newVal+"旧值"+oldVal) },{deep:true})还可以加个immediate参数,一上来先执行,因为原来肯定是undefined
watch(person,(newVal,oldVal)=>{ console.log("Person被修改了"+newVal+"旧值"+oldVal) },{deep:true,immediate:true})另外(newVal,oldVal)如果里面只写一个参数代表的是新值
监视Reatctive定义对象
默认开启深度监视,不能关闭
ref替换对象才是真正的替换,地址也变了,并且一直保持响应式
如果用reactive person={},会失去响应式;object.assign:是修改人的属性而已
监视对象类型的某个属性
对象属性是基本类型
姓名{{ person.name }}
年龄{{ person.age }}
车{{ person.car.c1 }},{{person.car.c2 }}
修改名字 修改年龄 修改第一台车 修改第二台车 修改车 import {reactive,watch} from 'vue' let person=reactive({ name:"白居易", age:18, car:{ c1:'奔驰', c2:'宝马' } }) function changeName(){ person.name="素食" } function changeAge(){ person.age++ } function changeFirstCar(){ person.car.c1="保时捷" } function changeSecondCar(){ person.car.c2="法拉利" } function changeCar(){ // Object.assign(person.car,{c1:'蓝宇',c2:'比亚迪'}) person.car={c1:'蓝宇',c2:'比亚迪'} } watch(()=>{ return person.name },(newVal,oldVal)=>{ console.log("发生变化了",newVal,oldVal) })上文中watch第一个值用了一个getter函数,简写为箭头函数,能返回一个值
以实现只监视一个属性
进一步简写为
watch(()=>person.name,(newVal,oldVal)=>{ console.log("发生变化了",newVal,oldVal) })对象属性是对象类型
watch(person.car,(newVal,oldVal)=>{ console.log("发生变化了",newVal,oldVal) })上面的有缺陷:就是整个car变了没有被监视到
建议写全,监视的是地址值,属性变化监视不到
watch(()=>person.car,(newVal,oldVal)=>{ console.log("发生变化了",newVal,oldVal) })需要开启deep
watch(()=>person.car,(newVal,oldVal)=>{ console.log("发生变化了",newVal,oldVal) },{deep:true})监视多种值
watch([()=>person.name,()=>person.car.c1],(newVal,oldVal)=>{ console.log("发生变化了",newVal,oldVal) },{deep:true})watch Effect
当前水温:{{ temp }}C
当前水位:{{ height }}M
点我水温+1 点我水位+1 import {ref,watch} from 'vue' let temp=ref(20) let height=ref(3) function changeTemp(){ temp.value++ } function changeHeight(){ height.value++ } watch([temp,height],(newVal,oldVal)=>{ // 从newVal中获取新的温度和水位 let [newTemp,newHeight]=newVal if(newTemp>=30||newHeight>=8){ console.log("alarm",newVal,oldVal) } })改用watch effect,有immediate效果
因为被监视值就是定义的属性,
watchEffect(()=>{ if(temp.value>=30||height.value>=10){ console.log("hello") } })标签REF
用Ref的原因,是因为如果使用id属性,那么不同vue文件可能重复使用
深圳
console.log(document.getElementById('title2'))
输出为
对应改为ref2
中国
深圳
龙华
点我输出h2 import{ref} from 'vue' // 创建一个title2,用于存储ref2标记的内容 let title2=ref() function showLog(){ // console.log(document.getElementById('title2')) console.log(title2.value) }有的时候会输出,标签中会有 data-v-4cadc14e,这是局部样式导致的,去掉scoped就没有了
中国
深圳
龙华
点我输出h2 import{ref} from 'vue' // 创建一个title2,用于存储ref2标记的内容 let title2=ref() function showLog(){ // console.log(document.getElementById('title2')) console.log(title2.value) } .person{ background-color: cadetblue; }REF在组件
Ref都是加在普通的html标签上而不是组件标签
如果给组件加,比如
ok
点我输出 import Person from './components/Person.vue' import {ref} from 'vue' let title2=ref() let ren=ref() function outPrint(){ // console.log(title2.value) console.log(ren.value) } // export default{ // name:'App', // components:{Person} // }输出ref是组件的实例对象,但是什么具体值也看不到,原因是被本vue文件作为父级保护起来,这与Vue2不同,在Vue2中父级是可以看到所有子级的
如果要显示,则需要去组件中添加
import{ref,defineExpose} from 'vue
defineExpose({a,b,c})
中国
深圳
龙华
点我输出h2 import{ref,defineExpose} from 'vue' // 创建一个title2,用于存储ref2标记的内容 let title2=ref() let a=ref(0) let b=ref(1) let c=ref(2) function showLog(){ // console.log(document.getElementById('title2')) console.log(title2.value) } defineExpose({a,b,c}) .person{ background-color: cadetblue; }总结
ref可以定义在html普通标签,拿到的是元素
也可以定义在组件上,拿到的是组件实例,能看到哪些元素取决于组件本身的expose
TS
约束定义
1)在src下新建types文件夹,下面新建index.ts
// 定义一个接口对象,用于限制person对象的具体数值 export interface PersonInter{ id:string, name:string, age:number } // export type Persons=Array export type Persons=PersonInter[]上面定义的是对象
下面定义了数组,引用了对象,有两种定义方法,一种用泛型,另外一种用[]
约束使用
先引入
然后用冒号
??? // import的是个约束,不是具体值,所以无法打印 import {type PersonInter,type Persons} from '@/types' //下文的意思是person要符合接口规范,包括变量名称 let person:PersonInter={id:'001',name:'李白',age:22} //下文的意思是person数组泛型要符合接口规范,包括变量名称 // let persons:Array=[ let persons:Persons=[ {id:'001',name:'李白',age:22}, {id:'002',name:'杜甫',age:32} {id:'003',name:'白居易',age:36} ] .person{ background-color: cadetblue; }Props
1. 属性中冒号
let x=99有了冒号代表取变量或者本身就是表达式,结果如下
2.给子组件传值
子组件接收:注意子组件里面的let x把所有的defineProps里面的值都保存了起来
import {defineProps} from 'vue' // 接收a // defineProps(['a','b']) //接收a并且保存起来 let x=defineProps(['a','b','list']) console.log(x.a) console.log(x)v-for
如果写为
:list="5" 子组件显示会展示5次
v-for的key主要是为了更新,作为索引;如果不指定key,则用数据下标0,1,2等为索引,容易混乱
- persons也可以用具体数字
如果v-for对应list为空,则直接不显示
可有可无的属性
参见x后的?
export interface PersonInterface{ id:string, name:string, age:number, x?: number } export type Persons=PersonInterface[]优雅泛型
参加reactive后面的取代了PersonList:Persons
let PersonList=reactive ([ {id:'001',name:'李白',age:22}, {id:'002',name:'杜甫',age:23}, {id:'003',name:'白居易',age:24}, {id:'004',name:'苏轼',age:25}, ])接收并限制类型
子组件接收时检查类型
- {{item.id}}---{{item.name}}---{{ item.age }}
宏函数
define定义的函数比如defineProps等,vue3默认引入,不会报错
生命周期
也叫生命周期函数,钩子
组件:创建 挂载 更新 销毁
created mounted
vue2声明周期
全局安装
npm install -g @vue/cli
查看vue版本 vue -V
创建 vue create vue2test
创建 前 before 后
挂载
更新
销毁
当前求和为{{sum}}
点我sum+1 export default{ /* eslint-disable */ name:'Person', data(){ return{ sum : 1, } }, methods:{ addSum(){ this.sum++; } }, // 顺序不重要,何时调vue决定 // 创建:比如人怀孕 beforeCreate() { console.log("创建前"); }, created(){ console.log("创建完毕") }, // 挂载:出生 beforeMount() { console.log("没有挂载") // debugger //停在这里 }, mounted(){ console.log("挂载完毕") }, //更新,多次 beforeUpdate(){ console.log("更新前") }, updated() { console.log("更新完毕") }, //销毁前 beforeDestroy(){ console.log("销毁前") }, destroyed(){ console.log("销毁完毕") } }vue3声明周期
1.子先挂载
1.子先挂载,App这个组件最后挂载
2.vue2与vue3生命周期对比
3.vue3
{{ sum }}
点我加1 import { ref,onBeforeMount,onMounted,onBeforeUpdate,onUpdated,onBeforeUnmount,onUnmounted} from 'vue'; let sum=ref(0) function addSum(){ sum.value++ } // setup 相当于befeforeCreate和created console.log("创建完了") //挂载需要引入onBeforeMounted onBeforeMount(()=>{ console.log("挂载前") }) //挂载完引入onMounted onMounted(()=>{ console.log("子---挂载完毕") }) //更新前 onUpdated(()=>{ console.log("更新前") }) //更新后 onUpdated(()=>{ console.log("更新后") }) //卸载前 onBeforeUnmount(()=>{ console.log("卸载前") }) //卸载后 onUnmounted(()=>{ console.log("卸载后") })axios
npm i axios
获取狗的图片
https://dog.ceo/api/breed/pembroke/images/random
后端服务器返回的一定是对象,里面有很多key value
{data: {…}, status: 200, statusText: '', headers: AxiosHeaders, config: {…}, …}
找到需要要的key
hooks
{{ sum }}
点我加1再来一只狗 import { ref,reactive } from 'vue'; import axios from 'axios' let sum=ref(0); function addSum(){ sum.value++; } let dogList=reactive([ "https://images.dog.ceo/breeds/pembroke/n02113023_3927.jpg", "https://images.dog.ceo/breeds/pembroke/n02113023_1659.jpg" ]) async function moreDog(){ try{let result= await axios.get('https://dog.ceo/api/breed/pembroke/images/random') dogList.push(result.data.message)}catch(error){ console.log(error) } } img{ height: 200px; margin-right: 10px; }
:把相关的数据和方法写在一起,有点像vue2 mixin
step1:
src下新建hooks文件夹
本质就是xx.ts或xx.js
命名规范 useXXX
step2:
定义bing暴露
useDog.ts
import { reactive } from 'vue'; import axios from 'axios' //export default后面直接跟值比如export default '1',所以也可以跟一个匿名函数,如果没有default则必须命名 export default function(){ let dogList=reactive([ "https://images.dog.ceo/breeds/pembroke/n02113023_3927.jpg", "https://images.dog.ceo/breeds/pembroke/n02113023_1659.jpg" ]) async function getDog(){ try{let result= await axios.get('https://dog.ceo/api/breed/pembroke/images/random') dogList.push(result.data.message)}catch(error){ console.log(error) } } // 向外部提供东西,可以提供对象 return {dogList,getDog} }useSum.ts
import { ref ,onMounted,computed} from 'vue'; export default function(){ let sum=ref(0); let bigSum=computed(()=>{ return sum.value*10 }) function addSum(){ sum.value++; } onMounted(()=>{ addSum() }) return {sum,addSum,bigSum} }stpe3
引入 接收 使用
{{ sum }} 放大10倍后{{ bigSum }}
点我加1再来一只狗 import useDog from '@/hooks/useDog' import useSum from '@/hooks/useSum' const {dogList,getDog}=useDog() const {sum,addSum,bigSum}=useSum() img{ height: 200px; margin-right: 10px; }
前端路由
因为是单页面应用所以需要路由
当点击左侧导航,触发路径变化
路径变化被路由器捕获到,加载对应组件
点击另外一个目录时,卸载原来的组件,挂载新的组件
路由设置
1)确定页面导航区,展示区
2)请来路由器
安装路由器 npm i vue-router
src下建立router文件夹 建立 index.ts
3)制定路由的具体规则(什么路径,对应什么组件)
// 创建一个路由器bing暴露出去 // 1.1引入createRouter import { createRouter ,createWebHistory} from "vue-router"; // 1.2引入要呈现的组件 ,一开始飘红需要关掉vscode重新打开,主要是不认vue3 import Home from '@/components/Home.vue' import News from '@/components/News.vue' import About from '@/components/About.vue' // 2.创建路由器 const router=createRouter({ history:createWebHistory(),//确定工作模式 routes:[ //一个一个的路由规则 { path:'/home',component:Home }, { path:'/news',component:News }, { path:'/about',component:About } ] }) // 3.暴露 export default router //4.要去main.ts引入路由器4)main.ts中引入路由
import { createApp } from "vue" import App from './App.vue' // 路由4:引入路由器 import router from "./router" // 创建一个应用 const app=createApp(App) // 使用路由器 app.use(router) // 挂载整个应用 app.mount('#app')当有个app.use(router)之后,就可以看到控制台多了Routes
这时候在地址栏加比如/news 就会被router监控到,但是还不知道展示在哪里
5)展示位置
import { RouterView } from 'vue-router';
6)增加切换支持
import { RouterView,RouterLink} from 'vue-router'; 首页 新闻 关于7)选中高亮
Vue 路由测试
首页 新闻 关于 import { RouterView,RouterLink} from 'vue-router'; /* app */ .title{ text-align: center; word-spacing: 5px; margin: 30px 0; height: 70px; line-height: 70px; background-image: linear-gradient(45deg,gray,white); border-radius: 10px; box-shadow: 0 0 2px; font-size: 30px; } .navigate{ display: flex; justify-content: space-around; margin: 0 100px; } .navigate a{ display: block; text-align: center; width: 90px; height: 40px; line-height: 40px; border-radius: 10px; background-color: gray; text-decoration: none; color: white; font-size: 18px; letter-spacing: 5px; } .navigate a.active{ background-color: #64967E; color: #ffc268; font-weight: 900; text-shadow: 0 0 1px black; font-family: 微软雅黑; } .main-content{ margin: 0 auto; margin-top: 30px; border-radius: 10px; width: 90%; height: 400px; border: 1px solid; }注意
路由组件:就是通过路由引入进来的
一般组件:通过import进来的,页面上要写
视觉小时了组件是被卸载了
路由工作模式
history
vue2 mode:'history'
vue3: history:createWebHistory()
hash
路由to的两种写法
1.字符串写法
{{item.title}}
2.对象写法1
关于
3.对象写法2
新闻
参见命名路由
命名路由
增加了name
routes:[ //一个一个的路由规则 { name:'zhueye', path:'/home', component:Home }, { name:'xinwen', path:'/news',component:News }, { name:'guanyu', path:'/about',component:About } ]嵌套路由
[Vue Router warn]: No match found for location with path "/" 没有/的路由http://localhost:5173/
先引入
routes:[ //一个一个的路由规则 { name:'zhueye', path:'/home', component:Home }, { name:'xinwen', path:'/news',component:News, children:[ { path:'detail', component:Details } ] }, { name:'guanyu', path:'/about',component:About } ] })- {{item.title}}
路由参数
query
父给子 {{item.title}}
a=哈哈相当于键值对 &连接多个
子组件接收:
引入route
- 编号:{{route.query.id}}
- 标题:{{route.query.title}}
- 内容:{{route.query.content}}
传递的参数在route对象的Target里面的query参数
第一种写法
{{item.title}}
第二种写法
简化子处使用参数
注意解构赋值会丢失响应式
新的details.vue
-
{route.query.id}}
- 标题:{{route.query.title}}
- 内容:{{route.query.content}} -->
- 编号:{{query.id}}
- 标题:{{query.title}}
- 内容:{{query.content}}
params
三个注意
1)在路由注册了参数,才能使用,并且不能缺
除非在路由参数加? path:'detail/:id/:title/:content?',
2)router link对象写法中必须使用路由name
3)router link对象写法中参数值不能是数组,对象
在路由后面不写?直接继续写/,也不用键值对
下面红色是路由,绿色是参数
{{item.title}}
没占位前会报 Vue Router warn]: No match found for location with path "/news/detail/哈哈/呃呃"
2)router/index.ts占位
path:'detail/:id/:title/:content',
有三个占位,则要求调用时必须是3个否则无法识别
{{item.title}}
-
{route.query.id}}
- 标题:{{route.query.title}}
- 内容:{{route.query.content}} --> {query.id}}
- 标题:{{query.title}}
- 内容:{{query.content}} -->
- 编号:{{route.params.id}}
- 标题:{{route.params.title}}
- 内容:{{route.params.content}}
父组件
如下写法必须使用name
- {item.title}} --> {item.title}} --> {item.title}} --> {item.title}} --> {item.title}} --> {{item.title}}
路由的props配置
为了简化页面引用的层级,可以在路由参数中配置: props:true
第一种写法
将路由收到所有params参数作为props传给路由组件
routes:[ //一个一个的路由规则
{
name:'zhueye',
path:'/home',
component:Home
},
{
name:'xinwen',
path:'/news',component:News,
children:[
{
name:'xiangqing',
path:'detail/:id/:title/:content?',
component:Details,
props:true
}
]
},
{ name:'guanyu',
path:'/about',component:About
}
]
加了props相当于在使用Detail时顺便传三个参数
-
{route.query.id}}
- 标题:{{route.query.title}}
- 内容:{{route.query.content}} --> {query.id}}
- 标题:{{query.title}}
- 内容:{{query.content}} --> {route.params.id}}
- 标题:{{route.params.title}}
- 内容:{{route.params.content}} -->
- 编号:{{id}}
- 标题:{{title}}
- 内容:{{content}}
第二种写法
可以用于query
自己决定将什么作为路由组件传给props
{{item.title}}
children:[
{
name:'xiangqing',
// path:'detail/:id/:title/:content?',
path:'detail',
component:Details,
// 第一种写法将路由收到所有params参数作为props传给路由组件
// props:true
//第二种写法 自己决定将什么作为路由组件传给props
props(qwe){
console.log(qwe)
return{
x:100,
y:200,
z:300
}
}
}
]
},
可以看到qwe就是route
router文件修改为
children:[
{
name:'xiangqing',
// path:'detail/:id/:title/:content?',
path:'detail',
component:Details,
// 第一种写法将路由收到所有params参数作为props传给路由组件
// props:true
//第二种写法 自己决定将什么作为路由组件传给props
// props收到的参数本质就是route,并且因为在query查询时query也是对象,所以可以返回
props(route){
return route.query
}
}
]
-
{route.query.id}}
- 标题:{{route.query.title}}
- 内容:{{route.query.content}} --> {query.id}}
- 标题:{{query.title}}
- 内容:{{query.content}} --> {route.params.id}}
- 标题:{{route.params.title}}
- 内容:{{route.params.content}} -->
- 编号:{{id}}
- 标题:{{title}}
- 内容:{{content}}
第三种写法
对象写法,但是是写死了,很少用
replace
路由跳转时,会操作浏览器历史记录,默认是push
push:
相当于浏览器历史记录是个栈,有个指针
replace
替换
在导航区routelink上加replace
首页
新闻
关于
编程式导航
脱离实现跳转
1.设置自动跳转
import { useRouter } from 'vue-router';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
const router=useRouter()
onMounted(()=>{
setTimeout(()=>{
// 在此处编写一段代码,让路由实现跳转
router.push('/news')
},3000)
})
.home{
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
2.设置点击跳转
- {item.title}} --> {item.title}} --> {item.title}} --> {item.title}} --> {item.title}} --> 查看新闻 {{item.title}}
重定向
主要是解决刚进入网站初始页面路由报错的问题
、让指定的路径重新定位到另一个路径
// 创建一个路由器bing暴露出去
// 1.1引入createRouter
import { createRouter ,createWebHistory} from "vue-router";
// 1.2引入要呈现的组件 ,一开始飘红需要关掉vscode重新打开,主要是不认vue3
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'
import Details from "@/pages/Details.vue";
// 2.创建路由器,自己路由不用写斜杠
const router=createRouter({
history:createWebHistory(),//确定工作模式
routes:[ //一个一个的路由规则
{
name:'zhueye',
path:'/home',
component:Home
},
{
name:'xinwen',
path:'/news',component:News,
children:[
{
name:'xiangqing',
// path:'detail/:id/:title/:content?',
path:'detail',
component:Details,
// 第一种写法将路由收到所有params参数作为props传给路由组件
// props:true
//第二种写法 自己决定将什么作为路由组件传给props
// props收到的参数本质就是route,并且因为在query查询时query也是对象,所以可以返回
// props(route){
// return route.query
// }
// 第三种写法
props:{
a:100,
b:200,
c:300
}
}
]
},
{ name:'guanyu',
path:'/about',component:About
},
{
path:'/',
redirect:'/home'
}
]
})
// 3.暴露
export default router
//4.要去main.ts引入路由器
pinia:共用数据的处理
Pinia | The intuitive store for Vue.js
Pinia 是 Vue 的存储库,它允许跨组件/页面共享状态。实际上,pinia就是Vuex的升级版,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3的Vuex。
vue2用的是vueX
vue3用的是pinia
集中式状态(也就是数据)管理
把各个组件需要共享的数据交给pinia
https://api.uomg.com/api/rand.qinghua?format=json
生成id npm i nanoid npm i uuid
准备
1.sum.app 注意把下值转换为数字的两种方法
v-model.number=‘n’ 这样更优雅
或者:value=
当前求和为{{ sum }}
1
2
3
加
减
import { ref } from 'vue';
let sum=ref(0)
let n=ref(0)
function add(){
sum.value+=n.value
}
function minus(){
sum.value-=n.value
}
.count{
background-color: skyblue;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px;
}
select,button{
margin: 0 5px;
height: 30px;
}
2.Talk.vue
注意axios获取接口数据后的两次结构,一次重命名
还有取随机数
获取一句诗歌
- {{ item.title }}
安装
npm i pinia
main.ts引入 创建 安装三步
import { createApp } from "vue"
import App from './App.vue'
// pinia1 :引入pinia
import { createPinia } from "pinia"
// 路由4:引入路由器
// import router from "./router"
// 创建一个应用
const app=createApp(App)
// 使用路由器
// app.use(router)
// pinia2 app创建完后创建pinia
const pinia=createPinia()
// pinia3 :安装pinia
app.use(pinia)
// 挂载整个应用
app.mount('#app')
之后浏览器会出现
建立仓库
pinia强调分类
src下应该建立store
在其下建立比如count.ts
import { defineStore } from "pinia";
// const命名规范:以use开始
// 下面的export使用的是分离暴露,也可以使用统一暴露,分离暴露引用时只能用import {}
export const useCountStore=defineStore(
// 第一个参数一般建议与文件名一致
'count',
{
// 真正存储数据的地方是state
state(){
return{
sum:6
}}
}
)
这样就有了
把cout.ts想象成一个仓库,只要跟统计相关的都放入
组件使用
// pinar1:引入
import {useCountStore} from '@/store/count'
//
const countStore=useCountStore();
console.log("@@@@@",countStore)
countStore实际上是reactive定义的响应式对象
关注sum和$state
进一步展开
sum是个REF对象,其有value,但是contStore是个Reactive对象
怎么样读出reactive里面的ref值呢?不用.value即可
let obj=reactive({
a:1,
b:2,
c:ref(3)
})
// 在reactove里面的ref不用再拆value出来
// console.log(obj.c.value)
console.log(obj.c)
countStore.sum
也可以从state下面拿到,麻烦一点
countStore.$state.sum
再查看控制台,注意 就是count.ts被组件使用了才会出现在这里
另外一个例子
talk.ts
import { defineStore } from "pinia";
// const命名规范:以use开始
// 下面的export使用的是分离暴露,也可以使用统一暴露,分离暴露引用时只能用import {}
export const useTalkStore=defineStore(
// 第一个参数一般建议与文件名一致
'talk',
{
// 真正存储数据的地方是state
state(){
return{
talkList:[
{id:'0001',title:'秦时明月汉时关'},
{id:'0002',title:'床前明月光'},
{id:'0003',title:'北风卷地百草折'},
{id:'0004',title:'东边不亮西边亮'},
{id:'0005',title:'遥看瀑布挂前川'}
]
}}
}
)
被使用后
talk.vue
获取一句诗歌
- {{ item.title }}
talkList是reactive下的reactive,
修改数据
三种修改方法-1,2
当前求和为{{ countStore.sum }}
欢迎来到{{ countStore.school }}坐落于{{ countStore.address }}
1
2
3
加
减
import { reactive, ref } from 'vue';
// pinar1:引入
import {useCountStore} from '@/store/count'
//
const countStore=useCountStore();
// console.log("@@@@@",countStore.sum)
console.log("@@@@@",countStore)
let n=ref(0)
function add(){
// 第一种修改数据方法:直接改
// countStore.sum+=n.value
// countStore.school="南京大学"
// countStore.address="南京"
// 第二种修改方法:适用于数据较多
countStore.$patch(
{
sum:999,
school:'苏州大学',
address:'苏州'
}
)
}
function minus(){
}
.count{
background-color: skyblue;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px;
}
select,button{
margin: 0 5px;
height: 30px;
}
其它知识:
时间线的介绍
Mouse:看鼠标事件
常用的Componet events:组件事件
当用上述第一种直接修改,时间线pinia出现了3次,因为确实直接修改了三次
当使用上述第二种方法时,只发生了1次
第三种修改方法
在store/count.ts中使用action
import { defineStore } from "pinia";
// const命名规范:以use开始
// 下面的export使用的是分离暴露,也可以使用统一暴露,分离暴露引用时只能用import {}
export const useCountStore=defineStore(
// 第一个参数一般建议与文件名一致
'count',
{
//action里面放置的是一个一个方法,用于响应组件中的“动作"
actions:{
increment(value:any){
console.log("increment被调用了",value,this)
// 通过this操作
this.sum+=value
// 或者
}
},
// 真正存储数据的地方是state
state(){
return{
sum:6,
school:'btj',
address:'beijing'
}}
}
)
组件
当前求和为{{ countStore.sum }}
欢迎来到{{ countStore.school }}坐落于{{ countStore.address }}
1
2
3
加
减
import { reactive, ref } from 'vue';
// pinar1:引入
import {useCountStore} from '@/store/count'
//
const countStore=useCountStore();
// console.log("@@@@@",countStore.sum)
console.log("@@@@@",countStore)
let n=ref(0)
function add(){
// 第一种修改数据方法:直接改
// countStore.sum+=n.value
// countStore.school="南京大学"
// countStore.address="南京"
// 第二种修改方法:适用于数据较多
// countStore.$patch(
// {
// sum:999,
// school:'苏州大学',
// address:'苏州'
// }
// )
// 第三种修改方法:
countStore.increment(n.value);
}
function minus(){
}
.count{
background-color: skyblue;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px;
}
select,button{
margin: 0 5px;
height: 30px;
}
注意this是当前的store,就是countStore可以直接使用sum
还有一个常识:$开始的都是给程序员用的,也可以用里面的$state
第二个案例
import { defineStore } from "pinia";
import axios from "axios";
import { nanoid } from "nanoid";
// const命名规范:以use开始
// 下面的export使用的是分离暴露,也可以使用统一暴露,分离暴露引用时只能用import {}
export const useTalkStore=defineStore(
// 第一个参数一般建议与文件名一致
'talk',
{
actions:{
async getPoem(){
// 发请求
let result= await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
// 以下是从返回中先结构data,再从data中结构content,并且把content重命名为title
// let {data:{content:title}}= await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
// // console.log(result.data.content)
// // 把请求回来的字符串包装成一个对象
let obj={id:nanoid(),title:result.data.content}
// // let obj={id:nanoid(),title:title}
// let obj={id:nanoid(),title}
this.talkList.unshift(obj)
}
},
// 真正存储数据的地方是state
state(){
return{
talkList:[
{id:'0001',title:'秦时明月汉时关'},
{id:'0002',title:'床前明月光'},
{id:'0003',title:'北风卷地百草折'},
{id:'0004',title:'东边不亮西边亮'},
{id:'0005',title:'遥看瀑布挂前川'}
]
}}
}
)
获取一句诗歌
- {{ item.title }}
优雅化
目的是让vue文件插值中的{{counterStore.sum]]中的counterStorm能被去掉
方法1 使用toRefs解构
代价:太多东西被toRefs
count.ts
import { defineStore } from "pinia";
// const命名规范:以use开始
// 下面的export使用的是分离暴露,也可以使用统一暴露,分离暴露引用时只能用import {}
export const useCountStore=defineStore(
// 第一个参数一般建议与文件名一致
'count',
{
//action里面放置的是一个一个方法,用于响应组件中的“动作"
actions:{
increment(value:number){
console.log("increment被调用了",value,this)
// 通过this操作
this.sum+=value
// 或者
}
},
// 真正存储数据的地方是state
state(){
return{
sum:6,
school:'btj',
address:'beijing'
}}
}
)
Count.vue
当前求和为{{ sum }}
欢迎来到{{ countStore.school }}坐落于{{ countStore.address }}
1
2
3
加
减
import { reactive, ref,toRefs } from 'vue';
// pinar1:引入
import {useCountStore} from '@/store/count'
//
const countStore=useCountStore();
// console.log("@@@@@",countStore.sum)
// console.log("@@@@@",countStore)
// 解构
const {sum,school,address} =toRefs(countStore)
console.log(toRefs(countStore))
let n=ref(0)
function add(){
// 第一种修改数据方法:直接改
// countStore.sum+=n.value
// countStore.school="南京大学"
// countStore.address="南京"
// 第二种修改方法:适用于数据较多
// countStore.$patch(
// {
// sum:999,
// school:'苏州大学',
// address:'苏州'
// }
// )
// 第三种修改方法:
countStore.increment(n.value);
}
function minus(){
countStore.sum-=n.value
}
.count{
background-color: skyblue;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px;
}
select,button{
margin: 0 5px;
height: 30px;
}
上文中用了toRefs,但是显示把countStores所有属性都变成了ref,没有这个必要
方法2 使用storeToRefs 解构
pinia替你想到了
import { storeToRefs } from 'pinia';
// 解构
const {sum,school,address} =storeToRefs(countStore)
console.log("aaaaa",storeToRefs(countStore))
可以看到storeToRefs只关注数据,不会对方法进行包裹
当前求和为{{ sum }}
欢迎来到{{ school }}坐落于{{ address }}
1
2
3
加
减
import { reactive, ref,toRefs } from 'vue';
import { storeToRefs } from 'pinia';
// pinar1:引入
import {useCountStore} from '@/store/count'
//
const countStore=useCountStore();
// console.log("@@@@@",countStore.sum)
// console.log("@@@@@",countStore)
// 解构
const {sum,school,address} =storeToRefs(countStore)
console.log("aaaaa",storeToRefs(countStore))
let n=ref(0)
function add(){
// 第一种修改数据方法:直接改
// countStore.sum+=n.value
// countStore.school="南京大学"
// countStore.address="南京"
// 第二种修改方法:适用于数据较多
// countStore.$patch(
// {
// sum:999,
// school:'苏州大学',
// address:'苏州'
// }
// )
// 第三种修改方法:
countStore.increment(n.value);
}
function minus(){
countStore.sum-=n.value
}
.count{
background-color: skyblue;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px;
}
select,button{
margin: 0 5px;
height: 30px;
}
getters
对数据不满意,可以加工下
写法1
import { defineStore } from "pinia";
// const命名规范:以use开始
// 下面的export使用的是分离暴露,也可以使用统一暴露,分离暴露引用时只能用import {}
export const useCountStore=defineStore(
// 第一个参数一般建议与文件名一致
'count',
{
//action里面放置的是一个一个方法,用于响应组件中的“动作"
actions:{
increment(value:number){
console.log("increment被调用了",value,this)
// 通过this操作
this.sum+=value
// 或者
}
},
// 真正存储数据的地方是state
state(){
return{
sum:6,
school:'btj',
address:'beijing'
}},
getters:{
// bigSum(){
// return 999
// }
// 默认被传递进来state
bigSum(state){
return state.sum*10
}
}
}
)
写法2
this起始也是state
import { defineStore } from "pinia";
import { compileScript } from "vue/compiler-sfc";
// const命名规范:以use开始
// 下面的export使用的是分离暴露,也可以使用统一暴露,分离暴露引用时只能用import {}
export const useCountStore=defineStore(
// 第一个参数一般建议与文件名一致
'count',
{
//action里面放置的是一个一个方法,用于响应组件中的“动作"
actions:{
increment(value:number){
console.log("increment被调用了",value,this)
// 通过this操作
this.sum+=value
// 或者
}
},
// 真正存储数据的地方是state
state(){
return{
sum:6,
school:'btj',
address:'beijing'
}},
getters:{
// bigSum(){
// return 999
// }
// 默认被传递进来state
// bigSum(state){
// return state.sum*10
// },
//简写
bigSum:state=>state.sum*10,
// 也可以用this
// upperSchool(state){
// :String是表示返回值是字符串
upperSchool():String{
console.log("upperschool",this)
return this.school.toUpperCase()
}
}
}
)
Count.vue
当前求和为{{ sum }},放大10倍后{{ bigSum }}
欢迎来到{{ school }},坐落于{{ address }} 大写{{ upperSchool }}
1
2
3
加
减
import { reactive, ref,toRefs } from 'vue';
import { storeToRefs } from 'pinia';
// pinar1:引入
import {useCountStore} from '@/store/count'
//
const countStore=useCountStore();
// console.log("@@@@@",countStore.sum)
// console.log("@@@@@",countStore)
// 解构
const {sum,school,address,bigSum,upperSchool} =storeToRefs(countStore)
console.log("aaaaa",storeToRefs(countStore))
let n=ref(0)
function add(){
// 第一种修改数据方法:直接改
// countStore.sum+=n.value
// countStore.school="南京大学"
// countStore.address="南京"
// 第二种修改方法:适用于数据较多
// countStore.$patch(
// {
// sum:999,
// school:'苏州大学',
// address:'苏州'
// }
// )
// 第三种修改方法:
countStore.increment(n.value);
}
function minus(){
countStore.sum-=n.value
}
.count{
background-color: skyblue;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px;
}
select,button{
margin: 0 5px;
height: 30px;
}
订阅
相当于监视store数据变化,可以利用来保存到storage,实现持久化
获取一句诗歌
- {{ item.title }}
mutate 和 state分别打印
应用1:修改locaiStorage
比如
locaiStorage里面都是字符串,如果你传的不是字符串会调用toString,如果都是对象就会变成如下:
修改为
localStorage.setItem('talkList',JSON.stringify(state.talkList))
talk.ts
import { defineStore } from "pinia";
import axios from "axios";
import { nanoid } from "nanoid";
// const命名规范:以use开始
// 下面的export使用的是分离暴露,也可以使用统一暴露,分离暴露引用时只能用import {}
export const useTalkStore=defineStore(
// 第一个参数一般建议与文件名一致
'talk',
{
actions:{
async getPoem(){
// 发请求
let result= await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
// 以下是从返回中先结构data,再从data中结构content,并且把content重命名为title
// let {data:{content:title}}= await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
// // console.log(result.data.content)
// // 把请求回来的字符串包装成一个对象
let obj={id:nanoid(),title:result.data.content}
// // let obj={id:nanoid(),title:title}
// let obj={id:nanoid(),title}
this.talkList.unshift(obj)
}
},
// 真正存储数据的地方是state
state(){
return{
// 可以从localStorage中获取
// talkList:[
// {id:'0001',title:'秦时明月汉时关'},
// {id:'0002',title:'床前明月光'},
// {id:'0003',title:'北风卷地百草折'},
// {id:'0004',title:'东边不亮西边亮'},
// {id:'0005',title:'遥看瀑布挂前川'}
// ]
// talkList:JSON.parse(localStorage.getItem('talkList'))
// 可能去除null,解决方法1:用断言,但是这样初始化就是null,二要添加的时候就会报不能在null上添加unshift,就是null.unshift
// talkList:JSON.parse(localStorage.getItem('talkList') as string)
//如下解决null的问题
talkList:JSON.parse(localStorage.getItem('talkList') as string)||[]
}}
}
)
talk.vue
获取一句诗歌
- {{ item.title }}
store组合式写法
缺点是return多
import { defineStore } from "pinia";
import axios from "axios";
import { nanoid } from "nanoid";
// const命名规范:以use开始
// 下面的export使用的是分离暴露,也可以使用统一暴露,分离暴露引用时只能用import {}
// 1.选项式写法
// export const useTalkStore=defineStore(
// // 第一个参数一般建议与文件名一致
// 'talk',
// {
// actions:{
// async getPoem(){
// // 发请求
// let result= await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
// // 以下是从返回中先结构data,再从data中结构content,并且把content重命名为title
// // let {data:{content:title}}= await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
// // // console.log(result.data.content)
// // // 把请求回来的字符串包装成一个对象
// let obj={id:nanoid(),title:result.data.content}
// // // let obj={id:nanoid(),title:title}
// // let obj={id:nanoid(),title}
// this.talkList.unshift(obj)
// }
// },
// // 真正存储数据的地方是state
// state(){
// return{
// // 可以从localStorage中获取
// // talkList:[
// // {id:'0001',title:'秦时明月汉时关'},
// // {id:'0002',title:'床前明月光'},
// // {id:'0003',title:'北风卷地百草折'},
// // {id:'0004',title:'东边不亮西边亮'},
// // {id:'0005',title:'遥看瀑布挂前川'}
// // ]
// // talkList:JSON.parse(localStorage.getItem('talkList'))
// // 可能去除null,解决方法1:用断言,但是这样初始化就是null,二要添加的时候就会报不能在null上添加unshift,就是null.unshift
// // talkList:JSON.parse(localStorage.getItem('talkList') as string)
// //如下解决null的问题
// talkList:JSON.parse(localStorage.getItem('talkList') as string)||[]
// }}
// }
// )
// 1.组合式写法
// 组合式数据直接用reactive定义
import { reactive } from "vue";
export const useTalkStore=defineStore('talk',()=>{
// talklist就是state
const talkList=reactive(
JSON.parse(localStorage.getItem('talkList') as string)||[]
)
// getPoem相当于action
async function getPoem(){
let result= await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
let obj={id:nanoid(),title:result.data.content}
talkList.unshift(obj)
}
return {talkList,getPoem}
})
组件通信
1.props
可以实现父子双向传递
父:
父组件
汽车{{ car }}
子给的 {{ toy }}
import Child from './Child.vue';
import {ref} from 'vue'
// 数据
let car=ref('奔驰')
let toy=ref('')
// 方法
function getToy(value:string){
console.log('父',value)
toy.value=value
}
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
子
子组件
{{ toy }}
父亲给的车:{{ car }}
把玩具给父亲
import {ref} from 'vue'
// 数据
let toy=ref('奥特曼')
// 声明接收props
defineProps(['car','sendToy'])
// let props=defineProps(['car','sendToy'])
// // 方法
// function fasong(){
// console.log(props.sendToy)
// }
.child{
background-color: skyblue;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
尽可能不要给孙子传,虽然能实现
2.自定义事件
典型的用于子传父
事件名是多个单词是推荐肉串形式 a-b-c, 使用驼峰可能导致见听不到
$event
点我
function test(value){
console.log('test',value)
}
1)调用时如果什么都不传,发现value是事件对象
2)有参调用时需要调用时写$event
父组件
点我
import Child from './Child.vue';
function test(a:number,b:number,c:Event){
console.log('test',c)
}
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
3)直接使用 @click="str=$event"
父组件
{{ str }}
点我
import Child from './Child.vue';
import { ref } from 'vue';
let str=ref("你好")
function test(){
str.value="哈哈"
}
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
点击输出:
自定义事件
父亲绑定:
父组件
{ str }} -->
import Child from './Child.vue';
// import { ref } from 'vue';
// let str=ref("你好")
// function test(){
// str.value="哈哈"
// }
function xyz(value:number){
console.log('xyz',value)
}
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
子声明
并且调用时可以传值
子组件
玩具 {{ toy }}
测试调用父给子绑定的自定义事件并且传递值
import {ref,onMounted} from 'vue'
let toy=ref("奥特曼")
// 声明事件,并赋值给变量,这样模版就可以用了
const emit=defineEmits(['abc'])
// 加载3秒后触发abc事件,触发结果就是调用Father的xyz
onMounted(()=>{
setTimeout(()=>{
emit('abc')
},3000)
})
.child{
background-color: skyblue;
margin-top: 10px;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
自定义事件例子2
Father.vue
父组件
{ str }} -->
子给的玩具 {{ toy }}
import Child from './Child.vue';
import {ref} from 'vue'
let toy=ref("")
// import { ref } from 'vue';
// let str=ref("你好")
// function test(){
// str.value="哈哈"
// }
// function xyz(value:number){
// console.log('xyz',value)
// }
// function saveToy(value:number){
// console.log('xyz',value)
function saveToy(value:string){
console.log('sendToy',value)
toy.value=value
}
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
Child.vue
子组件
玩具 {{ toy }}
测试调用父给子绑定的自定义事件并且传递值
import {ref,onMounted} from 'vue'
let toy=ref("奥特曼")
// 声明事件,并赋值给变量,这样模版就可以用了
// const emit=defineEmits(['abc'])
const emit=defineEmits(['sent-toy'])
// 加载3秒后触发abc事件,触发结果就是调用Father的xyz
// onMounted(()=>{
// setTimeout(()=>{
// emit('abc')
// },3000)
// })
.child{
background-color: skyblue;
margin-top: 10px;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
3.mitt
任意组件通信
pubsub 订阅
$bus
mitt
收数据的:提前绑定好事件(pubsub叫提前订阅好消息)
提供数据的:在合适的时候触发事件(pubsub叫发布消息)
本质是定义一个公共区域
1.安装 npm i mitt
2.src下建立utils文件夹
emitter.ts
// 引入mitt import mitt from 'mitt' // 调用mitt 获得傀儡emitter emitter可以绑定事件,出发事件 const emitter=mitt() // 暴露emitter export default emitter
3.main.ts中引入一行
import { createApp } from "vue";
import App from './App.vue'
import { createPinia } from "pinia";
import router from "./router";
import emitter from '@/utils/emitter'
const app=createApp(App)
const pinia=createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')
4.使用
emitter.ts中
all:拿到所有事件
emit:触发事件
off:解绑事件
on:绑定事件
简单使用
// 引入mitt
import mitt from 'mitt'
// 调用mitt 获得傀儡emitter emitter可以绑定事件,出发事件
const emitter=mitt()
// 绑定事件
emitter.on('test1',()=>{
console.log("test1被调用了")
})
emitter.on('test2',()=>{
console.log("test2被调用了")
})
// 触发事件
// setTimeout(()=>{
// emitter.emit('test1')
// emitter.emit('test2')
// },5000)
setInterval(
()=>{
emitter.emit('test1')
emitter.emit('test2')
},2000
)
// 解绑事件,3s后解绑test1
setTimeout(()=>{
emitter.off('test1')
},4000)
// 清空事件
setTimeout(()=>{
emitter.all.clear()
},8000)
// 暴露emitter
export default emitter
例子1
哥哥提供玩具给弟弟
哥哥提供数据:触发事件/发布消息
弟弟接收数据:绑定事件/接收消息
关键是两个组件都要import emitter from '@/utils/emitter'
弟弟
子组件2
电脑{{ computer }}
哥哥给的玩具{{ toy }}
import {ref,onUnmounted} from 'vue'
let computer=ref('ThinkPad')
let toy=ref('') //一开始玩具是空
import emitter from '@/utils/emitter'
// 要拿到文具信息,就要gei emiiter绑定事件
emitter.on('send-toy',(value:any)=>{
console.log("send-toy",value)
toy.value=value
})
// 注意组件卸载时,解绑事件,有点类似总线,不卸载会被发消息的组件一直惦记,占用内存
onUnmounted(()=>{
emitter.off('send-toy')
})
.child{
margin-top: 50px;
background-color: orange;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
哥哥:
子组件1
玩具{{toy }}
玩具给弟弟
import {ref} from 'vue'
import emitter from '@/utils/emitter'
let toy=ref('奥特曼')
.child{
margin-top: 50px;
background-color: skyblue;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
emitter.ts
// // 引入mitt import mitt from 'mitt' // // 调用mitt 获得傀儡emitter emitter可以绑定事件,出发事件 const emitter=mitt() // 暴露emitter export default emitter
main.ts
不必引入emitter,因为传递消息双发已经独立引入了
4. v-model
很少用,可以父子双向传递
比如 hello,这里的大写的Button不是小写的button标签,而是组件
这里的a是传给组件的
UI组件库底层大量使用v-model进行通信
例子1:HTML标签之v-model
父组件
import {ref} from 'vue'
let userName=ref("李白")
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
在控制台可以看到
验证双向可修改
用在html标签上的v-model的本质
父组件
import {ref} from 'vue'
let userName=ref("李白")
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
例子2 组件之v-model
自制一个ui组件
input{
border: 2px solid;
background-image: linear-gradient(45deg,red,yellow,blue);
height: 30px;
font-size:20px;
color: white;
}
这个本质上input,但是input自身是可以使用v-model,这个不行
ui组件库绑定事件
import {defineProps,defineEmits} from 'vue'
defineProps(['modelValue'])
const emit=defineEmits(['update:modelValue'])
input{
border: 2px solid;
background-image: linear-gradient(45deg,red,yellow,blue);
height: 30px;
font-size:20px;
color: white;
}
使用ui组件
父组件
import {ref} from 'vue'
import SonghuiInput from './SonghuiInput.vue'
let username=ref("李白")
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
综合效果就是
补充
ui中的
event.target中的event是html元素,所以用.target
而下面是组件
注意
vue2中还是 :value和@input
vue3改为:modelValue和@update
改名
v-model 后面加qwe
右边3处也改为qwe
实现i多次使用v-model
5.attrs
当前组件的父组件给当前组件的子组件传数据,祖给孙
1.父传的多于子收的
父:
子: defineProps(['a',‘b’])
虽然c d没有收,但是记在了attrs里面,所以没声明接收的都在这里
使用
{{ $attrs }}
进一步引入v-bind
接收时在attrs里面被拆成x y
向孙传递
如果儿子一个都不接收,那么就可以完整的向孙传递
可见把响应数据和写死的xy都传给了孙子
孙子给爷爷
爷爷有方法一样可以传递
function updateA(value:number){
a.value+=value
}
孙子接收,并可更新爷爷的数据
孙组件
爷爷a{{ a }}
爷爷a{{ b}}
爷爷a{{ c }}
爷爷a{{ d }}
爷爷a{{ x }}
爷爷a{{ y }}
点位更新爷爷a
import { defineProps } from 'vue';
defineProps(['a','b','c','d','x','y','updateA'])
.child{
margin-top: 20px;
background-color: orange;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
6.$refs $parent
一父多子
$refs:父传子
$parent: 子传父
父亲动儿子的-1对1
子组件首先要释放权限defineExpose
子组件1
儿子1玩具{{ toy }}
儿子1书籍{{ book }}本书
import {ref} from 'vue'
let toy=ref("奥特曼")
let book=ref(4)
// 把数据交给外部
defineExpose({toy,book})
.child{
background-color: skyblue;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
父组件调用子组件时给唯一标记ref,因为c1是ref()就可以通过c1.value访问数据
父组件
父亲房产数{{ house }}
点我修改儿子1玩具
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';
import {ref} from 'vue'
let c1=ref()
let house=ref('4')
function changeToy(){
console.log(c1.value)
c1.value.toy="变形金刚"
}
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
访问结果
父亲动儿子的-1对多$refs
父组件
父亲房产数{{ house }}
点我修改儿子1玩具
点我修改儿子2电脑
获取所有子组件
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';
import {ref} from 'vue'
let c1=ref()
let c2=ref()
let house=ref('4')
function changeToy(){
console.log(c1.value)
c1.value.toy="变形金刚"
}
function changeComputer(){
c2.value.computer="Dell"
}
function getAllChild(e){
console.log(e)
}
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
$refs
获取了父组件中标记了ref的子组件,没标记则不获取
警告:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Object'.
No index signature with a parameter of type 'string' was found on type 'Object'.ts(7053)
因为index 可能是c1或 c2
// function getAllChild(refs:Object){
// 写成any也可以
// function getAllChild(refs:any){
function getAllChild(refs:{[key:string]:any}){
console.log(refs)
//更改两个儿子的书籍数目,在对象中遍历用key,在数组中遍历用index
for (let key in refs) {
console.log(key) //key就是字符串c1 c2
console.log(refs[key]) //获取对象中每一个key对应的值
refs[key].book+=3
}
父组件
父亲房产数{{ house }}
点我修改儿子1玩具
点我修改儿子2电脑
让所有孩子的书变多
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';
import {ref} from 'vue'
let c1=ref()
let c2=ref()
let house=ref('4')
function changeToy(){
console.log(c1.value)
c1.value.toy="变形金刚"
}
function changeComputer(){
c2.value.computer="Dell"
}
// function getAllChild(refs:Object){
// 写成any也可以
// function getAllChild(refs:any){
function getAllChild(refs:{[key:string]:any}){
console.log(refs)
//更改两个儿子的书籍数目,在对象中遍历用key,在数组中遍历用index
for (let key in refs) {
console.log(key) //key就是字符串c1 c2
console.log(refs[key]) //获取对象中每一个key对应的值
refs[key].book+=3
}
}
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
$parent
$parent获取父亲的实例对象
一样,父亲要先允许 defineExpose({house})
子组件1
儿子1玩具{{ toy }}
儿子1书籍{{ book }}本书
去掉父亲的1套房产
import {ref} from 'vue'
let toy=ref("奥特曼")
let book=ref(4)
// 把数据交给外部
defineExpose({toy,book})
function minusHouse(parent:any){
console.log(parent)
parent.house-=1
}
.child{
background-color: skyblue;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
补充
reactive下的ref不需要使用.value读取,自动解包
let obj=reactive({
a:1,
b:2,
c:ref(3)
})
// 当访问obj.c的时候,底层会自动读取value属性,因为c是在这个响应式对象中
console.log(obj.a)
console.log(obj.b)
console.log(obj.c)
7.provide-inject
祖给后代
祖
父组件
银子{{ money }}
一辆{{ car.brand }}车,价格{{ car.price }}
import Child from './Child.vue';
import {ref,reactive,provide} from 'vue'
let money=ref(5000)
let car=reactive({
brand:'奔驰',
price:200
})
// 向后代提供数据
provide('qian',money)
provide('che',car)
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
Grandson
孙组件
爷爷的钱{{ x }}
爷爷的车品牌是{{ y.brand }} 价格是{{ y.price }}
import {inject} from 'vue'
// 80000是找不到qian的注入的备用值
let x=inject('qian',80000)
// 使用第二个参数 缺省值可以相当于起到了推断断言的作用
let y=inject('che',{brand:'未知',price:'未知'})
.child{
margin-top: 20px;
background-color: orange;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
后代给祖
要求祖要有方法,一并传给后代
// 向后代提供数据及方法
provide('qianContext',{money,updateMoney})
父组件
银子{{ money }}
一辆{{ car.brand }}车,价格{{ car.price }}
import Child from './Child.vue';
import {ref,reactive,provide} from 'vue'
let money=ref(5000)
let car=reactive({
brand:'奔驰',
price:200
})
function updateGrandMoney(value:number){
money.value-=value
}
// 向后代提供数据
// provide('qian',money)
provide('che',car)
// 向后代提供数据及方法,注意:千万不能写为money:money.value,这样会让后代接收时失去响应式
provide('qianContext',{money,updateGrandMoney})
//provide('qianContext',{money:money.value,updateGrandMoney})
.father{
background-color: rgb(165,164,164);
padding: 20px;
border-radius: 10px;
}
孙
孙组件
爷爷的钱{{ money}}
爷爷的车品牌是{{ y.brand }} 价格是{{ y.price }}
花爷爷的钱
import {inject} from 'vue'
// 80000是找不到qian的注入的备用值
// let x=inject('qian',80000)
// 解构
// let {money,updateGrandMoney}=inject('qianContext',"我是缺省值")
let {money,updateGrandMoney}=inject('qianContext',{money:0,updateGrandMoney:(param:number)=>{}})
// 使用第二个参数 缺省值可以相当于起到了推断断言的作用
let y=inject('che',{brand:'未知',price:'未知'})
.child{
margin-top: 20px;
background-color: orange;
padding: 10px;
box-shadow: 0 0 10px black;
border-radius: 10px;
}
插槽
三种
需求
因为子组件被调用了3次,需要把三个变量都放在右侧,这样就没法在子组件接收参数
重新分析
如下span不不会显示在页面上,因为vue见到了组件标签就去解析组件了
在子组件加入slotE
默认插槽
挖个坑,一般只挖一个,如果挖多个就会每个坑都放
父组件
- {{ item.id }}--{{item.name}}
{{title}}
defineProps(['title'])
.category{
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
width:200px;
height: 300px;
}
h2{
background-color: orange;
text-align: center;
font-size: 20px;
font-weight: 800;
}
具名插槽
默认插槽也是有名字,只不过省略了
v-slot只能用在组件名称和template上
{title}} -->
默认标题
默认内容
// defineProps(['title'])
.category{
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
width:200px;
height: 300px;
}
父组件
热门游戏列表
- {{ item.id }}--{{item.name}}
今日美食城市
今日影视推荐
import Category from './Category.vue'; import {ref,reactive} from 'vue' let games=reactive([ {id:'001',name:'王者荣耀'}, {id:'002',name:'奇迹'}, {id:'003',name:'传奇'}, {id:'004',name:'原神'}, ]) let imgUrl=ref("https://t12.baidu.com/it/app=25&f=JPG&fm=175&fmt=auto&u=517088237%2C2849401642?w=402&h=300&s=501C76966449434754337E740300E078") let videoUrl=ref("https://media.w3.org/2010/05/sintel/trailer.mp4") .father{ background-color: rgb(165,164,164); padding: 20px; border-radius: 10px; } .content{ display: flex; justify-content: space-evenly; } img,video{ width: 100%; } h2{ background-color: orange; text-align: center; font-size: 20px; font-weight: 800; }简写#s1
今日影视推荐
作用域插槽
子组件slot上定义的属性,被传递给了使用者
父组件通过如下v-slot='a' 中的a接收
可以看到a是对象{},有三组key value
最后Father
父组件
{ a }} -->
- {{ item.id }} {{ item.name }}
- {{ item.id }} {{ item.name }}
{{ item.id }} {{ item.name }}
import Game from './Game.vue'; .father{ background-color: rgb(165,164,164); padding: 20px; border-radius: 10px; } .content{ display: flex; justify-content: space-evenly; } img,video{ width: 100%; }最后Game
游戏列表
import {reactive} from 'vue'
let games=reactive([
{id:'001',name:'王者荣耀'},
{id:'002',name:'奇迹'},
{id:'003',name:'传奇'},
{id:'004',name:'原神'},
])
.game{
width: 200px;
height: 300px;
background-color: skyblue;
border-radius: 10px;
box-shadow: 0 0 10px;
}
h2{
background-color: orange;
text-align: center;
font-size: 20px;
font-weight: 800;
}
其它API
ShallowRef:
浅层次ref,只处理第一层
如下可修改
求和为{{ sum }}
名字为{{ person.name }}
年龄为{{ person.age}}
sum++
修改名字
修改年龄
修改整个人
import {ref,shallowRef} from 'vue'
let sum=shallowRef(0)
let person=shallowRef({
name:'张三',
age:12
})
function addSum(){
sum.value++
}
function changeName(){
person.value.name='李白'
}
function changeAge(){
person.value.age=22
}
function changePerson(){
person.value={name:'王维',age:55}
}
.app{
background-color: #ddd;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
}
可修改sum和整个人,但是名字和年龄不能改
第一层就是person.value
shallowReactive
reactive定义的数据不能整体替换,要用Objective.
shallowReactive:如下只能改brand这个第一层
求和为{{ sum }}
名字为{{ person.name }}
年龄为{{ person.age}}
汽车为{{ car}}
sum++
修改名字
修改年龄
修改整个人
修改品牌
修改颜色
修改发动机
修改整个车
import {ref,shallowRef,reactive,shallowReactive} from 'vue'
let car=shallowReactive({
brand:'奔驰',
options:{
color:'红色',
engine:'v8'
}
})
let sum=shallowRef(0)
let person=shallowRef({
name:'张三',
age:12
})
function addSum(){
sum.value++
}
function changeName(){
person.value.name='李白'
}
function changeAge(){
person.value.age=22
}
function changePerson(){
person.value={name:'王维',age:55}
}
// 修改车
function changeCarBrand(){
car.brand='宝马'
}
function changeCarColor(){
car.options.color='黄色'
}
function changeCarEngine(){
car.options.engine='V9'
}
function changeCarWhole(){
car={
brand:'奥迪',
options:{
color:'紫色',
engine:'A100'
}
}
}
.app{
background-color: #ddd;
border-radius: 10px;
box-shadow: 0 0 10px;
padding: 10px;
}
button{
margin: 0 10px;
}
readOnly&shallowReadOnly
readOnly:复制一份数据,避免原数据被其他组件更改;原数据变化也会跟着变化
复制出来的这个数据不能修改自己
shallowReadOnly:复制出来的这分数据第一层不能修改,但其它层可以修改
sum为{{ sum }}
sum2为{{ sum2 }}
当前(car1)汽车为{{ car }}
当前(car2)汽车为{{ car2 }}
点我sum+1
点我sum2+1
修改品牌(car1)
修改颜色(car1)
修改价格(car1)
修改品牌(car2)
修改颜色(car2)
修改价格(car2)
import {ref,reactive,readonly,shallowReadonly} from 'vue'
let sum=ref(0)
// 根据可变的sum缔造出一个不可变sum
let sum2=readonly(sum)
let car=reactive(
{
brand:'宝马',
options:{
color:'red',
price:100
}
}
)
let car2=readonly(car)
function addSum(){
sum.value++
}
function addSu2m(){
sum2.value++ //只读无法使用
}
// 汽车
function changeCarBrand(){
car.brand='奥迪'
}
function changeCarColor(){
car.options.color='紫色'
}
function changeCarPrice(){
car.options.price=400
}
// car2
function changeCar2Brand(){
car2.brand='奥迪'
}
function changeCar2Color(){
car2.options.color='紫色'
}
function changeCar2Price(){
car2.options.price=400
}
button{
margin: 0 10px;
}
toRaw和MarkRaw
toRaw
let person2=toRaw(person)
lodash
MarkRaw
customRef
ref立即变化
customRef用于实现比如6秒后变化
{{ msg }}
import { TIMEOUT } from 'dns'
import {customRef, ref} from 'vue'
// 自带ref
// let msg=ref('你好')
// customRef要求必须有get set,用customRef来定义响应式数据
let initValue="你好"
let timer
// 接收底层所传track 跟踪,trigger触发
let msg=customRef((track,trigger)=>{
return{
// ,sg被读取时调用 因为其在{{ msg }} ,v-model="msg"两处被读取,所以初始化页面打印出现两次
get(){
track() //告诉vue 要对msg关注,一旦msg变化就去更新,如果没有管制就算有了triggger页面也不会更新
console.log('get')
return initValue
},
//msg修改时 调入修改值value
set(value){
clearTimeout(timer) //清除之前的定时器,因为页面每改一次就会调用setTimmeout
//msg改变后,要求等1秒页面再变化
timer=setTimeout(() => {
console.log('修改',value)
initValue=value
trigger() //通知vue一下数据变了,如果没有trigger vue不知道数据变化,很无聊
}, 1000)
}
}
})
button{
margin: 0 10px;
}
//hooks改造
import {customRef} from 'vue'
// initValue 初始值 delayTime:延迟时间
export default function(initValue:string,delayTime:number){
// customRef要求必须有get set,用customRef来定义响应式数据
// let initValue="你好"
let timer:number
// 接收底层所传track 跟踪,trigger触发
let msg=customRef((track,trigger)=>{
return{
// ,sg被读取时调用 因为其在{{ msg }} ,v-model="msg"两处被读取,所以初始化页面打印出现两次
get(){
track() //告诉vue 要对msg关注,一旦msg变化就去更新,如果没有管制就算有了triggger页面也不会更新
console.log('get')
return initValue
},
//msg修改时 调入修改值value
set(value){
clearTimeout(timer) //清除之前的定时器,因为页面每改一次就会调用setTimmeout
//msg改变后,要求等1秒页面再变化
timer=setTimeout(() => {
console.log('修改',value)
initValue=value
trigger() //通知vue一下数据变了,如果没有trigger vue不知道数据变化,很无聊
}, delayTime)
}
}
})
return {msg}
}
{{ msg }}
import useMsgRef from './useMsgRef'
// 使用useMsgRef定义响应式数据
let {msg}=useMsgRef('你好',2000)
button{
margin: 0 10px;
}
TelePort
传送:默认子组件在父组件里,但是子组件弹窗需要以视口定位
父组件
我是APP组件

import Model from './Model.vue';
.outer{
background-color: #ddd;
border-radius: 10px;
padding:5px;
box-shadow: 0 0 10px;
width: 400px;
height: 800px;
filter: saturate(200%);
}
img{
width: 270px;
}
弹窗组件
展示弹窗
展示弹窗
我是弹窗标题
我是弹窗内容
关闭弹窗
import {ref} from 'vue'
let isShow=ref(false)
.model{
width: 200px;
height: 150px;
background-color: skyblue;
border-radius: 10px;
padding: 5px;
box-shadow: 0 0 5px;
text-align: center;
/* 参考视口定位而不是父组件 */
position: fixed;
top:20px;
left: 50%;
margin-left: -100px;
}
加了传送 弹窗跑到了body下
Supense
异步任务导致子组件消失
改为
我是App
加载中
import { Suspense } from 'vue';
import Child from './Child.vue';
.app{
background-color: #ddd;
border-radius: 10px;
padding:10px;
box-shadow: 0 0 10px;
}
我是Child
当前求和为{{ sum }}
import {ref} from 'vue'
import acxios from 'axios'
import axios from 'axios'
let sum=ref(0)
let {data:{content}}=await axios.get("https://api.uomg.com/api/rand.qinghua?format=json")
console.log(content)
.child{
background-color: skyblue;
border-radius: 10px;
padding:10px;
box-shadow: 0 0 10px;
}





























































































































