创建项目
- 在命令行中执行命令
npm create vite@latest
✔ Project name: … vue3_vite4_study
✔ Select a framework: › Vue
✔ Select a variant: › JavaScript
Scaffolding project in /Users/laobai/TempProjects/vue3_vite4_study...
Done. Now run:
cd vue3_vite4_study
npm install
npm run dev
用WebStorm打开vue3_vite4_study目录
执行
npm install
安装安装vue-router
npm install --save vue-router
安装Pinia
npm install --save pinia
安装axios
npm install --save axios
在项目目录的src下新建views目录,创建Home.vue组件
<script setup> </script> <template> <div> Home </div> </template> <style scoped> </style>
在项目目录的src下新建router目录,创建index.js路由文件
import { createRouter,createWebHistory } from 'vue-router' import Home from '../views/Home.vue' const routes = [ { name:'Home', path:'/home', component:Home }, { path:'/', redirect:'/home' }, ] const router = createRouter({ history: createWebHistory(), routes, }) export default router
修改main.js
import { createApp } from 'vue' import App from './App.vue' import router from './router' import { createPinia } from 'pinia' // 导入pinia // const pinia = createPinia() const app = createApp(App) app.use(router) // 注册路由插件 app.use(createPinia()) // 注册pinia状态插件 app.mount("#app")
修改App.vue
<template> <div> <router-view></router-view> </div> </template> <script setup> </script> <style> *{ margin: 0; padding: 0; } </style>
执行
npm run dev
启动本地服务即可。项目目录结构如下图所示:
选项式和组合式
选项式(Vue-Options-API,VOA),官方已不再推荐,支持Vue2和Vue3;
<template>
<div @click="changeMsg">{{msg}}</div>
</template>
<script>
import {ref, defineComponent} from "vue"
export default defineComponent({
setup() {
const msg = ref("hello vue3");
const changeMsg = ()=> {
msg.value = "hello PLScript";
}
return {
msg,
changeMsg
}
}
})
</script>
组合式(Vue-Composition-API,VCA),官方推荐的方式,仅支持Vue3;
组合式分两种写法:
- 在
setup()
函数中写代码,然后进行return
; - 使用
<script setup>
语法糖来写;
更推荐<script setup>
语法糖形式来写,以下及之后的所有示例均使用这种方式。
<template>
<div @click="changeMsg">{{msg}}</div>
</template>
<script>
export default{
name:"App",
data(){
return{
msg:"hello vue3"
}
},
methods:{
changeMsg(){
this.msg = "hello AiLynn"
}
}
}
</script>
Vue模板语法与v-指令
便于以下内容的测试,只进行一个文件的导入及访问路由配置,每次只更新导入的文件名及对应的path。
import { createRouter, createWebHistory } from 'vue-router'
import NotFound from '../views/NotFound.vue'
import Home from '../views/Home.vue'
import example from '../views/01/01.vue'
const routes = [
{
name:'Home',
path:'/home',
component:Home
},
{
name:'Example'
path:'/1-1',
component:example
},
{
path:'/',
redirect:'/home'
},
{
path:"/:pathMatch(.*)*",
name:"NotFound",
component: NotFound
}
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router
插值语法
双花括号
<script setup>
let msg = 'PLScript'
</script>
<template>
<div>文本插值:{{msg}}</div>
</template>
<style scoped>
</style>
引入ref
实现响应式更新数据.(后续会详细复习响应式)
<script setup>
import { ref } from 'vue'
let msg = ref('PLScript')
setTimeout( ()=> {
msg.value = 'AILynn'
},2000)
</script>
<template>
<div>文本插值:{{ msg }}</div>
</template>
双花括号
<script setup>
import { ref } from 'vue'
let msg = ref('PLScript')
setTimeout( ()=> {
msg.value = 'AILynn'
},2000)
</script>
<template>
<div>文本插值:{{ msg }}</div>
<div :class="Math.random()>0.5?'red':'blue'">红色还是蓝色</div>
</template>
<style scoped>
.red {
color:red;
}
.blue {
color:blue;
}
</style>
插入HTML代码 v-html
<script setup>
import { ref } from 'vue'
let text = ref('<h1>你好,PLScript</h1>')
</script>
<template>
<div v-html="text"></div>
</template>
<style scoped>
</style>
属性绑定 v-bind
可以绑定单个属性,也可以绑定多个属性
属性绑定简写格式::属性='值'
<script setup>
import { ref } from 'vue'
let id = ref('AILynn')
let obj = ref({
id:'PLScript',
style:{'font-size':'20px'}
})
</script>
<template>
<div v-bind:id="id">绑定ID</div>
<div :id="id">简写绑定ID</div>
<div v-bind="obj">绑定多个属性</div>
</template>
<style scoped>
</style>
调用函数
<script setup>
const getColor = ()=> {
return Math.random()>0.5?'red':'blue'
}
</script>
<template>
<div :class="getColor()">红色还是蓝色</div>
</template>
<style scoped>
.red {
color:red;
}
.blue {
color:blue;
}
</style>
事件监听 v-on
v-on
监听绑定的事件,简写为@事件
<script setup>
const showLog = ()=>{
console.log("show log")
}
</script>
<template>
<button @click="showLog()">点我查看log</button>
</template>
<style scoped>
</style>
双向绑定 v-model
v-bind
是单向绑定,数据只能从js流向html,即script
->template
而v-model
是双向绑定,数据不但能从js流向html,还可以从html流向js,即script
<->template
<script setup>
import {ref} from "vue";
let name = ref("")
</script>
<template>
<input type="text" placeholder="请输入姓名" v-model="name">
<h3>姓名:{{name}}</h3>
</template>
<style scoped>
</style>
v-model
有三个修饰符:
.lazy
:当脱离焦点后再进行同步
<input type="text" placeholder="请输入姓名" v-model.lazy="name">
.number
:会将v-model
绑定的数据转成number
<input type="text" placeholder="请输入姓名" v-model.number="name">
.trim
:自动过滤用户的首尾的空白字符
<input type="text" placeholder="请输入姓名" v-model.trim="name">
条件渲染
v-if
,后面可以跟v-else
或v-else-if
形成多条件分支。
v-show
,与v-if
用法相同,区别是v-if
销毁创建的dom
,而v-show
只是更改display
样式。即v-if
的资源开销要比v-show
要大。
所以,v-show
适合频繁显示或隐藏的标签,而v-if
不适合频繁的显示或隐藏的标签。
<script setup>
import {ref} from "vue";
let name = ref(true)
let age = ref(true)
// const showName = ref(true)
</script>
<template>
<button @click="name=!name">show</button>
<button @click="age=!age">if</button>
<h3 v-show="name">PLScript</h3>
<h3 v-if="age">18</h3>
</template>
<style scoped>
</style>
v-if
后面跟着v-else
或v-else-if
可以实现分支效果
<script setup>
import {ref} from "vue";
let status = ref(true)
</script>
<template>
<button @click="status=!status">吃饭/消化</button>
<h3 v-if="status">吃饱了</h3>
<h3 v-else>饿了</h3>
</template>
<style scoped>
</style>
循环遍历 v-for
v-for
用于遍历列表或对象
语法格式:
v-for="item in items"
v-for="(item, index) in items"
v-for="(value, key) in object"
<script setup>
import {ref} from "vue";
let list = ref(["吃饭","睡觉","打豆豆"])
let obj = ref({
name:'AILynn',
age:18
})
</script>
<template>
<li v-for="todo in list">{{todo}}</li>
<div v-for="(todo,index) in list">{{ index }}-{{ todo }}</div>
<div v-for="(value,key,index) in obj">{{ index }}-{{ key }}-{{ value }}</div>
</template>
<style scoped>
</style>
v-pre
通过v-pre
修饰的元素,会进行原样输出,而不会按照Vue模板语法进行解析,比如{{js表达式}}
依然显示为,既不会作为插值语法,也不会进行表达式的运算
<script setup>
import {ref} from "vue";
let name = ref("AILynn")
</script>
<template>
<h3 v-pre>{{name}}</h3>
</template>
v-once
被v-once
修饰的元素,只渲染一次
<script setup>
import {ref} from "vue";
let name = ref("AILynn")
</script>
<template>
<input type="text" placeholder="请输入姓名" v-model.lazy="name">
<h3 v-once>姓名:{{name}}</h3>
</template>
响应式
reactive
在Vue组合式API中,使用reactive()
函数接收一个普通的对象,把对象数据转化为响应式对象并返回。响应式对象意味着当有数据发生变化时,会自动触发组件重新渲染。
<script setup>
import {ref,reactive} from "vue";
let name=ref('PLScript')
setTimeout(()=>{
// name = 'AILynn' // 这种写法不生效,需要.value才可以。
name.value = 'AILynn'
},2000)
let person1 = ref({
name:'PLScript',
age:18
})
person1.value.age++
let person2 = reactive({
name:'AILynn',
age:20
})
person2.age++
</script>
<template>
<h3>{{person1.name}}</h3>
<h3>{{person1.age +1}}</h3>
<h3>{{person2.name}}</h3>
<h3>{{person2.age +1}}</h3>
</template>
ref
ref()
是创建一个包装式对象,含有一个响应式属性value
。它和reactive()
的差别是,reactive()
没有包装属性value
。ref()
是在reactive()
基础上进行的二次封装。reative()
是将引用类型数据转换为响应式数据,而ref()
不仅能够将引用类型数据转换为响应式数据,还可以将基本类型转换为响应式数据,并可以获取DOM元素。
JavaScript的基本数据类型:Number
、String
、Boolean
、Null
、Undefined
、Symbol
;引用数据类型:Object
(除了基本数据类型以外的都是对象)
<script setup>
import {ref} from "vue";
let name=ref('PLScript')
setTimeout(()=>{
// name = 'AILynn' // 这种写法不生效,需要.value才可以。
name.value = 'AILynn'
},2000)
</script>
<template>
<h3>{{name}}</h3>
</template>
在script
中调用ref()
调用的对象是需要.value
的,而在template
中就不需要.value
,ref会自动解包(自动解包有个前提,只有顶级变量才可以自动解包)
<script setup>
import {ref} from "vue";
let name=ref('PLScript')
setTimeout(()=>{
// name = 'AILynn' // 这种写法不生效,需要.value才可以。
name.value = 'AILynn'
},2000)
let person1 = ref({
name:'PLScript',
age:18
})
person1.value.age++
</script>
<template>
<h3>{{person1.name}}</h3>
<h3>{{person1.age +1}}</h3>
</template>
计算属性和方法
计算属性和方法的区别:
- 计算属性在没有改变响应式变量的时候,只会计算一次,存入内存,下次取结果从内存中获取。当响应式变量发生改变,会再次触发重新计算这个结果。
- 方法每次都会重新计算,会造成性能损耗;
- 计算属性只是一个属性值,而方法调用时要加()括号。
<script setup>
import {computed} from "vue";
let a=10
let b=20
let sum = computed(()=>{
return a+b
})
let getSum = ()=>{
return a+b
}
</script>
<template>
<h3>{{ sum }}</h3>
<h3>{{ getSum() }}</h3>
</template>
事件处理
内联事件处理
内联事件处理就是在标签中直接写JS代码
<script setup>
import {ref} from "vue";
let age = ref('18')
</script>
<template>
<button v-on:click="age++">点我加1岁</button>
<h3>年龄:{{age}}</h3>
</template>
方法事件处理
<script setup>
import {ref} from "vue";
let age = ref('18')
const plClick = (index)=> {
if (index===3) {
console.log('PLScript')
} else {
console.log(index)
}
}
</script>
<template>
<button v-on:click="age++">点我加1岁</button>
<h3>年龄:{{age}}</h3>
<!-- 传入参数的方法 -->
<div v-for="(item, index) in 5" @click="plClick(index)">{{index}}</div>
</template>
<style scoped>
</style>
在内联事件处理器中访问事件参数
有时需要在内联处理器中访问原生DOM事件,可以向处理器方法传入一个特殊的$event
变量或者使用内联箭头函数来实现。
<script setup>
import {ref} from "vue";
let age = ref('18')
const plClick = (index)=> {
if (index===3) {
console.log('PLScript')
} else {
console.log(index)
}
}
const plWarn = (message, event)=>{
if (event) {
event.preventDefault()
}
alert(message)
}
</script>
<template>
<button v-on:click="age++">点我加1岁</button>
<h3>年龄:{{age}}</h3>
<!-- 传入参数的方法 -->
<div v-for="(item, index) in 5" @click="plClick(index)">{{index}}</div>
<!-- 使用特殊的 $event 变量 -->
<button @click="plWarn('Form cannot be submitted yet.', $event)">Submit</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => plWarn('Form cannot be submitted yet.', event)">Submit</button>
</template>
表单数据双向绑定
文本、多行文本、单选框、复选框及下拉选择器的双向绑定演示
<script setup xmlns="http://www.w3.org/1999/html">
import {ref} from "vue";
let f = ref({
form: {
hobby:"",
hobbies:{},
places:[],
placesChecked:[false,false,false,false]
}
})
let addOrRemove = (e,name) => {
// 由于使用的是ref,所以访问对象属性需要加.value
console.log(f.value)
if (e.target.checked) {
if (f.value.form.places.findIndex(e => e == name) == -1) {
f.value.form.places.push(name)
}
} else {
f.value.form.places.splice(f.value.form.places.findIndex(e => e == name),1)
}
}
</script>
<template>
<form method="post" action="http://localhost:8080">
<!-- 文本 -->
<p>账号名是:{{f.form.username}}</p>
<input v-model="f.form.username" placeholder="请输入用户名">
<!-- 多行文本 -->
<br>
<div style="height: 50px"></div>
<p>个性签名是:{{f.form.sign}}</p>
<textarea v-model="f.form.sign" placeholder="个性签名"></textarea>
<!-- 单选框 -->
<br>
<div style="height: 50px"></div>
<label for="checkbox">你最爱的是:{{f.form.hobby}}</label>
《三国演义》
<input type="radio" id="one" name="hobby" v-model="f.form.hobby" value="sanguo">
《西游记》
<input type="radio" id="two" name="hobby" v-model="f.form.hobby" value="xiyou">
《水浒传》
<input type="radio" id="three" name="hobby" v-model="f.form.hobby" value="shuihu">
《红楼梦》
<input type="radio" id="four" name="hobby" v-model="f.form.hobby" value="honglou">
<!-- 复选框 -->
<br>
<div style="height: 50px"></div>
<label for="checkbox">你喜爱的是【多选】:{{f.form.hobbies}}</label>
《三国演义》
<input type="checkbox" id="one" name="sanguo" v-model="f.form.hobbies['sanguo']">
《西游记》
<input type="checkbox" id="two" name="xiyou" v-model="f.form.hobbies['xiyou']">
《水浒传》
<input type="checkbox" id="three" name="shuihu" v-model="f.form.hobbies['shuihu']">
《红楼梦》
<input type="checkbox" id="four" name="honglou" v-model="f.form.hobbies['honglou']">
<br>
<div style="height: 50px"></div>
<label for="checkbox">你去过的地方是【多选】:{{f.form.places}}</label>
北京
<input type="checkbox" id="one" @change="addOrRemove($event,'北京')" v-model="f.form.placesChecked[0]">
上海
<input type="checkbox" id="two" @change="addOrRemove($event,'上海')" v-model="f.form.placesChecked[1]">
天津
<input type="checkbox" id="three" @change="addOrRemove($event,'天津')" v-model="f.form.placesChecked[2]">
重庆
<input type="checkbox" id="four" @change="addOrRemove($event,'重庆')" v-model="f.form.placesChecked[3]">
<!-- 下拉选择器 -->
<br>
<div style="height: 50px"></div>
<div>Selected:{{f.form.selected}}</div>
<select v-model="f.form.selected">
<option disabled value="">请选择一个</option>
<option>北京</option>
<option>上海</option>
<option>深圳</option>
</select>
</form>
<button type="submit" @click.prevent="console.log(f.form)">提交</button>
</template>

生命周期
组合式API
<script setup>
import {onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref} from "vue";
let person = ref({
name:'AILynn'
})
const modifyPersonName = ()=>{
// 使用的是ref,所以需要访问person.value
person.value.name='PLScript'
}
onBeforeMount(()=>{
console.log('1.数据挂载之前触发,可以去后端查询数据,准备挂载')
})
onMounted(()=>{
console.log('2.数据挂载之后触发')
})
onBeforeUpdate(()=>{
console.log('3.数据修改之前触发')
})
onUpdated(()=>{
console.log('4.数据修改之后触发')
})
onBeforeUnmount(()=>{
console.log('5.数据卸载之前触发')
})
onUnmounted(()=>{
console.log('6.数据卸载完成之后触发')
})
</script>
<template>
<h3>{{person.name}}</h3>
<button @click="modifyPersonName()">改名字</button>
</template>
<style scoped>
</style>
选项式API
<script>
export default {
data(){
return {
name:'AILynn'
}
},
methods:{
modifyPersonName(){
this.name='PLScript'
}
},
beforeCreate() {
console.log('1.页面创建之前触发')
},
created() {
console.log('2.页面创建之后触发')
},
beforeMount() {
console.log('3.数据挂载之前触发')
},
mounted() {
console.log('4.数据挂载之后触发')
},
beforeUpdate() {
console.log('5.数据修改之前触发')
},
updated() {
console.log('6.数据修改之后触发')
},
beforeUnmount() {
console.log('7.数据卸载之前触发')
},
unmounted() {
console.log('8.数据卸载完成之后触发')
}
}
</script>
<template>
<h3>{{name}}</h3>
<button @click="modifyPersonName">改名字</button>
</template>
监听器watch
当一个响应式的变量,值发生变化后,会触发监听器,从而可以根据这个值的变化,去做其他事件处理。
<script setup>
import {ref,watch} from "vue";
let person = ref({
name:'AILynn',
age:'16',
marry:false
})
const modifyPersonAge = ()=>{
// 使用的是ref,所以需要访问person.value
person.value.age='22'
}
watch(()=> person.value.age, (newValue,oldValue)=>{
console.log('person.age变化之后的值:',newValue)
console.log('person.age变化之前的值:',oldValue)
if(newValue >= 20){
person.value.marry=true
}
})
</script>
<template>
<h3>{{person.name}}</h3>
<button @click="modifyPersonAge()">长大了</button>
<h3>{{person.name}}是否可以结婚:{{person.marry}}</h3>
</template>
组件
组件的定义
组件是Vue的一大特色,每个页面都是由一个或多个组件构成的。组件时可复用的Vue实例,可以把页面中在多个场景中重复使用的部分,抽出为一个组件进行复用。并且还可以给组件传递不同的数据和方法,让组件有不一样的效果。
定义组件的属性
新建组件demo1.vue
<script setup>
// 在子组件中可以使用defineProps声明该组件需要接收的props,
// 需要传递一个包含props字段的对象,每个字段表示该props的默认值和类型等信息。
const props = defineProps({
// 只指定类型时
// title:String
// 即指定类型又指定默认值
title:{
type:String,
default:'PLScript'
}
})
</script>
<template>
<h3>{{props.title}}</h3>
</template>
<style scoped>
</style>
在组件01.vue中引入demo01.vue,来实现在01页面上显示并修改demo01组件的内容
<script setup>
import cps1 from '../../components/demo01.vue'
import {ref} from "vue";
const a = ref({
title:'除去巫山不是云'
})
</script>
<template>
<h3>组件的使用</h3>
<cps1></cps1>
<cps1 :title="'曾经沧海难为水'"></cps1>
<cps1 :title=a.title></cps1>
</template>
<style scoped>
</style>
demo01.vue
<script setup>
// 在子组件中可以使用defineProps声明该组件需要接收的props,
// 需要传递一个包含props字段的对象,每个字段表示该props的默认值和类型等信息。
const props = defineProps({
// 只指定类型时
// title:String
// 即指定类型又指定默认值
// 定义属性
title:{
type:String,
default:'海纳百川'
},
// 定义对象
user:{
type:Object,
default:()=>{
return {
name:'LaoBai',
age:18
}
}
},
// 定义数组
mobiles:{
type:Array
}
})
</script>
<template>
<h3>{{props.title}}</h3>
<h3>{{props.user.name}}已经{{props.user.age}}了,竟然是说{{props.title}}</h3>
<h3 v-for="mobile in props.mobiles">{{mobile.brand}}-{{mobile.price}}</h3>
</template>
<style scoped>
</style>
01.vue
<script setup>
import cps1 from '../../components/demo01.vue'
import {ref} from "vue";
const a = ref({
title:'除去巫山不是云'
})
const moblies=[{brand:'Apple',price:'5999'},{brand: 'SanSang',price:'7999'},{brand: 'HuaWei',price: '9999'}]
</script>
<template>
<h3>组件的使用</h3>
<cps1></cps1>
<cps1 :title="'曾经沧海难为水'"></cps1>
<cps1 :title=a.title></cps1>
<cps1 :title="'生而无畏'" :user="{name:'AiLynn',age:28}"></cps1>
<cps1 :mobiles="moblies"></cps1>
</template>
父子组件传参与插槽
父组件向子组件传递参数
如果组件A中使用组件B,那么A叫做B的父组件,B叫做A的子组件。
上面的例子中,01.vue是父组件,demo01.vue是01.vue的子组件。
父组件向子组件传递数据使用诸如下面的方式:
<cps1 :title="'曾经沧海难为水'"></cps1>
<cps1 :title=a.title></cps1>
<cps1 :title="'生而无畏'" :user="{name:'AiLynn',age:28}"></cps1>
<cps1 :mobiles="mobiles"></cps1>
子组件向父组件传递数据有两种方式:
1、事件发射;(推荐使用)
2、函数回调:父组件传一个回调函数给子组件,子组件要发送数据的时候,调用父组件的函数。
02.vue父组件
<script setup>
import Demo02Form from "../../components/demo02-form.vue";
// 获取子组件的form数据(配合事件发射方式)
const getFormData = (form)=> {
console.log("子组件传递过来的数据",form)
}
// 获取子组件的form数据 (配合回调函数方式)
const getChildComponentData = (form)=> {
console.log("子组件传递过来的数据,采用函数传递的方式",form)
}
</script>
<template>
<h3>子组件传递参数到父组件</h3>
<!-- 使用子组件默认值 -->
<!-- <demo02-form></demo02-form> -->
<!-- 在父组件重新给子组件赋值 -->
<demo02-form :title1="'手机号'" :title2="'验证码'" :input-name="'phone'" :getData="getChildComponentData" @submitForm="getFormData"></demo02-form>
</template>
<style scoped>
</style>
demo02-form.vue子组件
<script setup>
import {ref} from "vue";
// 接收父组件传过来的值,使用defineProps进行接收
const props = defineProps({
title1:{
type:String,
default:()=>{
return '用户名'
}
},
title2:{
type:String,
default:()=>{
return '密码'
}
},
inputName:{
type:String,
default:()=>{
return 'userName'
}
},
// 声明getData属性,用于子给父传递数据,配合回调函数方式
getData:{
type:Function,
default: ()=> {
return (form)=> {
return { form }
}
}
}
})
// 定义要从子组件返给父组件的事件集合,使用defineEmits (发射事件)
const emits = defineEmits(['submitForm'])
// 定义响应式变量formData,初始form为空。用于template中进行双向绑定
const formData = ref({
form:{}
})
const submit = ()=> {
console.log(formData.value.form)
// 触发发射事件
emits('submitForm',formData.value.form)
// 把值返回给父组件的调用函数,即将form返回给父组件
props.getData(formData.value.form)
}
</script>
<template>
<div>
{{props.title1}} <input :name="props.inputName" v-model="formData.form[props.inputName]">
</div>
<div>
{{props.title2}} <input name="password" v-model="formData.form.password">
</div>
<button type="button" @click="submit">提交</button>
</template>
<style scoped>
</style>
动态组件
使用component
标签可以渲染组件,is="组件名称"
。(使用的组件,需要import)
<component :is="组件的名称"></component>
自定义属性的数据双向绑定
子组件修改数据后,父组件的数据自动同步改变。而且自定义任何的属性都可以。
父组件03.vue
<script setup>
import Demo03 from "../../components/demo03.vue";
import {ref} from "vue";
import Demo04Form from "../../components/demo04-form.vue";
let person = ref({
name: 'AILynn',
form:{}
})
// setInterval(()=>{
// console.log(person.value.form)
// },2000)
</script>
<template>
<h1>自定义属性数据双向绑定</h1>
<demo03 v-model:name="person.name"></demo03>
<demo04-form v-model:form="person.form"></demo04-form>
</template>
<style scoped>
</style>
子组件demo03.vue
<script setup>
let props = defineProps(['name']) // 必须声明属性名称
let emits = defineEmits(['update:name']) // 事件发射必须是upadate:属性名称
let changeName = ()=>{
emits('update:name','laobai')
}
</script>
<template>
<h1>{{ props.name }}</h1>
<button @click="changeName">点击改名字</button>
</template>
<style scoped>
</style>
子组件demo04-form.vue
<script setup>
import {onMounted, ref, watch} from "vue";
let props = defineProps(['form'])
let emits =defineEmits(['update:form']) // 事件发射必须是upadate:属性名称
// 子绑父
// 方法一:事件监听watch
// 监听props的属性改变,赋值给当前的formData.value.form
// watch(()=> props.form, (newValue,oldValue)=>{
// console.log('新的值:',newValue)
// console.log('旧的值:',oldValue)
// formData.value.form=props.form
// })
// 方法二:声明周期 (推荐)
onMounted(()=>{
console.log('从父组件传过来的值:',props.form)
formData.value.form = props.form
})
let formData = ref({
form:{}
})
</script>
<template>
<div>
用户名:<input type="text" name="userName" v-model="formData.form.userName">
</div>
<div>
密码:<input type="password" name="password" v-model="formData.form.password">
</div>
<button type="button" @click="emits('update:form',formData.form)">提交</button>
</template>
<style scoped>
</style>
slot插槽
没有指明name的插槽为默认插槽,指定了name的插槽为具名插槽。若默认插槽有多个,同一个组件会插入多次。具名插槽在引用时需要再template中指定所引用的插槽名称<template #插槽名称>
,插槽名称前用#
子组件demo05-slot.vue
<script setup>
</script>
<template>
<div>
<slot name="header"></slot>
<h1>以上是header</h1>
<div style="border: solid red">
<slot name="content"></slot>
</div>
</div>
<h1>以下是footer</h1>
<div style="border: solid green">
<slot name="footer"></slot>
</div>
</template>
<style scoped>
</style>
父组件04.vue
<script setup>
import Demo05Slot from "../../components/demo05-slot.vue";
</script>
<template>
<demo05-slot>
<template #header>
<h1>在header中插入了内容</h1>
</template>
<template #content>
<h1>在content中插入了内容</h1>
</template>
<template #footer>
<h1>在footer中插入了内容</h1>
</template>
</demo05-slot>
</template>
<style scoped>
</style>
在子组件中定义属性内容,绑定到某个插槽上,父组件引用的时候可以同步展示
子组件中使用<slot :user="data.user" name="header"></slot>
来绑定到插槽
父组件在使用时使用v-slot='user'
来获取。需要注意的是,v-slot
是来获取没有名字的默认插槽的,具名插槽则直接在名称后面直接获取<template #header="user">
子组件demo05-slot.vue
<script setup>
import {ref} from "vue";
let data =ref({
user:{
name:'AILynn',
age:18,
phone:8006668888
}
})
</script>
<template>
<div>
<slot :user="data.user" name="header"></slot>
<h1>以上是header</h1>
<div style="border: solid red">
<slot :age="data.user.age" name="content"></slot>
</div>
</div>
<h1>以下是footer</h1>
<div style="border: solid green">
<slot name="footer"></slot>
</div>
</template>
<style scoped>
</style>
父组件04.vue
<script setup>
import Demo05Slot from "../../components/demo05-slot.vue";
</script>
<template>
<demo05-slot>
<template #header="user">
<h1>在header中插入了内容{{ user }}</h1>
</template>
<template #content="age">
<h1>在content中插入了内容{{ age }}</h1>
</template>
<template #footer>
<h1>在footer中插入了内容</h1>
</template>
</demo05-slot>
</template>
<style scoped>
</style>
provide提供和inject注入
上面举的例子都是父子间的数据传输,若涉及到爷孙等跨多级的数据传输可以使用provide
在最外层的组件中进行声明,使用inject
在内层的组件中进行使用。
provide
<script setup>
import { provide } from 'vue'
provide('message','hello!') // 两个参数:名称,值
</script>
inject
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
Vue-Router路由
Vue Router
是vue.js
的官方路由,在本小节开始的部分<创建项目>中已经进行了安装。
创建路由文件和main.js使用路由
在项目的src目录下新建router目录,在router目录下创建index.js路由文件,主要是配置路径及对应的页面。
import { createRouter,createWebHistory } from 'vue-router'
// 导入需要用到的组件
import NotFound from '../views/NotFound.vue'
import example from '../views/03/04.vue'
// 配置组件路由
const routes = [
{
name:'Example', // 路由名称
path:'/3-4', // 路径
component:example // 对应的组件
},
{
name:'Home',
path:'/home',
component: ()=> import('../views/Home.vue') // 懒加载的方式导入组件,即用到时才导入。推荐使用
},
{
path:'/', // 根节点
redirect:'/home'
},
{
path:"/:pathMatch(.*)*", // 未配置的路径
name:"NotFound",
component: NotFound
}
]
const router = createRouter({
// 使用基于浏览器history API的路由模式 (正常的URL,无#)
history: createWebHistory(),
routes,
})
export default router
在main.js中导入路由
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router) // 注册路由插件
app.mount("#app")
router-view和router-link
router-view
表示哪个地方需要显示其他子页面
router-link
表示要跳转到哪个页面,类似a标签
修改App.vue
<template>
<div style="display: flex">
<div style="border: solid red">
<div>
<router-link to="3-1">组件的定义与使用</router-link>
</div>
<div>
<router-link to="3-2">父子组件间的传递</router-link>
</div>
</div>
<div style="border: solid green;flex: 1">
<router-view></router-view>
</div>
</div>
</template>
其中,<router-view>
应用在需要显示子页面的页面中,而<router-link>
可以用于任何需要进行页面跳转的组件中,to=
指向要跳转的页面。
在路由文件index.js中新增如下代码:
{path: '/3-1', component:()=> import('../views/03/01.vue')},
{path: '/3-2', component:()=> import('../views/03/02.vue')},
路径传参和query传参
useRoute
和useRouter
// useRoute通常是用来接收路径传参、查询参数、判断当前的页面路由path等
let route = useRoute();
// useRouter可实现编程式(JS代码中通过router.push()方法完成页面跳转)导航
let router = useRouter();
01.vue
<script setup>
import {useRoute, useRouter} from "vue-router";
// useRoute通常是用来接收路径传参、查询参数、判断当前的页面路由path等
let route = useRoute();
console.log('route',route)
console.log('路径参数',route.params.userId)
console.log('查询参数',route.query)
// useRouter可实现编程式(JS代码中通过router.push()方法完成页面跳转)导航
let router = useRouter();
console.log('router',router)
</script>
<template>
<h1>这是路径传参+query传参的例子</h1>
</template>
App.vue
<template>
<div style="display: flex">
<div style="border: solid red">
<div>
<router-link to="/3-1">组件的定义与使用</router-link>
</div>
<div>
<router-link to="/3-2">父子组件间的传递</router-link>
</div>
<div>
<router-link to="/4-1/996">路径传参</router-link>
</div>
<div>
<button @click="router.push({path:'/4-1/996',query:{name:'laobai',age:18}})">路径传参+query传参</button>
</div>
</div>
<div style="border: solid green;flex: 1">
<router-view></router-view>
</div>
</div>
</template>
<script setup>
import router from "./router/index.js";
</script>
<style>
*{
margin: 0;
padding: 0;
}
</style>
router/index.js
{path: '/4-1/:userId', component:()=> import('../views/04/01.vue')},
嵌套路由
在上面的例子的基础上,增加一个页面,并在这个页面文件的同级目录新建一个目录存放孙页面
02.vue
<script setup>
</script>
<template>
<h1>这是子路由</h1>
<div>
<router-link to="/4-2-1">子页面1</router-link>
<router-link to="/4-2-2" style="margin-left: 20px">子页面2</router-link>
</div>
<div style="border: solid orange">
<router-view/>
</div>
</template>
<style scoped>
</style>
在02.vue同级目录新建子页面目录,在子页面目录中新建01.vue和02.vue两个孙页面
<script setup>
</script>
<template>
<h1>内嵌子页面1</h1>
</template>
<style scoped>
</style>
配置路由文件index.js,在4-2路由的内部增加childern子路由配置(可根据项目的实际情况增加多级子路由)。
{path: '/3-1', component:()=> import('../views/03/01.vue')},
{path: '/3-2', component:()=> import('../views/03/02.vue')},
{path: '/3-3', component:()=> import('../views/03/03.vue')},
{path: '/4-1/:userId', component:()=> import('../views/04/01.vue')},
{
path: '/4-2',
component:()=> import('../views/04/02.vue'),
children:[
{path: '/4-2-1',component:()=> import('../views/04/子页面/01.vue')},
{path: '/4-2-2',component:()=> import('../views/04/子页面/02.vue')},
]
},
页面预览效果如下:

