一、计算属性
实时监听
data 中数据的变化,并return 一个计算后的新值
, 供组件渲染 DOM 时使用。可以被
模板结构
(插值、v-bind ) 或methods
方法使用。
- 但是在某些情况下,我们可能需要对数据进行一些转化后在显示,或者需要将多个数据结合起来进行显示,这时候我们可以使用计算属性。
实例1:
<div id="app">
<h2>{{getFullName()}}</h2>
<h2>{{fullName}}</h2>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
firstName: 'lin',
lastName: 'willen'
},
computed: {
fullName () {
return this.firstName + ' ' + this.lastName;
}
},
// 使用 methods: 每次都会调用方法
methods: {
getFullName () {
return this.firstName + ' ' + this.lastName;
}
}
})
</script>
特点:
- 定义的时候,要被定义为 “方法”。
- 在使用计算属性的时候,当普通的属性使用即可
- 实现了代码的复用,只要计算属性中依赖的数据源变化了,则计算属性会自动重新求值。
实例2:
<div id="app">
<h2>总价格:{{totalPrice}}</h2>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
books:[
{id: 1001, name: 'Unix编程艺术',price: 119},
{id: 1002, name: '代码大全',price: 105},
{id: 1003, name: '深入理解计算机原理',price: 99},
{id: 1004, name: '现代操作系统',price: 109}
]
},
computed: {
totalPrice () {
let totalPrice = 0;
for (let i in this.books) {
totalPrice += this.books[i].price;
}
// 也可以使用 for of
for (let book of this.books) {
totalPrice += book.price;
}
return totalPrice;
}
}
})
</script>
计算属性 vs 方法
- methods和computed看起来都可以实现我们的功能,那么为什么还要多一个计算属性
- methods: 每次使用都会调用方法
- computed: 计算属性会缓存计算的结果, 不变的情况下只调用一次, 除非原属性发生改变,才会重新调用.
计算属性 vs 侦听器
- 计算属性侧重于监听多个值的变化,最终计算并返回一个新值。
- 侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任何返回值。
二、Vue组件
什么是组件化开发 ?
根据封装的思想,把页面上可重用的 UI 结构封装为组件,方便项目的开发和维护。
vue 中的组件化开发
- vue 是一个支持组件化开发的前端框架。
- vue 中组件的后缀名是
.vue
组件的构成
每个 .vue 组件都由 3 部分构成,分别是:
- template :组件的模板结构,且每个组件中必须包含template模板结构。
- script : 组件的 JavaScript 行为
- style :组件的样式
// 标签上添加 lang=“less” 属性,即可使用 less 语法编写组件的样式
// scoped 防止样式冲突
script 节点:
(1)script 中的 name 节点
- 用来定义组件的名称,调试的时候可以清晰的区分每个组件。
(2)script 中的 data 节点
- 组件渲染期间需要用到的数据, data 必须是函数, 不能直接指向对象数据。
(3)script 中的 methods 节点
- 组件中的事件处理函数(方法),必须定义到 methods 节点中
注册私有组件
通过 components 注册的是私有子组件,被注册的组件只能用在当前组件中。
1.使用 import 语法导入需要的组件
import Left from '@/components/Left.vue'
2.在 script 标签中使用 components
节点注册组件
<script>
export default {
comments:{
Left
}
}
</script>
3.以标签的形式
使用刚才注册的组件
<template>
<Left></Left>
</template>
注册全局组件
在 vue 项目的
main.js
入口文件中,通过Vue.component()
方法,可以注册全局组件。
import Vue from 'vue'
import App from './App.vue'
// 导入需要被全局注册的那个组件
import Count from '@/components/Count.vue'
// 参数一: 组件的 '注册名称',将来以标签形式使用时要求和这个名称一样。
// 参数二: 需要被全局注册的那个组件。
Vue.component('MyCount', Count)
// 消息提示的环境配置,设置为开发环境或者生产环境
Vue.config.productionTip = false
new Vue({
// render 函数中,渲染的是哪个 .vue 组件,那么这个组件就叫做 “根组件”
render: h => h(App)
}).$mount('#app')
使用:
<template>
// 这里需要注意 '注册名称'
<MyCount ></MyCount>
</template>
组件注册时名称的大小写
在 Vue 定义组件注册名称的方式有两种:
1.短横线命名法,例如 my-swiper 和 my-search
- 使用组件时也必须使用短横线命名
2.驼峰命名法,例如 MySwiper 和 MySearch
- 既可以按照大驼峰命名法使用,也可以转化为短横线名称进行使用。
注意: 在开发中,推荐使用驼峰命名法为组件注册名称,因为它的适用性更强。
组件之间的样式冲突问题
默认情况下,写在 .vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。
scoped 属性
让当前组件的样式对其子组件是不生效。
Vue 中提供了在style 节点添加 scoped
属性,来防止样式冲突问题:
- 原理是为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域。
<template>
<div class="container" data-v-001>
<h3 data-v-001 > 轮播图组件件</h3>
</div>
</template>
<style>
// 通过中括号'属性选择器',防止样式冲突问题
// 因为每个组件分配的自定义属性是'唯一的'
. container[data-v-001]{
border: 1px solid red;
}
</style>
为了提高开发效率和开发体验,直接在 style 节点使用 scoped 属性:
<style lang="less" scoped>
</style>
/deep/ 样式穿透
让某些样 式对子组件生效。
使用场景: 当使用第三方组件库的时候,需要修改第三方组件默认样式的时候。
<style lang="less" scoped>
/*不加 /deep/ 时,生成的选择器格式为 .title[data-v-052242de]*/
.title{
color: blue;
}
/*加/deep/ 时,生成的选择器格式为 [data-v-052242de] .title*/
/deep/ .title {
color: pink;
}
</style>
Class 与 Style 绑定
通过
v-bind
动态操作元素样式。
1. 动态绑定 HTML 的 class:
通过三元表达式,动态的为元素绑定 class 的类名:
<h3 class="thin" :class="isItalic ?'italic':''">MyDeep 组件</h3>
<button @click="isItalic=!lisItalic"> Toggle Italic </button>
data(){
return { isItalic:true }
.thin{
font-weight:200;
.italic{
font-style:italic;
}
2. 以数组语法绑定 HTML 的 class
如果元素需要动态绑定多个 class 的类名,此时可以使用数组的语法格式
<h3 class="thin":class="[isItalic? 'italic': '',isDelete? 'delete':'']">MyDeep组件</h3>
<button @click="isItalic= !isItalic"> 字体变细 </button>
<button @click="isDelete= !isDelete"> 添加删除线 e</button>
<script>
export default {
data () {
return {
isItalic: true,
isDelete: false,
}
}
}
</script>
3. 以对象语法绑定 HTML 的 class:
<h3 class="thin":class="classObj">MyDeep组件</h3>
<button @click="classObj.isItalic = !classObjisItalic"> 字体变细 </button>
<button @click="classObj.isDelete = !classObj.isDelete"> 添加删除线 e</button>
<script>
export default {
data () {
return {
classObj:{
isItalic: true,
isDelete: false,
}
}
}
}
</script>
4. 以对象语法绑定内联的 style
命名可以用驼峰式
或短横线分隔
(记得用引号括起来) 来命名:
<div :style="{color:active, fontSize: fsize +'px','background-color': bgcolor}">
Hello world!!
</div>
<button @click="fsize += 1">字号+1</button>
<button @click="fsize -= 1">字号-1</button>
data () {
return {
active: 'red',
fsize: 30,
bgcolor: 'pink'
}
}
自定义属性 props
props
是组件的自定义属性,允许使用者通过自定义属性,为当前组件指定初始值,极大的提高组件的复用性。
在组件中声明 prpos
my-article 组件的定义如下:
<template>
<h3>标题:{{title}}</h3>
<h5>作者:{{author}}</h5>
</template>
父组件传递给my-article
组件的数据,必须在props
节点中声明 :
<script>
export default {
props:['title','author'],
</script>
无法使用未声明的 props
如果父组件给子组件
传递了未声明的 props 属性
,则这些属性会被忽略,无法被子组件使用。
动态绑定 props 的值
使用 v-bind 属性绑定的形式,可以为组件动态绑定 props 的值。
<!--通过V-bind属性绑定,为author动态赋予一个表达式的值
<my-article :title="info.title" :author="'post by'+info.author"></my-article>
props 的大小写命名
组件中如果使用“camelCase (驼峰命名法)
”声明了 props 属性的名称,则有两种方式为其绑定属性的值:
<script>
export default {
props:['pubTime'], // 使用'驼峰命名'法为当前组件声明 pubTime 属性
</script>
使用时既可以用驼峰命名
,亦可以用短横线分隔命名
的形式为组件绑定属性的值 :
<my-article pubTime="2021" ></my-article>
// 等价于
<my-article pub-time="2021" ></my-article>
props 验证
- 基础的类型检查
type
props:{ //支持的8种基础类型 propA:String, //字符串类型 propB:Number, //数字类型 propC:Boolean, //布尔值类型 propD:Array, //数组类型 propE:Object, //对象类型 propF:Date, //日期类型 propG:Function, //函数类型 propH:Symbol //符号类型 }
-
多个可能的类型
-
必填项校验
required
-
属性默认值
default
props:{ // 通过数组形式,为当前属性定义多个可能的类型 type: [String, Number], required: true, default: 0, }
自定义验证函数
在封装组件时,可以为 prop 属性指定自定义的验证函数,从而对 prop 属性的值进行更加精确的控制:
props:{
type:{
// 通过 validataor函数,对type 属性进行校验,属性值 通过val形参接收
validataor(val){
// 必须匹配下列字符串中的一个
return ['success','warning','danger'].indexOf(val) !== -1
}
}
}
prpos 传入不同的初始值
在实际开发中我们经常会碰到下面的情况:
- 在不同组件
使用同一个注册的组件
时候希望赋值一个不同的初始值
。
1.组件的封装者通过 props 允许使用者自定义初始值:
<template>
<div>
<h5>Count是全局组件,将被Left 和 Right组件使用</h5>
<p>count 的值是:{{ init }}</p>
<button @click="count += 1">+1</button>
</div>
</template>
<script>
export default {
// props: ['init'],
props: {
// 自定义属性的名字,是封装者自定义的(只要名称合法即可)
init: {
// 如果外界使用 Count 组件的时候,没有传递 init 属性,则默认值生效
default: 0,
// 指定值类型必须是 Number 数字
type: Number,
// 必填项校验(表示必须传入值)
required: true
}
},
data() {
return {
//props是只读的且不可修改
// 要想修改 props 的值,可以把 props 的值转存到 data中
count: this.init
}
}
}
2.组件的使用者通过属性节点传入初始值
Left 组件、Right 组件:
<template>
<div class="left-container">
<h3>Left 组件</h3>
<hr />
<MyCount :init="9"></MyCount>
</div>
</template>
<template>
<div class="right-container">
<h3>Right 组件</h3>
<hr />
<MyCount :init="6"></MyCount>
</div>
</template>
props 里传参注意点
-
props 可以通过
[]
给数据定义多个可能的数据类型 ; -
props 传入
Object
默认值必须是一个fn
props: { commCount: { type: String, default: ‘’ }, pubdate: { // 通过数组形式,为当前属性定义多个可能的类型 type: [String, Number], default: ‘’ }, cover: { type: Object, // 通过 default 函数,返回 cover 的默认值 default: function() { // 这个 return 的对象就是 cover 属性的默认值 return { cover: 0 } } } }
- props 是
只读的
,想修改 props 的值,可以把 props 的值转存到 data 中。 - props 的三个属性值:default、type、required。
三、自定义事件
封装组件时,为了让组件的使用者
可以监听到组件内状态的变化
,此时需要用到组件的自定义事件
。
vue2 中自定义事件的 3 个使用步骤:
-
在封装组件时 (子组件):
触发
自定义事件 -
在使用组件(父组件)时:
监听
自定义事件
触发自定义事件
// 子组件
<button @click="onBtnClick"> +1 </button>
<script>
export default {
data() {
// 子组件自己的数据,将来希望把 count 值传给父组件
return { count: 0 }
},
methods: {
onBtnClick() {
this.count t= 1
//修改数据时,通过 $emit()触发自定义事件
// 当点击 '+1' 按钮时 调用this.$emit 触发自定义的 numchange 事件
this.$emit( 'numchange')}
}
}
</script>
监听自定义事件
// 父组件
<Son @numchange="getNewCount"></Son>
methods : {
getNewCount(val) {
console.log('监听到了 count 值的变化', val)
},
在调用 this.$emit() 方法触发自定义事件时,可以通过第 2 个参数为自定义事件传参:
methods: {
onBtnClick() {
this.count t= 1
this.$emit( 'numchange' , this.count)} // 触发自定义事件,通过第二个参数传参
}
四、filter 过滤器
在 vue 3.x 的版本中剔除了过滤器
相关的功能。
在 vue 3.x 使用计算属性或方法代替被剔除的过滤器功能。
过滤器(Filters)常用于文本的格式化。过滤器可以用在两个地方:
双括号插值表达式
和v-bind
属性绑定。
<!-- 在双花括号中通过 | 调用capitalize过滤器,对message值进行格式化 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
私有过滤器
在 filters 节点下定义的过滤器,称为“私有过滤器”,因为它只能在当前 vm 实例所控制的 el 区域内使用。
创建一个私有过滤器,示例代码如下:
const vm = new Vue({
el: '#app',
data: {
message: 'hello world!',
info: 'title info'
},
// 在 filters 节点下定义过滤器
filters: {
// 把首字母转换为大写的过滤器
capitalize (value) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
全局过滤器
如果希望在多个 vue 实例之间共享过滤器,则可以按照如下的格式定义全局过滤器:
// Vue.filters()方法接收两个参数:
// 第一个参数:过滤器名字 第二个参数:过滤器的处理函数
Vue.filter('capitalize',(str)=>{
return value.charAt(0).toUpperCase() + value.slice(1)'
} )
调用多个过滤器:
{{ message | filterA | filterB }}
- 先把 message 的值交给 filterA 处理,再把 filterA 处理结果交给 filterB 进行处理,最终把 filterB 的处理结果,作为最终值渲染到页面上。
过滤器传参
过滤器是 JavaScript 函数,因此可以接收参数:
{{ message | filterA('arg1', arg2) }}
// 第一个参数永远是 '管道符' 前面待处理的值,第二个参数开始才是调用过滤器时传递的参数
Vue.filter('filterA',(msg,arg1,arg2)=>{
// 过滤器的逻辑代码
})
这里,filterA
被定义为接收三个参数的过滤器函数。其中 message
的值作为第一个参数,普通字符串 'arg1'
作为第二个参数,表达式 arg2
的值作为第三个参数。
<p>{{message | cap | maxl(5)}}</p>
Vue.filter('cap', (str) => {
return str.charAt(0).toUpperCase() + str.slice(1) + '----'
})
Vue.filter('maxl', (str, len = 10) => {
if (str.length <= len) return str
return str.slice(0, len) + '....'
})
var app = new Vue({
el: '#app',
data: {
message: 'hello Vue 2021年10月30日00:07:45!'
}
过滤器的注意点
- 在过滤器函数中,一定要有 return 值
- 在过滤器的形参中,可以获取到“管道符”前面待处理的那个值
- 如果全局过滤器和私有过滤器名字一致,此时按照“就近原则”,调用的是”私有过滤器“
label的for属性
label的for属性
<input type="checkbox" :id="'cb' + item.id" v-model="item.status">
<label :for="'cb' + item.id" v-if="item.status">已启用</label>
<label :for="'cb' + item.id" v-else>已禁用</label>
五、watch 侦听器
watch 侦听器 监视数据的变化,从而针对数据的变化做特定的操作。
侦听器的格式
1.方法格式的侦听器
- 无法在刚进入页面的时候,自动触发!!
- 如果侦听的是一个对象,如果对象中的属性发生了变化,不会触发侦听器!
2.对象格式的侦听器
- 可以通过 immediate 选项,让侦听器自动触发!
- 可以通过 deep 选项,让侦听器深度监听对象中每个属性的变化!
使用方法格式创建的侦听器:
监听 username 值的变化,并使用 axios 发起 Ajax 请求,检测当前输入的用户名是否可用:
import axios from 'axios'
export default{
data(){
return{ username: ''}
}
},
watch: {
// newVal 是'变化后的新值',oldVal 是'变化之前的旧值'
async username(newVal,oldVal) {
if (newVal === '') return
// 使用 axios 发起请求,判断用户名是否可用
const { data: res } = await axios.get(`https://www.escook.cn/api/finduser/${newVal}` )
console.log(res)
}
}
使用方法创建时:组件在初次加载完毕后不会调用 watch 侦听器。
immediate 选项
如果想让 watch 侦听器在浏览器打开时立即被调用,则需要使 用
immediate
选项。
使用对象格式创建的侦听器:
watch: {
username: {
// handler 是固定写法,表示当 username 的值变化时,自动调用 handler 处理函数
async handler(newVal,oldVal) {
if (newVal === '') return
const { data: res } = await axios.get('https://www.escook.cn/api/finduser/' + newVal)
console.log(res)
},
// 表示页面初次渲染好之后,就立即触发当前的 watch 侦听器
immediate: true
}
}
- 使用
handler
定义侦听器函数 immediate
控制侦听器是否自动触发, 默认值为false
不立即触发。
deep 选项
如果 watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 开启 深度监听。
<input type="text" v-model.trim="username"/>
data: {
info: {username: 'admin'}
},
watch:{
// 监听info对象的变化
info:{
handler(newVal){
console.log(newVal.username)
},
// 开启深度监听,监听每个属性值的变化,默认值为 false
deep: true
}
}
监听对象单个属性的变化
如果只想监听对象中单个属性的变化,则可以按照如下的方式定义 watch 侦听器。
const vm = new Vue({
el: '#app',
data: {
info: {username: 'admin',}
},
watch:{
'info.username':{
handler(newVal){
console.log(newVal)
},
}
}
})
六、组件的生命周期
生命周期
(Life Cycle)是指一个组件从创建 -> 运行 -> 销毁的整个阶段,强调的是一个时间段。生命周期函数
:是由 vue 框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行。
生命周期
强调的是时间段,生命周期函数
强调的是时间点。
需要注意的三个周期函数:
在实际开发中,
created
是最常用的生命周期函数 !
七、组件间的数据共享(组件传值)
父→子
父组件通过
v-bind
属性绑定向子组件共享数据,子组件使用props
接收数据。
// 父组件
// 注册子组件
<Son :msg="message" :user="userinfo"></Son>
<script>
// 导入子组件
import Son from '@component/Son.vue'
export default {
data:{
return{
message:'hello vue.js'
userinfo:{
name: 'lilei',age: 21
}
}
},
components: {Son}
}
</script>
// 子组件
<template>
<div>
<h5>Son组件</h5>
<p>父组件传递过来的 msg值是: {{ msg }</p>
<p>父组件传递过来的user值是: {{ user }}</p>
</div>
</template>
export default {
props: [ 'msg' , 'user ']
}
子 → 父
子组件向父组件共享数据使用自定义事件。
// 子组件
export default {
data() {
// 子组件自己的数据,将来希望把 count 值传给父组件
return { count: 0 }
},
methods: {
add() {
this.count t= 1
//修改数据时,通过 $emit()触发自定义事件
this.$emit( 'numchange' , this.count)}
}
}
// 父组件
<h1>App 根组件</h1>
<h3>子组件传过来的数据是 : {{ countFromSon }}</h3>
<Son @numchange="getNewCount"> </Son>
<script>
import Son from '@component/Son.vue'
export default {
data() {
return { countFromSon: 0 }
},
methods : {
getNewCount(val) {
console.log('numchange 事件被触发了!', val)
this.countFromSon = val
},
components: {
Son
}
}
}
</script>
兄弟组件数据共享
在 vue2.x 中,兄弟组件之间数据共享的方案是
EventBus
。
EventBus 的使用步骤:
- 创建 eventBus.js模块,并向外共享一个 Vue 的实例对象
- 在数据发送方,调用 bus.$emit('事件名称', 要发送的数据)方法触发自定义事件 。
- 在数据接收方,调用 bus.$on('事件名称', 事件处理函数)方法注册一个自定义事件。
八、ref 引用 操作DOM
不依赖于 jQuery 和调用 DOM API 的情况下,获取
DOM 元素
或组件的引用
。
每个 vue 的组件实例上,都包含一个 $refs 对象
,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象。
使用 ref 引用 DOM 元素
<!--使用ref属性,为对应的DOM添加引用名称-->
<h3 ref="myh3">MyRef 组件</h3>
<button@click="getRef">获取$refs 引用</button>
methods:{
getRef(){
//通过this.$refs.引用的名称可以获取到DOM元素的引用
console.log(this.$refs.myh3)
//操作DOM元素,把文本颜色改为红色
this.$refs.myh3.style.color='red'
},
使用 ref 引用组件实例
需求: 在根组件控制子组件
<!--使用ref属性,为对应的“组件”添加引用名称-->
<my-counter ref="counterRef"> </my-counter>
<button @click="getRef"> 获取$refs 引用 </button>
methods:{
getRef(){
// 通过this.$refs.引用的名称可以引用组件的实例
console.log(this.$refs.counterRef)
// 引用到组件的实例之后,就可以调用 子组件上的 methods 方法
this.$refs.counterRef.add()
},
点击文本框自动获得焦点
添加 ref 引用,并调用原生 DOM 对象的.focus() 方法即可。
this.$nextTick(cb) 方法
$nextTick(cb)保证 cb 回调函数可以操作到最新的 DOM 元素(推迟到下一个 DOM 更新周期之后执行)。
- 解决我们在页面没有渲染完成前使用 ref 操作DOM元素报错问题。
控制文本框和按钮的按需切换
点击按钮展示文本框,文本框输入时隐藏按钮:
<template>
<input type="text" v-if="inputVisible" ref="ipt">
<button v-else @click="showInput">展示input输入框</button>
</template>
<script>
export default{
data(){
return{
//控制文本框和按钮的按需切换
inputVisible:false,
},
methods:{
showInput(){
//切换布尔值,显示文本框
this.inputVisible=true
//获取文本框的DOM引用,并调用.focus()使其自动获得焦点
// this.$refs.ipt.focus() 错误,此时页面未渲染完毕,无法获取文本框
//把对input文本框的操作,推迟到下次DOM更新之后。否则页面上根本不存在文本框元素
this.$nextTick(() =>{
this.$refs.ipt.focus()
})
}
},
</script>
九、动态组件
实现不同组件之间的按需展示。( 动态切换组件显示和隐藏 ) ,类似于
Vue-Router
。
动态组件的基本使用
Vue 提供了一个内置的 <component>组件,专门用来实现动态组件的渲染 :
- component 标签是 vue 内置的,作用:组件的占位符
- 通过 :is 属性,动态指定要渲染的组件
- is 属性的值,表示要渲染的组件的名字。
- is 属性的值,应该是组件在 components 节点下的注册名称。
3.使用 keep-alive保持组件的状态 (避免组件切换时重新渲染)。
- keep-alive 会把内部的组件进行缓存,而不是销毁组件。
4.通过 include 指定哪些组件需要被缓存。
5.通过 exclude属性指定哪些组件不需要被缓存。
// 点击按钮,动态切换组件的名称
<button @click=“comName = ‘Left’”>展示 Left
<button @click=“comName = ‘Right’”>展示 Right
在组件中定义 name 名称:
当提供了 name 属性之后,组件的名称就是 name 属性的值。
export default {
name: ‘MyRight’
}
声明 name 应用场景:结合<keep-alive>
标签实现组件缓存功能;以及在调试工具中看到组件的 name 名称。
十、插槽solt
在签形式使用的
组件中内容节点
中插入内容
。
- 通过
slot
元素定义插槽
,从而为用户预留内容占位符。 - 封装组件时,没有预留插槽的内容会被丢弃。
后备内容
在
slot
标签内添加的内容会被作为后备内容。
// 子组件 my-com
<template>
<slot>这是后备内容</slot>
</template>
// 使用插槽
<my-com>
// 如果用户没有提供内容,上面 slot标签 内定义的内容会生效,此时页面会有 "这是后备内容"
// 如过提供了,下面的 img 将会被渲染到页面上
<img src="../assets/log.png" alt="">
</my-com>
具名插槽
// MyArticle 组件
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
- Vue 官方规定,每个
slot
插槽,都要有一个 name名称,一个不带name
的<slot>
出口会带有隐含的名字“default”
为具名插槽提供内容
如果要把内容填充到指定名称的插槽中,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称:
<MyArticle>
<template v-slot:header>
// 把内容放在 MyArticle组件的 header 标签内
<h1>如今最好,没有来日方长。</h1>
</template>
// 上一个代码块中,main 标签未指定具名插槽,所以默认渲染到 main标签中
<p>现在是2021年11月7日 22点05分 星期天</p>
<p>今天不仅是周日,也是冬至</p>
<template #footer>
<p>最后,祝大家冬至快乐!</p>
</template>
</MyArticle>
-
v-slot:
可以简写#
。例如 v-slot:header 可以被重写为 #header。 -
v-slot
属性只能放在 组件标签和<template>
元素上 (否则会报错)。
作用域插槽
在封装组件的过程中,可以为预留的<slot>
插槽绑定 props 数据,这种带有 props 数据的<slot>
叫做“作用域插槽
”。示例代码如下:
<template>
<div>
<h1> 这是 Left 组件</h1>
<!--下面的slot 是一个作用域插槽 -->
<slot v-for="item in list" :user="item"></slot>
</div>
</template>
接收作用域插槽对外提供的数据, 使用解构赋值
简化数据的接收过程 :
<!-- 使用自定义组件 -->
<Left>
<!--作用域插槽对外提供的数据对象,可以通过“解构赋值”简化接收的过程-->
<template #default="{user}">
<tr>
<td>{{user.id}}</td>
<td>{{user.name}}</td>
<td>{{user.state}}</td>
</tr>
</template>
</Left>
十一、自定义指令
vue 官方提供了 v-text、v-for、v-model、v-if 等常用的指令。除此之外 vue 还允许开发者自定义指令。
私有自定义指令 directives: { }
在每个 vue 组件中,可以在 directives
节点下声明私有自定义指令。
1. 定义一个私有自定义指令:
directives:{
color:{
//为绑定到的HTML元素设置红色的文字
bind(el){
//形参中的el是绑定了此指令的、原生的DOM对象
el.style.color=‘red'
}
2. 使用自定义指令:
-
使用自定义指令时,需要加上
v-指令前缀
<!–声明自定义指令时,指令的名字是color 使用时就是 v-color -->
十二、App组件
3. 为自定义指令动态绑定参数值
在 template
结构中使用自定义指令
时,可以通过等号 =
的方式,为当前指令动态绑定参数值:
<template>
<!--在使用指令时,动态为当前指令绑定参数值 color-->
<h1 v-color="color">App组件</h1>
</template>
data(){
return{
color:'red’
}
4. 通过 binding
获取指令的参数值:
在上面的实例中我们为自定义指令绑定了一个动态的参数值,如何拿到这个值呢 ?
在声明自定义指令时,可以通过形参中的第二个参数,来接收指令的参数值
<h1 v-color="color">App组件</h1>
// 此时传入的是字符串,不会在data 数据中查找
<p v-color="'blue'" ></p>
directives:{
color:{
bind(el,binding){
//通过binding对象的.value属性,获取动态的参数值
el.style.color=binding.value
}
5. 使用 update 函数更新 DOM
update 函数会在每次 DOM 更新时被调用。
注意:在 vue3 的项目中使用自定义指令时, bind 必须改为 mounted 、 update 改为 updated 。
bind 函数只调用 1 次:当指令第一次绑定到元素时调用,当 DOM 更新时 bind 函数不会被触发。
directives:{
color:{
//当指令第一次被绑定到元素时被调用
bind(el,binding){
el.style.color=binding.value
},
//每次DOM更新时被调用
update(el,binding){
el.style.color=binding.value
}
6. 同时使用 bind 和 update 函数简写
如果 bind
和 update
函数中的 逻辑完全相同
,则对象格式的自定义指令可以简写成函数格式:
directives:{
//在 bind 和 update 时,会触发相同的业务逻辑
color( el,binding ){
el.style.color=binding.value
}
全局自定义指令 Vue.directive ()
通过“
Vue.directive()
” 进行声明 。
注意:在使用 Vue-cli ( 脚手架) 时,要把全局自定义指令写在main.js
文件中:
//参数1:字符串,表示全局自定义指令的名字
//参数2:对象,用来接收指令的参数值
Vue.directive('color',function(el,binding){
el.style.color=binding.value
})
// 简写
Vue.directive('color',(el,binding) =>{
el.style.color=binding.value
})
十三、axios
axios 是一个专注于网络请求的库, 调用 axios 方法得到的返回值是
Promise 对象
。
axios 的基本使用
-
发起 GET 请求:
axios({ // 请求方式 method: 'GET', // 请求的地址 url: 'http://www.liulongbin.top:3006/api/getbooks', // URL 中的查询参数(GET请求传参) params: { id: 1 } }).then(function (result) { console.log(result) })
params
表示传递到服务器端的数据,以url参数
的形式拼接在请求地址后面- 如 : params: { page:1,per:3 }
- 最终生成:http://jsonplaceholder.typicode.com/page=1&per=3
-
发起 POST 请求:
document.querySelector('#btnPost').addEventListener('click', async function () { // 如果调用某个方法的返回值是 Promise 实例,则前面可以添加 await! // await 只能用在被 async “修饰”的方法中 // 1. 调用 axios 之后,使用 async/await 进行简化 // 2. 使用解构赋值,从 axios 封装的大对象中,把 data 属性解构出来 // 3. 把解构出来的data属性,使用冒号进行重命名,一般都重命名为 { data: res } const { data: res } = await axios({ method: 'POST', url: 'http://www.liulongbin.top:3006/api/post', // POST请求体传参 data: { name: 'zs', age: 20 } }) console.log(res) })
axios 封装的 6 个属性:
axios 在请求到数据之后,在真正的数据之外,套了一层外壳。
{
config:{ },
// data 才是服务器返回的真实数据
data:{
status: 200,
msg: "获取数据成功!",
data: Array(6)
},
headers:{ ... },
request:{ },
status: 200,
statusText: 'OK',
}
axios直接发起GET和POST请求
document.querySelector('#btnGET').addEventListener('click', async function () {
/* axios.get('url地址', {
// GET 参数
params: {}
}) */
const { data: res } = await axios.get('http://www.liulongbin.top:3006/api/getbooks', {
params: { id: 1 }
})
console.log(res)
})
document.querySelector('#btnPOST').addEventListener('click', async function () {
// axios.post('url', { /* POST 请求体数据 */ })
const { data: res } = await axios.post('http://www.liulongbin.top:3006/api/post', { name: 'zs', gender: '女' })
console.log(res)
})
在 Vue-cli 中使用 axios
一般我们会直接这么用:
// 在组件内
<template>
<button @click="postInfo">发起 POST 请求</button>
</template>
<script>
// 1. 导入 axios
import axios from 'axios'
export default {
//2. 在 methods 定义 axios请求方法
methods: {
async postInfo() {
const { data: res } = await axios.post('http://www.liaoyia.top:3306/api/post', { name: 'zs', age: 20 })
console.log(res)
}
}
}
</script>
缺点: 在每次使用时候都要导入 axios 文件,写请求地址 (对后期维护不友好) 。
把 axios 挂载到 Vue原型上并配置请求根路径
避免重复导入axios 和重复写入完整请求地址。
在main.js
中配置
import Vue from 'vue'
import App from './App.vue'
import axios from 'axios'
Vue.config.productionTip = false
// 全局配置 axios 的请求根路径 (官方提供配置项)
axios.defaults.baseURL = 'http://www.liaoyia.top:3006'
// 把 axios 挂载到 Vue.prototype 上,供每个 .vue 组件的实例直接使用
Vue.prototype.$http = axios
// 今后,在每个 .vue 组件中要发起请求,直接调用 this.$http.xxx
// 但是,把 axios 挂载到 Vue 原型上,有一个缺点:不利于 API 接口的复用!!!
new Vue({
render: h => h(App)
}).$mount('#app')
使用:
<template>
<button @click="btnGetBooks">获取图书列表的数据</button>
</template>
<script>
export default {
methods: {
// 点击按钮,获取图书列表的数据
async btnGetBooks() {
const { data: res } = await this.$http.get('/api/getbooks')
console.log(res)
}
}
}
</script>
但是把 axios 挂载到 Vue原型上并配置请求根路径也有缺点:
无法实现API接口的复用
: 在多个组件想使用同一个请求方法(API)的时候,只能在每个组件重新定义一次。
axios 封装
1.如果项目中有多个请求地址,我们可以根据多个地址使用工厂模式封装 js 模块,创建多个 axios
实例对象,并设置请求根路径 (baseURL
) :
步骤如下: 在项目的 src
目录下创建utils
文件夹并新建一个 request.js
文件:
import axiox from 'axios'
const request =axiox.create({
//baseURL会在发送请求的时候拼接在url参数的前面
baseURL:'http://jsonplaceholder.typicode.com/',
timeout:5000
})
// 向外导出
export default request
- 使用这种方法时: 一般我们只会在一个 js 模块创建一个axios 实例对象,并向外导出。
- 如果有多个服务器地址,那就创建多个 js模块,并在里面创建axios实例对象。
2.为了实现复用性,我们还可以把所有请求,都封装在API模块里,在API模块中,按需导出一个方法,这个方法调用 request.get 或 request.post 来请求一个接口,最后return 一个Promise 对象。
比如想调用接口获取用户相关信息:
在根目录新建 utils 文件夹并在里面新建 userAPI.js 文件
//导入 utils 文件夹下的 request.js
import request from '@/utils/request.js'
export const getArticleListAPI = function(_page, _limit) {
return request.get('/articles', {
params: {
_page,
_limit
}
})
}
【实例】:点击按钮发起 GET 请求并自动调用请求拦截
和添加响应拦截器
:
<template>
<div class="home">
<button @click="getByMineHandle">调用封装的get请求</button>
</div>
</template>
<script>
// 导入 get 方法
import { get } from '../utils/request'
export default {
name: 'Home',
methods:{
getByMineHandle(){
get('',{page:3,per:2}).
then(res=>console.log(res))
}
}
}
</script>
import axiox from 'axios'
const instance =axiox.create({
//baseURL会在发送请求的时候拼接在url参数的前面
baseURL:'http://jsonplaceholder.typicode.com/',
timeout:5000
})
//请求拦截
// 所有的网络请求都会先走这个方法
// 添加请求拦截器,所有的网络请求都会先走这个方法
// 我们可以在它里面为请求添加一些自定义的内容 比如 token 或者在 headers 提供信息
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
console.group('全局请求拦截')
console.log(config)
console.groupEnd()
config.headers.token ='12343'
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
//此处可以根据服务器的返回状态码做响应的处理
//404 404 500
instance.interceptors.response.use(function (response) {
// 对响应数据做点什么
console.group('全局响应拦截')
console.log(response)
console.groupEnd()
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
// 从服务器查看获取数据
export function get(url,params) {
return instance.get(url,{
params
})
}
// 向服务器创建数据
export function post(url,data) {
return instance.post(url,data)
}
// 从服务器删除数据
export function del(url) {
return instance.delete(url)
}
// 向服务器发送更新数据
export function put(url,data) {
return instance.put(url,data)
}
十四、Vue-router
SPA 与前端路由
SPA (单页面网页)
,所有组件的展示与切换都在这唯一的一个页面内完成。
此时,不同组件之间的切换需要依赖 前端路由(router)
来实现。
1.什么是前端路由?
Hash 地址与组件之间的对应关系
,不同的Hash 展示不同的页面。
2.前端路由的工作方式:
vue-router 的基本用法
vue-router 是 vue.js 官方给出的路由解决方案。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目中组件的切换。
- 安装 vue-router 包
npm i vue-router@3
-
创建路由模块
在 src 源代码目录下,新建
router/index.js
路由模块,并初始化://1.导入Vue和VueRouter的包import Vue from'vue' import VueRouter from‘vue-router' //2.调用Vue.use()函数,把VueRouter 安装为Vue的插件 Vue.use(VueRouter) //3.创建路由的实例对象 const router=new VueRouter() //4.向外共享路由的实例对象 export default router
-
导入并挂载路由模块
在
src/main.js
入口文件中,导入并挂载路由模块:import Vue from'vue' import App from'./App.vue' //1.导入路由模块 import router from@/router' new Vue ({ render:h=>h(App), //2.挂载路由模块 router:router }).$mount('#app')
-
声明路由链接和占位符
在 src/App.vue 组件中,使用 vue-router 提供的
<router-link>
和<router-view>
声明路由链接和占位符:<tcmplate> <div class="app-container"> <h1>App组件</h1> <!--1.定义路由链接--> <router-link to="/home">首页</router-link> <router-link to="/movie">电影</router-link> <router-link to="/about">关于</router-link> <!--2.定义路由的占位符--> <router-view></router-view> </div> </template>
-
声明路由的
匹配规则
在src/router/index.js
路由模块中,通过routes
数组声明路由的匹配规则://导入需要使用路由切换展示的组件 import Home from'@/components/Home.vue' import Movie from'@/components/Movie.vue' import About from'@/components/About.vue' //2.创建路由的实例对象 const router=new VueRouter({ //在routes数组中,声明路由的匹配规则 routes:[ //path 表示要匹配的hash地址;component表示要展示的路由组件 {path:'/home',component: Home}, {path:'/movie',component: Movie}, {path:'/about',component: About} ] })
路由重定向
当访问地址 A 的时候,
强制用户跳转
到地址C, 从而展示特定的组件页面。
- 通过路由规则的
redirect
属性,指定一个新的路由地址来实现路由的重定向。
下面这个应用场景,当用户访问网页 / 目录时候,跳转到首页:
const router=new VueRouter({
//在routes数组中,声明路由的匹配规则
routes:[
//当用户访问 / 的时候,通过 redirect 属性跳转到 /home 对应的路由规则
{ path: '/',redirect:'/home' },
{ path: '/home',component: Home },
{ path: '/movie',component: Movie },
{ path: '/about',component: About }
})
路由高亮
可以通过如下的两种方式,将激活的路由链接进行高亮显示:
- 使用默认的高亮 class 类
- 自定义路由高亮的 class 类
默认的高亮 class 类
被激活的路由链接,默认会应用一个叫做 router-link-active 的类名, 可以使用此类名选择器,为激活 的路由链接设置高亮的样式:
/*在index.css全局样式表中,重新router-link-active的样式*/
.router-link-active {
color:#ef4238;
font-weight:bold;
border-bottom: 2px #ef4238 solid;
}
自定义路由高亮的 class 类
在创建路由的实例对象时,开发者可以基于 linkActiveClass
属性,自定义路由链接被激活时所应用的类名:
const router=createRouter({
history:createlebHashHistory(),
// 指定被激活的路由链接,会应用 router-active 这个类名,
// 默认的 router-link-active 类名会被覆盖掉
1inkActiveclass:'router-active',
routes:[
{ path: '/',redirect: '/home' },
{ path: '/home',component: Home },
{ path:'/movie',component: Movie },
{ path: '/about',component: About },
]
})
嵌套路由
通过路由实现组件的嵌套展示,叫做嵌套路由。
1. 声明子路由链接和子路由占位符
上图中,我们在 About.vue
组件中套娃了 tab1 和 tab2 组件 ,如果我们想展示它,则需要声明子路由链接
以及子路由占位符
:
<template>
<div class="about-container">
<h3>About 组件</h3>
<!--1.在关于页面中,声明两个子路由链接-->
<router-link to="/about/tab1">tab1</router-link>
<router-link to="/about/tab2">tab2</router-link>
<hr/>
<!--2.在关于页面中,声明子路由的占位符-->
<router-view></router-view>
</div>
</template>
到此为止,我们已经有了 子路由链接
、子路由占位符
,以及链接对应的组件
,但是要在页面显示,我们还缺少对应关系,也就是路由规则
。
2.通过 children 属性声明子路由规则
在 src/router/index.js
路由模块中,导入需要的组件,并使用 children
属性声明子路由规则:
// 导入组件
import Tab1 from '@/components/tabs/Tab1.vue'
import Tab2 from '@/components/tabs/Tab2.vue'
const router=new VueRouter({
routes:[
{ //about 页面的路由规则(父级路由规则)
path: '/about',
component: About,
//1.通过children属性,嵌套声明子级路由规则
children:[
{ path: 'tab1',component: Tab1 },//2.访问/about/tab1时,展示Tab1组件
{ path: 'tab2',component: Tab2 } //2.访问/about/tab2时,展示Tab2组件
}]
})
3. 默认子路由
如果
children
数组中,某个路由规则的 path 值为空字符串,则这条路由规则,叫作“默认子路由”。
// src/router/index.js
const router=new VueRouter({
routes:[
{
path: '/about',
component: About,
// rediect: 'about/table' // 通过重定向设置默认展示页面
children:[
// path值为空字符串,此时Tab1为默认子路由,一进入About页面,默认展示 Tab1
{ path: '',component: Tab1 },
{ path: 'tab2',component: Tab2 }
}]
})
- 所以,
默认子路由
和路由重定向
都可以用来设置展示特定的组件页面
。
动态路由匹配
动态路由指的是:把 Hash 地址中
可变的部分
定义为参数项
,从而提高路由规则的复用性
。
有如下3个路由链接:
<router-link to="/movie/1"> 电影1 </router-link>
<router-link to="/movie/2"> 电影2 </router-link>
<router-link to="/movie/3"> 电影3 </router-link>
如果定义下面3个路由规则,虽然可行,但太过繁琐且复用性差:
{path:'/movie/1',component:Movie}
{path:'/movie/2',component:Movie}
{path:'/movie/3',component:Movie}
使用动态路由,将上面创建3个路由规则,合并成一个,提高了路由规则的复用性:
// src/router/index.js 文件
//路由中的动态参数以 : 进行声明,冒号后面的是动态参数的名称
{ path:'/movie/:id',component:Movie}
- 在 vue-router 中使用英文的冒号(
:
)来定义路由的参数项 。
在 动态路由
渲染出来的组件中,可以使用 this.$route.params
对象访问到动态匹配的参数值:
<template>
<div class="movie-container">
<!--this.$route是路由的“参数对象”-->
<h3> Movie 组件-- {{this.$route.params.id}} </h3>
</div>
</template>
<script>
export default{
name:'Movie'
}
</script>
使用 props 接收路由参数
为了简化路由参数的获取形式,vue-router 允许在路由规则中开启 props
传参, 在定义路由规则时,声明props : true
选项 ,
// router下的 index.js 文件
// 1. 声明 props : true 选项
{ path:'/movie/:id',component: Movie,props:true }
定义好后,即可在Movie组件中,以props的形式接收到路由规则匹配到的参数项。
<template>
<!-- 3、直接使用props中接收的路由参数 -->
<h3> MyMovie组件--{{id}}</h3>
</template>
<script>
export default{
//2、使用props接收路由规则中匹配到的参数项
props:['id']
</script>
编程式导航 API
vue-router 提供了许多编程式导航的 API,其中最常用的导航 API 分别是:
1.this.$router. push(‘hash 地址’)
- 跳转到指定 hash 地址,并增加一条历史记录 。
2.this.$router.replace(‘hash 地址’)
- 跳转到指定的 hash 地址,并替换掉当前的历史记录 。
3.this.$router. go (数值 n)
- 实现导航历史前进、后退。
- $router.back(),后退到上一个页面。
- $router.forward() ,前进到下一个页面。
调用 this.$router.push() 或者 $router.replace()方法,可以跳转到指定的 hash 地址,展示对应的组件页面 :
<template>
<div class="home-container">
<h3> Home组件 </h3>
<button @click="gotoMovie">跳转到Movie页面</button>
</div>
</template>
<script>
export default{
methods:{
gotollovie(){
this.$router.push("/movie/1")
}
}
</script>
push 和 replace 的区别:
- push 会
增加一条历史记录
- replace 不会增加历史记录,而是
替换掉当前的历史记录
。
调用 this.$router.go()
方法,可以在浏览历史中前进和后退:
<template>
<h3> MyMovie组件---{{id}}</h3>
<button @click="goBack" >后退</button>
</template>
<script>
export default{
props:['id'],
methods:{
goBack(){
this.$router.go(-1) //后退到之前的组件页面
}
},
</script>
导航守卫
每次发生路由的导航跳转时,都会触发全局前置守卫。因此,在全局前置守卫中,我们可以对每个路由进行访问权限的控制:
//创建路由实例对象
const router = new VueRouter({..…})
//调用路由实例对象的beforeEach方法,即可声明“全局前置守卫"
//每次发生路由导航跳转的时候,都会自动触发这个“回调函数”
router.beforeEach((to,from,next) =>{
/* 必须调 next 函数 */
})
守卫方法的 3 个形参:
- to 是将要访问的路由的信息 (对象)
- from 是将要离开的路由的信息对象
- next 是一个函数,一定要调用 next () 才表示放行,允许这次路由导航 。
注意:
- 在守卫方法中如果不声明 next 形参,则默认允许用户访问每一个路由!
- 在守卫方法中如果声明了 next 形参,则必须调用 next() 函数,否则不允许用户访问任何一个路由!
next 函数的 3 种调用方式
![](https://img-blog.csdnimg.cn/img_convert/58b596f485738a215080da5b86c2933a.png)
- 直接放行:
next()
强制其停留在当前页面
:next(false
)强制其跳转到登录页面
:next(’/login’)
结合 token 控制后台主页的访问权限
router.beforeEach((to,from,next) => {
//获取浏览器缓存的用户信息
const userInfo = window.localStorage.getItem('token');
if (to.path === '/main' && !userInfo ){ //访问的是 main 页面且 token 不存在
// 访问的是后台主页,但是没有token的值,跳转到登入页
next('/login')
} else{
next()//访问的不是后台主页,直接放行
})
上面代码中,如果项目中有多个页面都需要设置访问权限怎么办呢
-
使用路由导航守卫 判断访问的是否是一个有权限的 Hash 地址问题:
// 第一种: 使用 || 多重判断,代码臃肿 router.beforeEach((to,from,next) => { if (to.path === ‘/main’ || to.path === ‘/user’ || to.path === ‘seeting’){ // code… } else{ // … } })
-
我们可以把多个地址存入到一个
数组
或者一个json
文件、js
文件:const patArr = ['/mian', '/home', '/home/users', '/home/rights'] router.beforeEach((to,from,next) => { if (patArr.indexOf(to.path) !== -1){ // code.... } else{ // .... } }) // 存入 js 使用 // 1. 定义一个 pathArr.js文件 export default ['/mian', '/home', '/home/users', '/home/rights'] // 2. 使用时导入 import pathArr from '@/router'
meta 路由原信息
上面的例子中,还是不够方便,vue-router 中给我们提供了 meta
字段,传入一个对象,设置requiresAuth: true
,就相当于开起来跳转验证。
const router = new VueRouter({
routes: [
{
path: '/blog',
component: Blog,
path: 'bar',
component: Bar,
// 设置 meta ,开启跳转验证
meta: { requiresAuth: true }
}
]
})
全局守卫中定义页面权限:
router.beforeEach((to, from, next) => {
console.log(to)
//判断 $route.matched数组 是否中有 meta 字段值
if (to.matched.some(record => record.meta.requiresAuth)) {
如果当前页面的 是否有 user
if (!localStorage.getItem("user")) {
next({
path: '/login',
//传入完整的路径
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next() 这次放行说明在白名单
}
})
更改登入页面的逻辑
// Login.vue
methods:{
handLeLonge(){
// 获取用户名和密码
setTimeout(()=>{
let data = {
user: this.user
}
// 保存用户名到本地
localStorage.setItem('user',JSON.stringify(data))
this.$route.push({
path: this.$route.query.redirect
})
})
}
}
拓展 - 网页链接详解
// APP 组件
<router-link to="/movie/1"> 洛基 </router-link>
<router-link to="/movie/2?name=zs&age=20"> 雷神< /router-link>
<router-link to="/movie/3"> 复联 </router-link>
<router-link to="/about"> 关于 </router-link>
<template>
<div class="movie-container">
<!-- this.$route 是路由的“参数对象” -->
<!-- this.$router 是路由的“导航对象” -->
<h3>Movie 组件 --- {{ $route.params.mid }} --- {{ mid }}</h3>
<button @click="showThis"> 打印 this</button>
<!-- 在行内使用编程式导航跳转的时候,this 必须要省略,否则会报错! -->
<button @click="$router.back()">back 后退</button>
<button @click="$router.forward()">forward 前进</button>
</div>
</template>
<script>
export default {
name: 'Movie',
// 接收 props 数据
props: ['mid'],
methods: {
showThis() {
console.log(this)
},
}
}
</script>
点击雷神电影打印出的 this 里,有一个 $route
对象, 如下:
1.在 hash 地址中,/后面的参数项,叫做 “路径参数”
- 在路由“参数对象”中,需要使用 this.$route.params 来访问路径参数
- 但是我们一般会使用 props传参来接收路径参数。
2.在 hash 地址中,? 后面的参数项,叫做**“查询参数”**
- 在路由“参数对象”中,需要使用 this.$route.query 来访问查询参数
3.在 this.$route 中,path 只是路径部分;fullPath 是完整的地址。
开发思路:
1.要想从一个地址跳到另一个地址 (如: 登录后立即跳转到首页,并且首页里设置默认的展示的页面)
- 先要确定 离开谁 、要去哪儿
- 找到 要离开 的页面,给它添加 redirect指向新地址。
注意点 :
1.标签节点不平级时无法使用 v-if 和 v-else 结合來判断
2.在 vue 中:
- 在使用组件的时候,如果某个属性名是“小驼峰”形式,则绑定属性的时候,建议改写成“连字符”格式。例如 cmtCount 建议写成 cmt-count
十五、Vuex
Vuex是什么
vuex
是终极的组件之间的数据共享方案。
- Vuex 是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。
优点:
- 集中管理共享的数居,易于开发和维护
- 高效地实现组件之间的数据共享,提高开发效率
- 响应式数据,与页面的同步
什么样的数据适合存储到 Vuex 中 ?
一般情况下,只有组件之间共享的数据,才有必要存储到 vuex 中;对于组件中的私有数据,依旧存储在组件自身的data中即可 !
Vuex 的基本使用:
- 安装 vuex 依赖包
npm install vuex --save
- 在
store / index.js
导入包 并创建 store 对象//导入 vuex 包 import Vuex from 'vuex' Vue.use(Vuex) // 创建 store 对象 const store = new Vuex.Store({ // state 中存放的就是全局共享的数据 state: { count: 0 }, mutations: {}, actions: {}, modules: {} })
-
将 store 对象挂载到 vue 实例
main.js
中import store from './store/index'; new Vue({ el: '#app', render: h => h(app), router, // 将创建的共享数据对象,挂载到 Vue 实例中 // 所有的组件,就可以直接从 store 中获取全局的数据了 store })
一张图看懂 vuex:
state 数据源
State 提供唯一的公共数据源,所有共享的数据都要统一放到
Store
的state
中进行存储。
访问 state 中数据两种方式 :
// this.$store.state.全局数据名称
<h3>当前最新的count值为:{{$store.state.count}}</h3>
- 由于在模板字符串中,是不需要写 this 的。
mapState 映射为计算属性:
通过 mapState 函数,将当前组件需要的全局数据,映射为 computed
计算属性:
// 1. 在想使用数据的组件中 从 vuex 中按需导入 mapState 函数
import { mapState } from 'vuex'
// 2. 将全局数据,映射为当前组件的计算属性
computed: {
...mapState(['count'])
}
mutations 变更数据
mutation 用于变更 store 中的数据。
- 只能通过 mutation 变更 Store 数据,不可以直接操作 Store 中的数据。
- 虽然操作起来稍微繁琐,但可以集中监控所有数据的变化。
this.$store.commit() 触发 mutations
// 定义 Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
add(state) {
// 变更状态
state.count++
}
}
})
// 在组件中 触发 mutation
methods: {
handle1() {
// 触发 mutations 的第一种方式
this.$store.commit('add')
}
}
可以在触发 mutations 时传递参数
:
// 定义Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
addN(state, step) {
// 变更状态
state.count += step
}
}
})
// 触发mutation
methods: {
handle2() {
// 在调用 commit 函数,
// 触发 mutations 时携带参数
this.$store.commit('addN', 3)
}
}
mapMutations 映射为方法
- 从vuex中按需导入 mapMutations函数
import { mapMutations } from 'vuex'
- 通过刚才按需导入的 mapMutations 函数,映射为当前组件的
methods
函数。// 2. 将指定的 mutations 函数,映射为当前组件的 methods 函数 methods: { ...mapMutations(['add', 'addN']) }
实例:
// store
mutations: {
add(state) {
// 变更状态
state.count++
},
sub(state) {
state.count--
},
addN(state, step) {
// 变更状态
state.count += step
},
subN(state, step) {
state.count -= step
}
},
// 组件A
import { mapState,mapMutations } from 'vuex'
methods:{
...mapMutations(['sub','subN']),
decrement(){
// 调用
this.sub()
},
decrementN(){
this.subN(5)
}
}
Actions 处理异步操作
如果通过异步操作变更数据,必须通过 Action,而不能使用Mutation,但是在 Action中还是要通过触发Mutation的方式间接变更数据。
注意: 在Actions 中不能直接修改 state中的数据,要通过 mutations修改。
this.$store.dispatch 触发 Actions
// store/index.js 定义 Action
const store = new Vuex.store({
// ...省略其他代码
mutations: {
// 只有 mutations中的函数才有权利修改 state。
// 不能在 mutations里执行异步操作。
add(state) {
state.count++
}
},
actions: {
// 在Actions 中不能直接修改 state中的数据,要通过 mutations修改。
addAsync(context) {
setTimeout(() => {
context.commit('add')
}, 1000);
}
},
})
// 在组件中 触发 Action
methods:{
handle(){
// 触发 actions 的第一种方式
this.$store.dispatch('addAsync')
}
}
mapActions 映射为方法
// 1. 从Vuex中按需导入 mapActions 函数。
import {mapActions} from 'vuex'
// 2. 将指定的 actions 函数,映射为当前组件 methods 的方法。
methods:{
...mapActions(['subAsync']),
decrementAsync(){
this.subAsync()
}
}
// store/index.js
actions: {
// 在Actions 中不能直接修改 state中的数据,要通过 mutations修改。
subAsync(context){
setTimeout(() => {
context.commit('sub')
}, 1000);
}
}
Getter 按需展示数据
Getter 用于对 Store中的数据进行加工处理形成新的数据。筛选或者排序显示。
-
Getter 不会修改 Store 中的原数据,它只起到一个包装器的作用,将Store中的数据加工后输出出来。
-
Store 中数据发生变化, Getter 的数据也会跟着变化。
//定义 Getter const store = new Vuex.Store({ state:{ count:0 }, getters: { showNum(state) { return ‘当前最新的数量是【’ + state.count + ‘】’ } }, })
this.$store.getters.名称 访问
this.$store.getters.名称
mapGetters 映射为计算属性
import { mapGetters } from 'vuex'
computed:{
...mapGetters(['showNum'])
}
简写 !!!
其实,通过mapState,mapMutations,mapActions,mapGetters
映射过来的计算属性,或者方法都可以直接调用,不用在 commit
或者 dispatch
。
正常写法:
<button @click="decrementAsync"> -1Async</button>
import {mapActions} from 'vuex'
methods: {
...mapActions(['subAsync']),
decrementAsync(){
this.subAsync()
}
},
其实可以简写成:
<button @click="decrementAsync"> -1Async</button>
import {mapActions} from 'vuex'
//...省略一些代码
methods: {
...mapActions(['subAsync']),
},
有参数的时候,也可以直接把参数带上,就像这样:
<button @click="subAsync(5)"> +5 </button>
import {mapActions} from 'vuex'
//...省略一些代码
methods: {
...mapActions(['addAsync']),
},
十六、vue3
组件之间的数据共享
在vu3中使用使用自定义事相较vu2中多了一个 emits
节点声明:
vue 3中使用自定义事件:
在封装组件时:
1. 在`emits`节点声明自定义事件
2. `触发`监听事件
在使用组件时: 监听
自定义事件
声明自定义事件:
// 子组件
<button @click="onBtnClick"> +1 </button>
<script>
export default {
// 自定义事件必须声明到 emits 节点中
emits: ['chage']
}
</script>
触发自定义事件:
// 子组件
<button @click="onBtnClick"> +1 </button>
<script>
export default {
// 自定义事件必须声明到 emits 节点中
emits: ['chage'],
data() {
// 子组件自己的数据,将来希望把 count 值传给父组件
return { count: 0 }
},
methods: {
onBtnClick() {
this.count t= 1
//修改数据时,通过 $emit()触发自定义事件
// 当点击 '+1' 按钮时 调用this.$emit 触发自定义的 numchange 事件
this.$emit( 'numchange')}
}
}
</script>
监听自定义事件:
// 父组件
// 使用 v-on 指令绑定监听事件
<Son @numchange="getNewCount"></Son>
methods : {
getNewCount(val) {
console.log('监听到了 count 值的变化', val)
},
组件上的 v-model
当需要维护组件内外数据的同步时
,可以在组件上使用 v-model 指令。示意图:
父子组件数据共享
实现父子组件数据双向同步。
子组件向父组件共享数据:
-
父组件通过
v-bind
属性绑定向子组件共享数据,子组件使用props
接收数据。
父组件向子组件共享数据:
-
通过自定义事件
emits
,然后触发$emit()
, 子组件监听。
- 在 v-bind: 指令之前添加 v-model 指令
- 在子组件中声明 emits 自定义事件,格式为必须 update:xxx
- 调用 $emit() 触发自定义事件,更新父组件中的数据
使 v-model 实现子→父组件共享数据好处:
在父组件中 不用再监听自定义事件了,也不用再额外声明事件处理函数。
父组件:
// 使用双向数据绑定指令
<my-son v-model:number="count"> </my-son>
data(){
return{
count: 0
}
}
子组件:
// 子组件
<button @click="add"> +1 </button>
<script>
export default {
prpos:['number']
emits:['update:number']
methods: {
add() {
this.$emit( 'update:number',this.num+1)}
}
}
</script>
后代关系组件之间的数据共享
指的是
父节点的组件
向其子孙组件
共享数据。
使用步骤:
-
父节点通过
provide
方法,对子孙组件共享数据。export default { data() { return { color: 'red', // 1. 定义"父组件"要向"子孙组件"共享的数据 } }, provide() { // provide函数 返回要共享的数据对象 return { color: this.color, count: 1, } }, }
- 子孙节点通过
inject
接收数据。<template> <h5>三级组件 --- {{ color }} --- {{ count }}</h5> </template> <script> export default { // 子孙组件,使用 inject 接收父节点共享的数据 inject: ['color', 'count'], } </script>
父节点对外共享响应式的数据
父节点使用 provide 向下共享数据时并非响应式
,我们可以结合 computed 函数
向下共享响应式的数据
:
import { computed } from 'vue' // 1. 从vue中按需导入 computed 函数
export default {
data() {
return {
color: 'red',
}
},
provide() {
return {
// 2. 使用 computed 函数,把共享数据包装为"响应式"的数据
color: computed(() => this.color),
count: 1,
}
},
}
子孙节点使用响应式数据, 注意接收的响应式数据必须以 .value
的形式使用:
<template>
// 响应式数据,必须以.value 的形式进行使用
<h5>子孙组件 --- {{ color.value }} --- {{ count }}</h5>
</template>
<script>
export default {
// 子孙组件,使用 inject 接收父节点共享的数据
inject: ['color', 'count'],
}
</script>
全局数据共享
vuex 是终极的组件之间的数据共享方案。
- 解决大量、频繁的共享数据麻烦问题。
- vuex 就是用来管理组件中需要共享的数据。
数据共享总结
父子关系:
- 父 → 子 : 自定义属性
- 子 → 父 :自定义事件
- 父子组件数据共享 使用组件上的 v-model
兄弟关系: EvenBus
后代关系:provide & inject
全局数据共享: vuex
在vue3中全局配置 axios
import { createApp } from 'vue'
import './assets/css/bootstrap.css'
import './index.css'
import axios from 'axios'
// 创建一个单页面应用程序实例
const app = createApp(App)
// 配置全局请求根路径
axios.defaults.baseURL = 'https://www.escook.cn'
// 全局挂载到app 根路径中
// $http 是模仿 vue封装成员的方式
app.config.globalProperties.$http = axios
// 实现挂载
app.mount('#app')
在组件中使用:
methods: {
async getInfo() {
const { data: res } = await this.$http.get('/api/get', {
// get 请求必须通过 params 传参
params: {
name: 'ls',
age: 33,
},
})
console.log(res)
},
},
vue3 中创建路由
-
vue3 中需要安装 4.x的版本
-
在 src 目录下创建
router.js
文件并配置import { createRouter, createWebHashHistory } from ‘vue-router’ import Login from ‘./components/MyLogin.vue’ import Home from ‘./components/Home.vue’ // 创建路由实例对象 const router = createRouter({ history: createWebHashHistory(), // 指定通过 hash 管理路由的切换 routes: [ { path: ‘/home’, redirect: ‘/login’ }, { path: ‘/login’, component: Login, name: ‘login’ }, ], }) export default router //4. 向外共享路由对象
在 src/main.js
入口文件中,导入并挂载路由模块:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 创建 app 实例
const app = createApp(App)
app.use(router)
// 挂载 app 实例
app.mount('#app')
事件:
// 父组件
// 使用 v-on 指令绑定监听事件
<Son @numchange="getNewCount"></Son>
methods : {
getNewCount(val) {
console.log('监听到了 count 值的变化', val)
},
组件上的 v-model
当需要维护组件内外数据的同步时
,可以在组件上使用 v-model 指令。示意图:
父子组件数据共享
实现父子组件数据双向同步。
子组件向父组件共享数据:
- 父组件通过
v-bind
属性绑定向子组件共享数据,子组件使用props
接收数据。
父组件向子组件共享数据:
- 通过自定义事件
emits
,然后触发$emit()
, 子组件监听。
- 在 v-bind: 指令之前添加 v-model 指令
- 在子组件中声明 emits 自定义事件,格式为必须 update:xxx
- 调用 $emit() 触发自定义事件,更新父组件中的数据
使 v-model 实现子→父组件共享数据好处:
在父组件中 不用再监听自定义事件了,也不用再额外声明事件处理函数。
父组件:
// 使用双向数据绑定指令
<my-son v-model:number="count"> </my-son>
data(){
return{
count: 0
}
}
子组件:
// 子组件
<button @click="add"> +1 </button>
<script>
export default {
prpos:['number']
emits:['update:number']
methods: {
add() {
this.$emit( 'update:number',this.num+1)}
}
}
</script>
后代关系组件之间的数据共享
指的是
父节点的组件
向其子孙组件
共享数据。
使用步骤:
-
父节点通过
provide
方法,对子孙组件共享数据。export default { data() { return { color: 'red', // 1. 定义"父组件"要向"子孙组件"共享的数据 } }, provide() { // provide函数 返回要共享的数据对象 return { color: this.color, count: 1, } }, }
- 子孙节点通过
inject
接收数据。<template> <h5>三级组件 --- {{ color }} --- {{ count }}</h5> </template> <script> export default { // 子孙组件,使用 inject 接收父节点共享的数据 inject: ['color', 'count'], } </script>
父节点对外共享响应式的数据
父节点使用 provide 向下共享数据时并非响应式
,我们可以结合 computed 函数
向下共享响应式的数据
:
import { computed } from 'vue' // 1. 从vue中按需导入 computed 函数
export default {
data() {
return {
color: 'red',
}
},
provide() {
return {
// 2. 使用 computed 函数,把共享数据包装为"响应式"的数据
color: computed(() => this.color),
count: 1,
}
},
}
子孙节点使用响应式数据, 注意接收的响应式数据必须以 .value
的形式使用:
<template>
// 响应式数据,必须以.value 的形式进行使用
<h5>子孙组件 --- {{ color.value }} --- {{ count }}</h5>
</template>
<script>
export default {
// 子孙组件,使用 inject 接收父节点共享的数据
inject: ['color', 'count'],
}
</script>
全局数据共享
vuex 是终极的组件之间的数据共享方案。
- 解决大量、频繁的共享数据麻烦问题。
- vuex 就是用来管理组件中需要共享的数据。
数据共享总结
父子关系:
- 父 → 子 : 自定义属性
- 子 → 父 :自定义事件
- 父子组件数据共享 使用组件上的 v-model
兄弟关系: EvenBus
后代关系:provide & inject
全局数据共享: vuex