路由全局前置、后置守卫
路由的前置后置守卫就是在跳转之前、之后可以做一些判断或处理
这里新建一个03.vue,个人中心页面,需要登录后才能访问。实现这个功能,这里使用router.beforeEach()
跳转之前拦截来实现。(全局拦截 beforeEach每次路由都进行拦截,to是要跳转的页面,from是从哪个页面跳过来的,next不执行就不会继续执行)
router.beforeEach((to, from, next) => { })
主要代码在路由文件index.js中
import { createRouter,createWebHistory } from 'vue-router'
import NotFound from '../views/NotFound.vue'
import Home from '../views/Home.vue'
import example from '../views/03/04.vue'
const routes = [
{
name:'Login',
path:'/login',
component:Login
},
{
name:'Home',
path:'/home',
component: ()=> import('../views/Home.vue')
},
{path: '/3-1', component:()=> import('../views/03/01.vue')},
{path: '/3-2', component:()=> import('../views/03/02.vue')},
{path: '/3-3', component:()=> import('../views/03/03.vue')},
{path: '/4-1/:userId', component:()=> import('../views/04/01.vue')},
{
path: '/4-2',
component:()=> import('../views/04/02.vue'),
children:[
{path: '/4-2-1',component:()=> import('../views/04/子页面/01.vue')},
{path: '/4-2-2',component:()=> import('../views/04/子页面/02.vue')},
]
},
{path: '/4-3', component:()=> import('../views/04/03.vue')},
{path: '/login', component:()=> import('../views/04/login.vue')},
{
name:'Example',
path:'/3-4',
component:example
},
{
path:'/',
redirect:'/home'
},
{
path:"/:pathMatch(.*)*",
name:"NotFound",
component: NotFound
}
]
const router = createRouter({
// 使用基于浏览器history API的路由模式 (正常的URL,无#)
history: createWebHistory(),
routes,
})
// beforeEach每次路由都进行拦截,to是要跳转的页面,from是从哪个页面跳过来的,next不执行就不会继续执行
router.beforeEach((to, from, next)=>{
if(to.path==='/4-3') { // 判断跳转的页面是否为/4-3 (这里判断的是路径,若路由进行了命名,也可以通过名称to.name进行判断)
next({path:'/login'}) // 是的话,跳转到登录页面,否则跳到else语句
}else next(); // next() 什么都不做,继续执行后面的语句
})
export default router
另,可以在路由配置中对单个路由添加元信息(meta:{}
)设置一些状态
{
path:"/4-4",
component: ()=> import('../views/04/04.vue'),
meta:{
requiredAuth:true, // 需要进行授权
keepalive:true // 需要缓存存活状态
}
}
这样配置后,上面对于是否需要登录的判断可以使用元信息中的requiredAuth
,例如:
router.beforeEach((to, from, next) =>{
let isAuthenticated = localStorage.getItem("token")
// 判断当前页面不是Login,并localStorage中不含有token,且meta的requiredAuth为True,跳转到登录页
if(to.name !== 'Login' && !isAuthenticated && to.meta.requiredAuth) next ({name:'Login'})
// 否则继续执行操作
else next()
})
实例:滚动条记录滚动位置
App.vue中的router-view
需要调整
<div style="border: solid green;flex: 1">
<router-view></router-view>
</div>
改为:
<div style="border: solid green;flex: 1">
<router-view v-slot="{Component}">
<keep-alive>
<component :is="Component" :key="route.path" v-if="route.meta.keepalive"/>
</keep-alive>
<component :is="Component" :key="route.path" v-if="!route.meta.keepalive"/>
</router-view>
</div>
动态组件来判断访问的组件,判断路由中元信息的keepalive
是否为true,true即进行状态保存,false不加任何处理继续执行。
新增05.vue示例滚动条页面
<script setup>
import {onActivated, ref} from "vue";
import {onBeforeRouteLeave} from "vue-router";
let scroll=ref(null) // 滚动条的初始化位置值
let scrollTop=ref(0) // 滚动条滚动后的位置值
onActivated(()=>{ // 再次进入组件时要做的处理
scroll.value.scrollTop = scrollTop.value
})
onBeforeRouteLeave(()=>{ // 离开这个组件时要做的处理
scrollTop.value = scroll.value.scrollTop
})
</script>
<template>
<div ref="scroll" style="background: rosybrown;height: 500px;overflow-y: auto;border: solid red">
<h1 v-for="i in 100">{{i}}</h1>
</div>
</template>
<style scoped>
</style>
两点需要注意:
- 在需要定节点上增加了
ref="scroll"
,ref若绑定在dom节点上,得到的就是原生dom节点;ref若绑定在组件上,得到的就是组件对象; - 应用到了组件内的两个钩子函数
onBeforeRouteLeave()
来实现离开组件时保存位置和onActivated()
来实现加载组件时赋值已经保存的位置信息
在路由文件index.js中增加新建页面的路由
{
path:"/4-5",
component: ()=> import('../views/04/05.vue'),
meta:{
keepalive:true // 需要缓存存活状态
}
}
Pinia状态管理
Pinia是Vue的专属状态管理库,它允许跨组件或页面共享状态。所谓状态管理,是把所有需要用到的相同响应式变量的组件的变量,存到内存中,然后在每个需要用到的组件里边使用,形成一个全局共享变量。
Vue2的状态管理使用Vuex,Vue3在继续支持Vuex的同时,还支持新开发了Pinia,当然Pinia也支持Vue2。
Pinia起始于2019年11月,Pinia其实就是Vuex的升级版本。根据官网的描述,Pinia将取代Vuex,即Pinia=Vuex5。
Pinia在本小节开始的部分<创建项目>中已经进行了安装。
在main.js中进行注册使用
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia' // 导入pinia
const app = createApp(App)
app.use(router) // 注册路由插件
app.use(createPinia()) // 注册pinia状态插件
app.mount("#app")
在项目的src目录下创建stores目录,这个目录中存放全部的状态js文件。新建购物车shopCart.js
import {defineStore} from "pinia";
import {computed, ref} from "vue";
// 通过defineStore()来定义状态
export const useShopCart = defineStore('shopCart', ()=>{
// 默认的购物车商品数量0
const count = ref(0);
// 定义商品的统一价格为10元,下面计算商品的总价格
const allPrice = computed(()=> count.value * 10);
// 增加商品
function increment(){
count.value++
}
// 减少商品
function reduce(){
count.value--
}
return {count, increment,reduce}
})
新建页面01.vue
<script setup>
import Pinia01 from "../../components/status/pinia01.vue";
import Pinia02 from "../../components/status/pinia02.vue";
import Pinia03 from "../../components/status/pinia03.vue";
</script>
<template>
<h1>Pinia状态管理</h1>
<Pinia01/>
<Pinia02/>
<Pinia03/>
</template>
<style scoped>
</style>
在src目录下的component目录中新建status目录,存放状态相关组件。新建三个组件pinia01.vue
<script setup>
import {useShopCart} from "../../stores/shopCart.js";
let shopCart = useShopCart() // 使用购物车状态存储
</script>
<template>
<h1>组件1-购物车中商品的数量:{{shopCart.count}}</h1>
<button @click="shopCart.increment()">增加商品</button>
<button @click="shopCart.reduce()">减少商品</button>
</template>
<style scoped>
</style>
最终预览效果如下:

Axios
使用Axios整合Vue来实现请求数据。(之前是用原生的ajax来请求数据,后面用jQuery的ajax,而在前后端分离的项目中,使用Axios请求数据)
Axios在本小节开始的部分<创建项目>中已经进行了安装。
Axios特性
- 从浏览器中创建XMLHttpRequests
- 从node.js创建http请求
- 支持Promise API (链式函数)
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防御XSRF(跨站请求伪造)
发送Get和Post请求
用flask写一个api演示请求接口
import json
from flask import Flask, request, make_response
from flask_cors import cross_origin
app = Flask(__name__)
@app.route('/get')
def pl_get():
name = request.args.get("name")
age = request.args.get("age")
response = make_response({"name": name, "age": age})
response.headers["Access-Control-Allow-Origin"] = "*"
return response
@app.route("/post", methods=["post"])
@cross_origin()
def pl_post():
request_data = json.loads(request.data)
name = request_data.get("name")
age = request_data.get("age")
response = make_response({"name": name, "age": age})
return response
if __name__ == '__main__':
app.run()
在新建的01.vue实现Get和Post请求
<script setup>
import axios from "axios";
let plGet = ()=> {
axios.get('http://127.0.0.1:5000/get?name=laobai&age=18').then(function (response) {
console.log(response);
})
}
let plPost = ()=> {
axios.post('http://127.0.0.1:5000/post',{
name:"laobai",
age:18
}).then(function (response) {
console.log(response);
})
}
</script>
<template>
<h1>Axios的例子</h1>
<button @click="plGet">Get请求</button>
<button @click="plPost">Post请求</button>
</template>
发送结果预览如下:

通过config参数发送请求
可以将url、method、data、parms等请求信息都放在axios方法中发送请求
<script setup>
import axios from "axios";
import {http} from "../../utils/RequestUtils.js";
let plGet = ()=> {
axios.get('http://127.0.0.1:5000/get?name=laobai&age=18').then(function (response) {
console.log(response);
})
}
let plPost = ()=> {
axios.post('http://127.0.0.1:5000/post',{
name:"laobai",
age:18
}).then(function (response) {
console.log(response);
})
}
let reqByConf = ()=> {
axios({
method:'POST',
url:'http://127.0.0.1:5000/post',
data:{
name:"laobai",
age:18
}
}).then(response => {
console.log(response)
})
}
</script>
<template>
<h1>Axios的例子</h1>
<button @click="plGet">Get请求</button>
<button @click="plPost">Post请求</button>
<button @click="reqByConf">通过config参数直接发送请求</button>
</template>
Axios的二次封装
axios提供了axios.create()
方法,可以进行axios的二次封装,可以对请求URL、超时时间等通用内容进行封装,可以将不同可归类的内容分别封装成多个新的axios。
RequestUtils.js
import axios from "axios";
export const plAxios = axios.create({
baseURL:"http://localhost:5000",
timeout:5000
})
01.vue
<script setup>
import axios from "axios";
import {plAxios} from "../../utils/RequestUtils.js";
let plGet = ()=> {
axios.get('http://127.0.0.1:5000/get?name=laobai&age=18').then(function (response) {
console.log(response);
})
}
let plPost = ()=> {
axios.post('http://127.0.0.1:5000/post',{
name:"laobai",
age:18
}).then(function (response) {
console.log(response);
})
}
let reqByConf = ()=> {
axios({
method:'POST',
url:'http://127.0.0.1:5000/post',
data:{
name:"laobai",
age:18
}
}).then(response => {
console.log(response)
})
}
let reqByHC = ()=> {
plAxios.post('/post',{
name:"laobai",
age:18
}).then(response => {
console.log(response)
})
}
</script>
<template>
<h1>Axios的例子</h1>
<button @click="plGet">Get请求</button>
<button @click="plPost">Post请求</button>
<button @click="reqByConf">通过config参数直接发送请求</button>
<button @click="reqByHC">使用自定义的Axios完成请求发送</button>
</template>
请求拦截器和响应拦截器
请求拦截器:发生在请求之前,一般被用来统一添加token等内容。处理完成后,需要return放行继续执行;
响应拦截器:对响应数据进行处理,如提取请求数据中的某个字段值等;
import {plAxios} from "../../utils/RequestUtils.js";
plAxios.interceptors.request.use(config => {
console.log("请求拦截器") // 发生在请求之前,一般被用来统一添加token等内容
return config // 对请求信息处理完成后进行放行
}, err => {
console.log("发生错误了")
return Promise.reject(err)
})
plAxios.interceptors.response.use(config => {
console.log("响应拦截器") // 对响应数据进行处理
}, err => {
console.log("发生错误了")
return Promise.reject(err)
})