文章目录
Vue2进阶学习笔记
前言
欢迎来到知识汲取者的个人博客!在这篇文章中,我将为你介绍Vue.js的一些进阶知识,帮助你快速入门并掌握Vue开发的一些进阶技巧。Vue.js是一个流行的JavaScript框架,被广泛用于构建现代化、交互式的Web应用程序。它采用了MVVM(Model-View-ViewModel)架构模式,通过数据驱动视图的方式实现了高效的前端开发。在这片文章中我们将学习Vue脚手架的使用、组件的封装与使用、事件与动画、插槽、Vuex和VueRouter、最后还有一些常用Vue组件库的推荐,如果你是一个初学者,相信通过本文的学习,一定可以让你对Vue有一个更加深入的认知,同时快速掌握这些进阶知识。
PS:对于文章一些描述不当、存在错误的地方,还请大家指出,笔者不胜感激
推荐阅读
1、Vue脚手架学习
1.1 Vue脚手架概述
-
什么是脚手架?
上面是Vue脚手架的官方介绍,我们可以理解脚手架就是为自动帮我们构建项目的工具。Vue脚手架全名:
Vue Command Line Interface
,简称:VueCLI
直接翻译就是Vue命令接口工具,不得不佩服第一次将这个东西翻译成”脚手架“这个词的人,VueCLI正如工地上的脚手架一样,能够快速帮我们自动搭建好Vue项目👉Home | Vue CLI (vuejs.org):Vue脚手架官方文档
-
脚手架有什么作用?
自动一键生成vue+webpack的项目模版,包括依赖库,从而大大减少项目构建所需的时间
-
脚手架如何使用?
下文会讲
1.2 Vue脚手架安装
-
Step1:安装node.js
参考文章:
我们需要用到npm命令
-
Step2:安装VueCLI
npm install -g @vue/cli
下载过慢的可以配置淘宝镜像
npm config set https://registry.npm.taobao.org
安装后,重启cmd窗口,输入vue指令,如果呈现以下这样,就说明你的Vue脚手架已经安装好了
-
Step3:创建Vue项目
vue create 项目名
注意:时你要切换到你需要创建项目的目录,不要在VueCLI的安装目录下直接创建Vue项目
-
Step4:运行创建好的Vue项目
注意:要先使用
cd vue_test
指令进入刚刚创建好的Vue项目中,然后再执行npm run serve
指令,运行Vue项目,在Vue脚手架初始化创建项目时,他会自带一个helloword,并且它会将这个项目默认部署在一个微型服务器上,这个服务器的默认端口号是8080,所以成功运行后,可以直接在浏览器访问到Vue脚手架自带的HelloWord项目备注:停止运行,直接在cmd窗口中按
Ctrl + C
可以看到项目无法访问了
Vue模板项目的结构:
Vue 脚手架隐藏了所有 webpack 相关的配置,若想查看具体的 webpakc 配置, 请执行:
vue inspect > output
命令
模板项目默认是引入精简版的Vue(缺少模板解析器,这只是一种精简版,此外还有很多不同的精简版),引入精简版Vue的目的是节约内存,因为如果一直使用完整版的Vue,当我们的项目被webpack打包时,会讲模板解析器打包,而webpack打包后的项目是已经经过解析了的,此时模板解析器显得很多余。所以当我们要使用精简版vue使用tempate添加DOM时,会报错,可以使用render
函数解决
render(createElement){
return 'h1','你好'
}
//简写:
render:h => h(‘h1’,'你好')
1.3 常用属性
-
ref
:用来给元组或子组件注册引用信息(id的替代者)当用在HTML标签上,是获取真实的DOM元素(和id的作用一样),当用在组件标签上,则是获取组件的实例对象(这个和id不一样,id是获取组件所在根标签的真实DOM,即获取的是组件最外层的div)
实例:
<div ref="a"></div> this.$refs.a
-
props
:App组件
<!-- :让数据动态绑定,这样就能够使age进行运算,否则age就是一个字符串 --> <Student name="张三" :age="18"></Student>
Student组件:
<h2>{{name}}</h2> <h2>{{age+1}}</h2> <!--此处的age在页面中是19,如果age不加:此处就是181--> //简单接收 props['name','age'] //接收且对数据进行限制 props:{ name:String, age:Number } //更加完善的配置 props:{ name:{ String, required:true //name是必要的,没有app组件没有传name过来就会报错 } age:{ Number, default:18 // 设置默认值,没有传age,age默认就是18 } }
注意:如果props和data中数据出现重名,props中的数据优先级更高,props中的数据最好不要去修改,否则会出现警告,可能会导致Vue发生奇怪的bug,所以要按规范写,如果真的想要修改,可以在组件中重新定义一个变量
-
mixin
:混合,把多个组件共有的配置提取成一个混入对象,作用是提高代码的复用性mixin.js:
用于存放要复用的代码,可以复用数据、函数(包括生命周期函数),如果数据和普通函数在原组件中也存在,则以原来的为准,原来没有的,混合(mixin.js)中有的,就直接在原来的组件中加上。但是对于生命周期函数而言,两者都要
//混合1: export const mixin = { data(){ return{ name:"张三" } } } //混合2 export const minx2 = { mounted(){ console.log("mounted被执行了") } }
Student组件:
import {mixin,mixin2} from '../mixin' export default{ name:'School', data(){ return{ name:'一中' } }, //局部混合 mixins:[mixin,mixin2] }
main.js:
//引入APP组件 import {mixin,mixin2} from './mixin' Vue.mixin(mixin) Vue.mixin(mixin2)
-
scoped
:随机生成一个data-v,类似于UUID作用:方式样式冲突,因为在一个Vue项目中,最终所有的样式都会汇总到一起,如果出现重名就会发生样式覆盖,后引入的组件会覆盖前引入组件的样式(就是学CSS时的”就近原则“)
<style scoped class="demo"> </style>
1.4 插件
作用:用于增强Vue
本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据。
-
Step1:定义一个插件
export default { install(Vue, name){ console.log(Vue+"++++"+name) //插件中还可以进行一些全局配置,比如:过滤器、全局指令、在Vue原型中添加数据、定义混入对象... } }
-
Step2:使用插件
Vue.use(plugins, '张三')
2、组件基本概述
-
什么是模块?
模块是一个提供特定功能的JS程序,一般的表现形式就是一个JS文件。模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。
-
模块化的作用?
- 提高复用率,模块化对每一个功能进行了划分,能够更好地移植相同代码
- 提高编码的效率,降低编码的复杂度,模块化很好的界定各个功能,特别是对于大型系统的开发,让开发者能够分工明确
- 提高系统的可维护性,模块化让系统更加清晰,能够更好地定位问题和修改代码
-
什么是组件?
组件是实现应用中局部功能代码和资源的集合。组件化是指解耦复杂系统时将多个功能模块拆分、重组的过程,由多种属性、状态反映其内部特性。
个人见解:是一个可重复利用的Vue实例,本质是代码的复用,类似于Java中的类,Java中的类也可以看作是一个组件
-
组件化的优点
- 提高复用率。
- 提高编码效率。
- 高扩展性。
- 提高了可维护性。
-
组件化和模块化的区别
两者一般很难区分,因为两者的主要目的都相同, 都是为了提高代码的复用率,模块就是完成某一功能的程序,而组件是可重用代码的集合,组件和模块有时是可以相互转换的。但一般而言,模块的层级是要大于组件的层级的,一个大型项目,会先进性模块划分,然后再对每个模块进行组件划分。
PS:这两个概念,在我看来并不是说一定要马上进行区分,通过后期编写项目都是可以慢慢深入理解区分的,现在我们只需要大致了解即可,因为他对我们的编码并没有影响
3、非单文件组件
一个文件中包含n个组件(n>=2)
3.1 非单文件组件的基本使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./js/vue.js"></script>
</head>
<div id="app">
<!-- Step3:将组件引入页面中 -->
<school></school>
<hr>
<student></student>
</div>
<script>
Vue.config.productionTip = false;
//Step1:创建组件
const school = Vue.extend({
template: `
<div>
<h2>学校姓名:{{schoolName}}</h2>
<h2>学校地址:{{schoolAddress}}</h2>
</div>
`,
data() {
return {
schoolName: '一中',
schoolAddress: '长沙'
}
}
});
//Step1:创建组件
const student = {
template: `
<div>
<h2>学生姓名:{{studentName}}</h2>
<h2>学生年龄:{{studentAge}}</h2>
</div>
`,
data() {
//此处return可以避免组件之间数据共享的冲突(使用return可以让每个组件互不影响)
return {
studentName: '张三',
studentAge: 18
}
}
}
//Step2:全局注册组件
Vue.component('student', student)
new Vue({
el: '#app',
//Step2:注册组件
components: {
// school: school
//简写形式:
school
}
});
</script>
<body>
</body>
</html>
注意事项:
-
当组件名是多个单词是,可以使用
kebab-case
方式和cameCase
方式备注:使用kebab-case方式命名组件需要使用引号包裹,使用cameCase组件必须是在脚手架环境下(后面学脚手架时会详细学习),否则会报错
-
组件名不能和HTML的标签同名,否则会报错
-
组件的名字直接在注册时确定,但是可以使用name属性在创建组件时确定
3.2 组件的嵌套
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./js/vue.js"></script>
</head>
<body>
<div id="root">
<app></app>
</div>
<script>
Vue.config.productionTip = false;
//创建student组件
const student = {
template: `
<div>
<h2>{{student}}</h2>
</div>
`,
data() {
return {
student: '张三'
}
}
}
//创建school组件
const school = {
template: `
<div>
<h2>{{school}}</h2>
<student></student>
</div>
`,
data() {
return {
school: '一中'
}
},
components: {
student
}
}
//创建app组件,vm下面就它一个组件
const app = {
template: `
<div>
<school></school>
</div>
`,
components: {
school
}
}
//创建vm
new Vue({
el: '#root',
data: {
},
components: {
app
}
})
</script>
</body>
</html>
知识拓展:VueComponent
- 上面组件的本质都是一个VueComnent的构造函数,且不是程序员定义的,是Vue.extend生成的
- 我们只需要直接使用组件标签,Vue在解析模板时,会自动帮我们去调用VueComnent函数,也就是执行new VueComnent(options)
- 每次调用Vue.extend,都会返回一个全新的VueComponent对象
- 组件配置中(data函数、methods函数、watch函数等)的this都是VueComponent(简称vc,组件实例对象),而 new Vue配置中所有的this都是vm
- 函数拥有显示原型对象,对象拥有隐式原型对象,两者都指向同一个原型对象
- 一个重要的内置关系:
VueComponent.prototype.__proto__ === Vue.prototype
,所以组件实例对象vc可以访问到Vue实例对象vm上的属性和方法
4、单文件组件
一个文件中只包含一个组件(常见形式)。
4.1 快速体验
-
Step1:编写功能组件
每一个组件都是一个小的部件,完成一个小的功能
1)Student组件
<template> //这里写html代码(注意,div必须要) <div> <h2>{{studentName}}</h2> <h2>{{studentAge}}</h2> <h2 v-if="isShow">{{studentSex}}</h2> <button @click="showSchool">点击显示性别</button> </div> </template> <script> //这里写JS代码 export default { name:'Student', data(){ return{ studentName:'张三', studentAge:19, studentSex:'男', isShow:false } }, methods:{ showSchool(){ this.isShow = true; } } } </script> <style> /*这里写css代码*/ </style>
2)School组件
<template> <div> <h2>{{schoolName}}</h2> </div> </template> <script> export default { name:'School', data(){ return{ schoolName:'一中' } } } </script> <style> </style>
-
Step2:编写APP组件
App组件用户统一管理其它所有的组件,APP组件是”万人之上,一人之下“,它只归vm管理
<template> <div> <Student></Student> <School></School> </div> </template> <script> import Student from './Student.vue' import School from '../School.vue' export default { name:'App', components:{ Student, School } } </script> <style> </style>
-
Stpe3:编写main.js
mian.js负责创建vm
import App from './App.vue' new Vue({ el: '#root', components: { App } })
-
Step4:编写index.html
index.html真正用来展示的网页
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="root"> <App></App> </div> <script src="../js/vue.js"></script> <script type="module" src="./main.js"></script> </body> </html>
备注:暂时还看不到效果,因为单文本组件需要在脚手架环境下使用
4.2 Todo案例
采用Vue单文本组件的形式编写一个Todo案例,具体效果如下所示:
-
Step1:拆分组件
技巧:将功能和位置在一块的看作一个整体,将其作为一个组件;组件内部若功能相同,且会发生动态变化,则可以拆分成一个子组件;组件的命名要能够见名知意,如果组件的命名不合理就说明有可能你的组件拆分不合理
- 一个组件在用,放在组件本身即可;一些组件再用,放在父组件上,提高复用性
注意:组件名不要和HTML中的关键字发生冲突
-
Step2:搭建环境
- 在我们要编码的目录,使用vue脚手架提供的vue指令
vue create todo-list
,初始化一个Vue工程 - 然后进入todo-list目录,使用
npm run serve
命令运行项目
- 在我们要编码的目录,使用vue脚手架提供的vue指令
-
Step3:编码(细节、bug拉满😇,对于我这种初学Vue的人而言还是有难度的,还好张老师够牛,讲解很详细)
-
CSS部分,
<style>
标签最好养成添加scoped
属性的习惯。因为最终所有组件的CSS样式都会融合到一个文件中,如果不添加会导致融合后发生CSS样式冲突,添加scoped属性能够保障这个CSS样式只作用于本组件;App组件中的CSS样式可以不添加scoped属性,因为App组件中的代码属于全局性的,是对所有组件生效的 -
动态数据传递,如果想要将父组件的属性传递到子组件中,可以使用
props
在父组件中定义一个数据,用户存储任务,然后遍历组件得到多个子组件,并且将遍历的对象传递给子组件
注意:动态数据的传递,需要使用
v-bind
进行绑定(:
是简写形式) -
动态修改一个标签的属性,也需要使用
v-bind
指令 -
使用uuid的简化形式:nanoid
-
组件间的通信。兄弟组件传值:将需要传值的对象放在两者相同父组件中(常用的是App)、全局事件总线、消息订阅和发布、vuex……
将需要传值的对象放在两者相同父组件中实现步骤:
1)在父组件中定义一个要传递的值,此外再定义一个待参的函数
2)将函数通过v-bind指令传递给子组件,子组件调用函数,将要传递的值作为函数的参数
3)父组件接收到函数中的参数,然后在通过v-bind指令传递给另一个组件
注意事项:props、data、computed中的属性值,不能重名。
props属性是父组件传递给子组件的值,一般在子组件中不要随便修改props中的值,所以v-model绑定的值最好不要是props,虽然不报错,但不规范
缺点:使用props进行组件间的通信,会造成代码污染,一些组件并不会使用到props传递的值,但是为了传递需要定义这个值
-
Vue监测props是浅层次的数据变化,比如:todo.a=222(Vue不能检测到修改),todo={a:1,b2}(Vue能检测到修改)
-
5、浏览器本地存储
浏览器中有两个地方可以用于缓存数据,LocalStorage和SessionStorage。它们的共同点都是:储大小为5MB,都保存在客户端,不与服务器进行交互通信,有相同的Web API;不同点是:localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据,sessionStorage 数据在当前浏览器窗口关闭后自动删除
示例:主要有以下四个API
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LocalStorage</title>
</head>
<body>
<button onclick="saveData()">setItem</button>
<button onclick="readData()">getItem</button>
<button onclick="removeData()">removeItem</button>
<button onclick="clearData()">clear</button>
<script>
function saveData() {
localStorage.setItem('msg1', 'abc');
localStorage.setItem('meg2', 123);
localStorage.setItem('msg3', new Date);
let p = {
name: '张三',
age: 18
};
localStorage.setItem('msg4', JSON.stringify(p));
}
function readData() {
let p = JSON.parse(localStorage.getItem('msg4'));
console.log(p);
}
function removeData() {
localStorage.removeItem('msg4');
}
function clearData() {
localStorage.clear();
}
</script>
</body>
</html>
只需要将localStorage
换成sessionStorage
就能存入浏览器的SessionStorage中
对Todo案例进一步改造(todo-list-storage),让数据能够缓存到浏览器中
使用
watch
属性,监视todos,只要todos发生改变,就将todos的值存入localStorage中watch: { // 监视todos,一但todos发生改变,就将todos的新值存入localStorage中 todos(value) { localStorage.setItem('todos', JSON.stringify(value)) } }
初始化时,需要读取localStorage中的todos,其次应当注意用户首次登录时,localStorage中没有todos,为null,此时footer会统计todos,
todos.length
会报错,需要使用todos: JSON.parse(localStorage.getItem('todos')) || []
启用深度监视,因为相对于todos而言,checked在第二层,
watch
默认是浅度监视,只能监视第一层,第一层是todos中的属性,第二层是todo中的属性watch: { // 监视todos,一但todos发生改变,就将todos的新值存入localStorage中 todos: { // 开启深度监视,todos中todo的checked属性会被监视 deep: true, handler(value) { localStorage.setItem('todos', JSON.stringify(value)) } } }
6、组件的自定义事件
6.1 使用自定义事件传递数据
$on
:监听事件$off
:移除监听事件$emit
:触发事件$once
:监听事件,只监听一次
-
子组件传递数据给父组件(后两种方法都使用了自定义的事件,只适用于子组件给父组件传递数据)
-
v-bind+props+函数
-
v-on+$emit
-
ref+$on
:这种方式更加灵活,但感觉有点绕
上代码:
App.vue:
<template> <div id="app"> <!-- 方式一:props + 函数 --> <SchoolTest :getSchoolName="getName"></SchoolTest> <!-- 方式二:v-on + $emit --> <!-- <StudentTest v-on:getStudentName="getName"/> --> <!-- 简写形式 --> <StudentTest @getStudentName="getName"/> <!-- 方式三:ref + $on --> <TeacherTest ref="teacher"/> </div> </template> <script> import SchoolTest from './components/SchoolTest' import StudentTest from './components/StudentTest' import TeacherTest from './components/TeacherTest' export default { name: 'App', components: { SchoolTest, StudentTest, TeacherTest }, mounted() { // 三秒后触发事件 setTimeout(() => { // 给TeacherTest组件绑定sendTeacherName事件,当getTeacherName事件触发时,调用getName方法 this.$refs.teacher.$on('sendTeacherName', this.getName) }, 1000) }, methods: { getName(value) { console.log("App组件收到:", value) } } } </script> <style> </style>
SchoolTest.vue:
备注:关于这里为什么要加一个Test,好像是现在Vue不支持单个单词当组件名了
<template> <button @click="sendSchoolName">School</button> </template> <script> export default { name: 'SchoolTest', props: ['getSchoolName'], data() { return { name: '东山中学' } }, methods: { sendSchoolName() { console.log("SchoolTest组件发送", this.name) this.getSchoolName(this.name) } } } </script> <style> </style>
Student.vue:
<template> <button @click="sendStudentName">Student</button> </template> <script> export default { name:'StudentTest', data() { return { name:'张三' } }, methods:{ sendStudentName(){ console.log("StudentTest组件发送", this.name) // $emit作用是触发事件(本质是获取组件对象vc,然后通过vc对象触发事件) this.$emit('getStudentName', this.name) } } } </script> <style> </style>
TeacherTest.vue:
<template> <button @click="sendTeacherName">Teacher</button> </template> <script> export default { name:'TeacherTest', data() { return { name:'李四' } }, methods:{ sendTeacherName(){ console.log("TeacherTest组件发送", this.name) // $emit作用是触发事件(本质是获取组件对象vc,然后通过vc对象触发事件) this.$emit('sendTeacherName', this.name) } } } </script> <style> </style>
效果展示:
-
6.2 解绑自定义事件
-
解绑一个事件
示例:
在上一个案例的基础上进行实验,先在StudentTest组件中,创建一个按钮,然后给按钮绑定一个unbind事件
<button @click="unbind">解绑事件</button>
unbind事件:
unbind() { this.$off('getStudentName') // getStudentName就是绑定在TeacherTest组件上的自定义组件 }
效果展示:
注意区分组件事件和DOM事件,getStudentName是组件事件,而SendStudentName是DOM事件
-
解绑多个事件
// 解绑多个自定义组件事件 this.$off(['event1', 'event2', ...]) // 解绑所有的自定义组件事件 this.$off()
注意事项:
-
注意点1:
$on
中避免使用匿名函数前面我们是通过在App的methods中定义了触发自定义事件后执行的事件,然后通过this直接引用,此时的this指向App组件
mounted() { // 当getTeacherName事件触发时,调用getName方法 this.$refs.teacher.$on('sendTeacherName', this.getName) }, methods: { getName(value) { console.log("App组件收到:", value) } }
但如果我们在这个地方使用匿名函数的形式来调用呢?
this.$refs.teacher.$on('sendTeacherName', function(value){ console.log("App组件收到:", value) console.log(this) // 此时的this指向触发sendTeacherName事件的对象,也就是TeacherTest组件对象 })
解决方法:使用箭头函数,这是由于ES6规定,箭头函数没有自己的this,他会使用父级的this
-
注意点2:组件使用原生DOM事件相关的API,需要添加
native
修饰符原生的DMO事件有:
@click
(v-on:click
)<TeacherTest ref="teacher" @click.native="alert"/> methods: { alert() { alert("你好") } }
如果不添加native,绑定的事件会没有作用
7、全局事件总线
可以实现任意组件通信,我直呼牛逼
全局事件总线的实现思路:添加一个中间量,通过中间量来传递组件间的数据
- 需要有一个东西(设它为x)能够被所有的组件识别
- x需要拥有
$on
、$off
、$emit
这几个事件API
具体实现方式有两种:
- 第一点毋庸置疑,只有原型对象Prototype能够被所有的组件所识别
- 第二点,原型对象没有那几个事件的API,只有Vue对象或者VueComponent对象上有,即vm对象和vc对象,所以我们可以将vm或vc对象当成x绑定到原型对象Prototype上,这样就能实现任意组件间的通信了
通常我们会将x命名为bus,并且一般原型对象的属性按照习惯都会加一个$
,所以x就成了$bus
其一所有的vc和vm都有同一个原型对象,参考下图:
示例:
mian.js:
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
// 方式一:将vc当作工具人
// const VueComponent = Vue.extend({})
// const vc = new VueComponent
// Vue.prototype.$bus = vc;
new Vue({
render: h => h(App),
beforeCreate() {
// 方式二:将vm当作工具如(推荐使用这种方式,更加简洁)
Vue.prototype.$bus = this // 安装全局事件总线
}
}).$mount('#app')
StudentTest.vue:
<template>
<div>
<!-- 实现兄弟组件间的通信(StudentTest组件和TeacherTest组件的通信) -->
<button @click="sendMsg" style="color: red;">Student</button>
</div>
</template>
<script>
export default {
name: 'StudentTest',
methods: {
// 测试事件总线
sendMsg() {
console.log('StudentTest组件发送数据', 666)
// 触发School组件的alert事件
this.$bus.$emit('alert', 666)
}
}
}
</script>
<style>
</style>
TeacherTest.vue:
<template>
<div>
</div>
</template>
<script>
export default {
name: 'TeacherTest',
mounted() {
// 给$bus绑定一个alert事件,当$bus触发alert,就进行输出
this.$bus.$on('alert', (data) => console.log('TeacherTest组件接收', data))
},
beforeDestroy() {
// 当TeacherTest组件销毁时,就解绑$bus上的alert
this.$bus.$off('alert')
}
}
</script>
效果展示:
点击StudentTest组件的按钮,就能将数据发送给TeacherTest组件
实战训练:
将todo-list-event中父孙传值。之前App组件与MyItem组件间的传值,是使用props+函数的形式进行传值的,需要经过中间层MyList,这样显得很没必要,现在就直接使用全局事件总线实现,App组件与MyItem之间的直接通信
8、消息订阅和发布
-
什么是消息订阅和发布?
消息的订阅和发布,简称发布-订阅(publish-subscribe),是一种通信模式,是指消息的发布者不会将消息直接发送给特定的接收者(也称订阅者),它会将要发布的消息分为不同的类别,无需了解订阅者是谁;订阅者只会接收某一种或多种类型的消息,而不是全部接收,同样的订阅者也无需了解发布者是谁
-
消息订阅和发布的作用是什么?
实现发布者和订阅者的解耦,让系统具有更高的扩展性和可维护性
-
常见的发布-订阅
DOM
操作中的addEventListener
Vue
中的事件总线的概念Node.js
中的EventEmitter
以及内置库
举个例子,假如你在某平台订阅了某个专题,该系统会自动在该专题更新的时候,主动推送信息给到你,而不用你手动地去查阅。这个例子就类似发布-订阅模式
-
发布-订阅的具体流程
发布-订阅
是对象中的一种一对多的依赖关系,当一个对象触发一个事件的时候,所有订阅该事件的对象将得到通知。-
发布者:通过事件中心派发事件
-
订阅者:通过事件中心进行事件的订阅
-
事件中心:负责存放事件和订阅者的关系
-
-
如何在项目中应用发布-订阅模式?
一种是自己实现,另一种是直接引用别人开发的库,这里我使用的是
pubsub-js
使用步骤:
-
Step1:安装
pubsub-js
npm i pubsub-js
-
Step2:引入
pubsub-js
import pubsub from 'pubsub-js'
-
Step3:订阅消息
this.pubId = pubsub.subscribe('hello', function(msgName, data) { // console.log(this) // 注意没有使用箭头函数,此时这里的this是undefined console.log("Subscribe接收", msgName, data) })
-
Step4:发布消息
publishMsg() { console.log('Publish发送', '你好') pubsub.publish('hello', '你好') }
示例:
PublishTest.vue(消息的发布者):
<template> <div> <button @click="publishMsg">Publish</button> </div> </template> <script> import pubsub from 'pubsub-js' export default { name: 'PublishTest', methods: { publishMsg() { console.log('Publish发送', '你好') pubsub.publish('hello', '你好') } } } </script> <style> </style>
SubscribeTest.vue(消息的订阅者):
<template> <div> <button>Subscribe</button> </div> </template> <script> import pubsub from 'pubsub-js' export default { name: 'SubscribeTest', mounted() { // 订阅 hello 这个消息(msgName是订阅的消息名,data是消息的内容) this.pubId = pubsub.subscribe('hello', function(msgName, data) { // console.log(this) // 注意没有使用箭头函数,此时这里的this是undefined console.log("Subscribe接收", msgName, data) }) }, beforeDestroy() { // 组件销毁后,关闭订阅 pubsub.unsubscribe(this.pubId) }, } </script> <style> </style>
效果展示:
-
实战演练:
修改之前的todo-list-bus,将里面的组件间通信修改成使用发布-订阅模式
添加一个编辑按钮,实现编辑功能
注意事项:添加isEdit时,会出现给对象新增一个属性失效,这是由于直接添加的数据没有Getter和Setter,也就是没有做数据代理,在Vue核心的15节(数据更新时的底层原理有讲过)。解决方案:
vm.$set(target,attribute,val)
vue-cli3.0版本后无法使用
foo.hasOwnProperty("bar")
,具体参考vue-cli3中不能使用hasOwnProperty的解决办法在设计编辑的输入框时,我们想要点击编辑然后就将焦点移到要编辑的数据上,但是发现失效了
// 编辑item handlerEdit(todo) { if (!Object.prototype.hasOwnProperty.call(todo, "isEdit")) { // 也可以使用 todo.isEdit === undefined 进行判断 // 只有当todo上没有isEdit属性时,才需要在他上面添加一个isEdit属性 this.$set(todo, 'isEdit', true) // console.log("Object.prototype.hasOwnProperty.call", Object.prototype.hasOwnProperty.call(todo, "isEdit")) } else { todo.isEdit = !todo.isEdit } // 这一行并不起效* this.$refs.inputTitle.focus() }
*这是由于只有当handlerEdit函数执行完才会执行解析模板并不是一修改数据就解析模板,只有解析模板后才能成功展示编辑的input框,当运行到
*
行时,由于input框还没有展示出来,所以此时就不起效了解决方案1:使用
setTimeout
函数,因为定时器是一个异步的操作setTimeout(() => { this.$refs.inputTitle.focus() }, 200)
解决方案2:使用
$nextTick
,$nextTick
指定的回调函数,会在DOM节点更新完毕后执行this.$nextTick(function(){ this.$refs.inputTitle.focus() })
参考文章: setTimeout时间设置为0详细解析
9、过渡和动画
9.1 手工实现
示例一:实现一个动态切换效果
CSS实现:
<template>
<div>
<button @click="change">显示/隐藏</button>
<h3 :class="active">你好啊</h3>
</div>
</template>
<script>
export default {
name: 'DemoTest',
data() {
return {
isShow: true,
active: [] // 注意,如果将active定义成字符串,使用=赋值,不会引起Vue的模板解析!
}
},
methods: {
// ? 无法实现class的切换效果???
change() {
if (this.active.length === 0) {
// this.active[0] = 'come'
this.active.unshift('come')
} else if (this.active.shift() != 'go') {
this.active.unshift('go')
} else {
console.log(this.active)
this.active.unshift('come')
}
}
},
}
</script>
<style scoped>
h3 {
background-color: orange;
}
.come {
/* 动画从左往右,from 到 to*/
animation: slide 1s;
}
.go {
/* 动画从右往左(reverse是动画反转,即是 to 到 from) */
animation: slide 1s reverse;
}
@keyframes slide {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0px);
}
}
</style>
半Vue实现
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition appear>
<h3 v-show='isShow'>你好啊!</h3>
</transition>
</div>
</template>
<script>
export default {
name: 'DemoTest',
data() {
return {
isShow: true
}
},
}
</script>
<style scoped>
h3 {
background-color: orange;
}
.v-enter-active{
/* 动画从左往右,from 到 to*/
animation: slide 1s;
}
.v-leave-active{
/* 动画从右往左(reverse是动画反转,即是 to 到 from) */
animation: slide 1s reverse;
}
@keyframes slide {
from{
transform: translateX(-100%);
}
to{
transform: translateX(0px);
}
}
</style>
注意:
-
如果给transition的name属性添加值,则
.v-enter-active
和.v-leave-active
前面的v都需要改成name对应的值 -
要想使DOM一上来就有动画效果,就需要设置
appear
属性,appear属性要设置为真或者直接写上appear属性方式一:<transition appear> 方式二:<transition :appear="true"> 如果直接是 appear="true",浏览器的控制台会报错,但是仍然有效果
纯Vue实现
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏 Demo2</button>
<transition appear name="hello">
<h3 v-show='isShow'>你好啊!</h3>
</transition>
</div>
</template>
<script>
export default {
name: 'DemoTest2',
data() {
return {
isShow: true
}
},
}
</script>
<style scoped>
h3 {
background-color: orange;
/* transition: .5s linear; */
}
/* 动画开始动画结束 */
.hello-enter-active,
.hello-leave-active {
transition: .5s linear;
}
/* 进入的起点(动画所到最左边),离开的终点 */
.hello-enter,
.hello-leave-to {
transform: translateX(-100%);
}
/* 进入的终点(动画所到最右边),离开的起点 */
.hello-enter-to,
.hello-leave {
transform: translateX(0);
}
</style>
注意:当我们有多个标签使用同一个动画时,必须使用transition-group
属性,里面的元素必须要有key属性
<transition-group appear name="hello">
<h3 v-show='isShow' key="1">你好啊!</h3>
<h3 v-show='isShow' key="2">你好啊!</h3>
</transition-group>
如果多个标签是紧挨着的,可以使用一个div包裹起来(但是如果互斥,则使用这种方式无法实现)
<transition appear name="hello">
<div v-show='isShow'>
<h3>你好啊!</h3>
<h3>你好啊!</h3>
</div>
</transition>
9.2 使用第三方库
这里演示使用Animate.css这个动画库实现动画效果
-
Step1:安装
npm install animate.css
-
Step2:引入
import 'animate.css'
-
Step3:
name="animate__animated animate__bounce"
是必须项enter-active-class
动画开始的效果leave-active-class
动画结束的效果<template> <div> <button @click="isShow = !isShow">显示/隐藏 Demo3</button> <transition-group appear name="animate__animated animate__bounce" enter-active-class="animate__hinge" leave-active-class="animate__backInUp"> <h3 v-show='!isShow' key="1">你好啊!</h3> <h3 v-show='isShow' key="2">你好啊!</h3> </transition-group> </div> </template> <script> import 'animate.css' export default { name:'DemoTest3', data() { return { isShow:true } }, } </script> <style scoped> h3 { background-color: orange; /* transition: .5s linear; */ } </style>
实战演练:
修改todo-list-bus案例,将MyList组件的删除和添加增加一个动画效果
10、Vue中AJAX的使用
10.1 快速体验
-
Step1:开启服务器
-
Step2:下载并引入axios
npm i axios # 下载axios import axios from 'axios' # 引入axios
关于axios这里就不多做介绍了,感兴趣的可以参考我之前写过的文章:【AJAX是什么】&【AJAX的基本使用】_
-
Step3:编码
<!-- 对应的结构 --> <h1>测试发送AJAX</h1> <button @click="getStudentMsgByAjax">获取学生信息</button> <!-- 对应的方法 --> methods: { getStudentMsgByAjax() { axios.get('http://localhost:5000/students').then( response => { console.log('请求成功了', response.data) }, error => { console.log('请求失败了', error.message) } ) } }
10.2 跨域问题
什么是跨域?
我们通常所说的跨域, 是由浏览器同源策略限制的一类请求场景。
同源策略(Same origin policy):是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现
为什么会出现跨域?
当前所处位置
http://localhost:8080/
,要发送的位置:http://localhost:5000
。协议和域名相同,但是端口号不同,违背了同源策略,此时请求发送出去了,同时服务器也接受了请求,但是返回的数据被浏览器拦截了,并不能被发送AJAX的程序接收如何解决跨域问题?
方案一:使用CORS解决跨域
CORS(Cross-Origin Resource Sharing,跨源资源共享)是一个系统,它由一系列传输的 HTTP 标头组成,这些 HTTP 标头决定浏览器是否阻止前端 JavaScript 代码获取跨源请求的响应。CORS 给了 web 服务器这样的权限,即服务器可以选择,允许跨源请求访问到它们的资源。
特点:后端解决,需要浏览器和后端同时支持,请求分为复杂请求和简单请求
方案二:使用JSONP解决跨域
JSONP(JSON with Padding)是JSON的一种"使用模式",可以让网页从别的网站获取资料,即跨域读取数据。
特点:前后端一起解决,只能解决GET请求跨域
方案三:配置代理服务器(常用、推荐)
配置一个代理服务器
http://localhost:8080/
(代理服务器),让它和http://localhost:8080/
(AJAX发送方)进行交互,这样就符合了同源策略。同源问题的本质是浏览器的为了安全而增加的限制!这样 AJAX的接收方 发送的数据有代理服务器接收,则有代理服务器交给AJAX的发送方,这样就不违背同源策略了,成功解决跨域问题。根本原因是利用服务器之间通信不使用同源策略开启代理服务器的方法(反向代理)
借助nginx
借助vue-cli
在
vue.config.js
中配置以下参数devServer: { // proxy中配置的是AJAX接收方的访问地址 proxy: 'http://localhost:5000' }
注意:现在AJAX的访问地址就是
http://localhost:8080/students
了,而不是http://localhost:5000/students
。当你代理服务器中有所请求的资源,就不会转发请求
示例:
借助vue-cli解决跨域问题
App.vue:
<template>
<div id="app">
<h1>测试发送AJAX</h1>
<button @click="getStudentMsgByAjax">获取学生信息</button><br/>
<button @click="getCarMsgByAjax">获取汽车信息</button>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'App',
components: {},
methods: {
getStudentMsgByAjax() {
axios.get('http://localhost:8080/server1/students').then(
response => {
console.log('server1请求成功了', response.data)
},
error => {
console.log('server1请求失败了', error.message)
}
)
},
getCarMsgByAjax() {
axios.get('http://localhost:8080/server2/cars').then(
response => {
console.log('server2请求成功了', response.data)
},
error => {
console.log('server2请求失败了', error.message)
}
)
}
},
}
</script>
<style>
</style>
vue.config.js:
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 方式一:开启代理服务器(只能代理一个服务器)
/* devServer: {
proxy: 'http://localhost:5000'
} */
// 方式二:开启代理服务器(代理多个服务器)
devServer: {
proxy: {
'/server1': {
target: 'http://localhost:5000',
// 此时访问的路径是 http://localhost:5000/server1/students,需要使用正则表达式去掉 /server1
pathRewrite: { '^/server1': '' },
// 用于支持 websocket
ws: true,
// 代理服务器告诉server1请求来自于哪里(true说谎:本来是来自8080,结果说来自5000,false如实回答:来自8080)
changeOrigin: true // 用于控制请求头的host属性的值
},
'/server2': {
target: 'http://localhost:5001',
pathRewrite: { '^/server2': '' },
ws: true,
changeOrigin: true
}
}
}
})
效果展示:
实战演练:
开发一个GitHub用户搜索框,效果展示如下:
注意事项:
通过import方式引入第三方库,vue-cli会对引入的第三方库进行严格检查,只要其中有用到但没有找到的资源就会直接报错,
解决方法有如下几种:
1)将缺失的资源下载到项目中
2)如果缺失的资源我们当前并没有用到,可以直接注释掉或者删除掉(不推荐)
3)不将要引入的第三方库放在assets目录下,而是放在public中,直接在页面中使用link标签引入,而不是在组件中引入
对象传参(头次见,好用😄)
this.info = {...this.info, ...dataObj}
使用
vue_resource
替代axios(这个用的比较少,基本上已经停止维护了,Vue官方推荐使用axios)
11、插槽
-
什么是插槽?
插槽(Slot)是Vue提供的一个概念,它允许开发者在组件外部将不确定的部分定义为一个插槽,然后整体解析到组件的内部。插槽分为插槽的入口和插槽的出口。参考下图:
作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于 父组件 ===> 子组件。
-
插槽的分类:
- 默认插槽:没有名字的插槽
- 具名插槽:具有名字插槽
- 作用域插槽:作用域插槽其实就是带数据的插槽,即带参数的插槽,简单的来说就是子组件提供给父组件的参数,该参数仅限于插槽中使用,父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容
备注:在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute,而在3.0中,已经移除了旧的写法
11.1 默认插槽
示例:
App.vue:
<template>
<div id="app">
<div class="container">
<CategoryTest title="美食">
<!-- 使用插槽,将内容传递到组件中 -->
<img src="https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/12497/202301241615404.jpeg" alt="">
</CategoryTest>
<CategoryTest title="游戏">
<ul>
<li v-for="(game,index) in games" :key="index">{{game}}</li>
</ul>
</CategoryTest>
<CategoryTest title="电影">
<video controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
</CategoryTest>
</div>
</div>
</template>
<script>
import CategoryTest from './components/CategoryTest'
export default {
name: 'App',
components: {
CategoryTest
},
data() {
return {
foods: ['火锅', '烧烤', '小龙虾', '牛排'],
games: ['红色警戒', '穿越火线', '劲舞团', '超级玛丽'],
films: ['《教父》', '《拆弹专家》', '《你好,李焕英》', '《尚硅谷》']
}
},
}
</script>
<style>
.container {
display: flex;
justify-content: space-around;
}
h3 {
text-align: center;
background-color: yellow;
}
img {
width: 100%;
}
video {
width: 100%;
}
</style>
CategoryTest.vue:
注意:插槽标签
<slot>
中可以有内容,当外部没有传值给插槽时,就会显示slot中的内容,当传值就以传的值为准
<template>
<div class="category">
<h3>{{title}}分类</h3>
<!-- 使用插槽(使用它占位,如果在App中传来元素,就在这里展示) -->
<slot></slot>
</div>
</template>
<script>
export default {
name: 'CategoryTest',
props:['title']
}
</script>
<style scoped>
.category {
background-color: skyblue;
width: 200px;
height: 300px;
}
</style>
11.2 具名插槽
App.vue:
<CategoryTest title="电影">
<video slot="video" controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
</CategoryTest>
CategoryTest.vue:
<slot name="video"></slot>
如果slot属性是加在template
标签上:
<template slot="list">
<ul>
<li>经典</li>
<li>热门</li>
<li>推荐</li>
</ul>
</template>
2.6提供一个新写法
<template v-slot:list>
<ul>
<li>经典</li>
<li>热门</li>
<li>推荐</li>
</ul>
</template>
11.3 作用域插槽
用于将插槽出口所在组件的数据传递到插槽入口所在组件(只能在组件中的插槽内部使用),也就是将子组件中的数据传递给父组件
App.vue
<template>
<div id="app">
<div class="container">
<CategoryTest title="游戏">
<template scope="data"> <!-- 推荐奖 scope 替换成 slot-scope -->
<ul>
<li v-for="(game,index) in data.gamesData" :key="index">{{game}}</li>
</ul>
</template>
</CategoryTest>
<CategoryTest title="游戏">
<template scope="{gamesData}"> <!-- 使用ES6的解构赋值 -->
<ul>
<li v-for="(game,index) in gamesData" :key="index">{{game}}</li>
</ul>
</template>
</CategoryTest>
</div>
</div>
</template>
<script>
import CategoryTest from './components/CategoryTest'
export default {
name: 'App',
components: {
CategoryTest
}
}
</script>
<style>
.container {
display: flex;
justify-content: space-around;
}
h3 {
text-align: center;
background-color: yellow;
}
</style>
CategoryTest.vue:
<template>
<div class="category">
<h3>{{title}}分类</h3>
<!-- 使用插槽(使用它占位,如果在App中传来元素,就在这里展示) -->
<slot :gamesData="games"></slot>
</div>
</template>
<script>
export default {
name: 'CategoryTest',
props: ['title'],
data() {
return {
games: ['红色警戒', '穿越火线', '劲舞团', '超级玛丽']
}
}
}
</script>
<style scoped>
.category {
background-color: skyblue;
width: 200px;
height: 300px;
}
</style>
12、Vuex
12.1 Vuex介绍
-
Vuex是什么?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库(可以认为是状态管理模式的一种具体实现)。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
简而言之:Vuex就是专门提供组件间数据共享的。前面最先使用props来传递组件间的数据,如果组件层数过多,会存在冗余的props,后面我们使用了全局事件总来实现组件间数据的传递,但是当一个项目中组件过多,且组件间数据的传递较多,这是用使用全局事件总线会显得很复杂。
vuex官方文档:Vuex 是什么? | Vuex (vuejs.org)
Github地址:vuejs/vuex
-
什么是状态管理模式?
状态管理模式是指将项目中所有组件的共享状态抽取出来,以一个全局单例模式进行管理。这种模式下任何组件都能获取状态或者触发行为。一个状态管理应用应该包含以下三部分
- 状态,驱动应用的数据源;
- 视图,以声明方式将状态映射到视图;
- 操作,响应在视图上的用户输入导致的状态变化
-
为什么使用状态管理模式?
能够让我们的代码更加简洁、更加结构化且易维护。
-
什么使用使用Vuex?
- 多个组件依赖同一状态。
- 来自不同组件的行为需要变更同一状态。
温馨提示:如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。
备注:Actions、Mutations、State都是对象,并且它们仨都被store对象管理
12.2 快速体验
-
Step1:安装Vuex
温馨提示:再2022年2月7日,
nmp i vuex
指令默认安装Vuex4版本了。Vuex4版本只能在Vue3中使用,也就是说Vue2中只能使用Vuex3版本,Vue3中推荐使用Vuex4版本npm i vuex@3 # 注意一定要加@3指定Vuex的版本,否则默认就是安装Vuex4
-
Step2:编写store
- 文件所在目录:
src/store/index.js
或者是src/store.js
- 要让store能够被所有组件识别
- 要让store能够管理Actions、Mutations、State三个对象
index.js:
// 该文件用于创建Vuex最为核心的store import Vue from 'vue' import Vuex from 'vuex' // 让Vuex能够被所有组件识别 Vue.use(Vuex) // 准备actions,用于响应组件中的动作 const actions = { add(context, value) { console.log('Actions中的add方法被调用了', context, value) context.commit('ADD', value) }, sub(context, value) { console.log('Actions中的sub方法被调用了', context, value) context.commit('SUB', value) } }; // 准备mutations,用于操作数据(state) const mutations = { ADD(state, value) { console.log('Mutations中的ADD方法被调用了', state, value) state.sum += value }, SUB(state, value) { console.log('Mutations中的SUB方法被调用了', state, value) state.sum -= value }, }; // 准备state,用户存储数据 const state = { sum: 0 // 当前所求的和 }; // 创建Store对象 export default new Vuex.Store({ /* actions: actions, mutations: mutations, state: state */ // 对象和属性同名,可以简写(ES6) actions, mutations, state });
- 文件所在目录:
-
Step3:编写main.js
-
文件执行时,会优先解析import所对应的文件,这也就是为什么Vue.use(Vuex)要写在store后面(详情见P108 17:56)
所以要明确,Store对象的创建是通过Vuex对象的Store构造函数生成的,所以要先Vue.use(Vuex)
import Vue from 'vue' import App from './App.vue' // import Store from './store/index' // index是能够被Vue-cli识别的,可以省略 import store from './store' Vue.config.productionTip = false new Vue({ render: h => h(App), // store: store // 对象和变量同名,可以简写(ES6语法) store }).$mount('#app')
-
-
Step3:编码
CountTest.vue
<template> <div> <h1>当前求和为:{{ $store.state.sum}}</h1> <!-- 这里可以通过添加.number,也可以使用v-bind绑定option的所有value属性 --> <select v-model.number="n" name="" id=""> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="increment">+</button> <button @click="decrement">-</button> <button @click="incrementOdd">当前求和为奇数再加</button> <button @click="incrementWait">等一等再加</button> </div> </template> <script> export default { name: 'CountTest', data() { return { n: 1 // 当前用户选择的数字 } }, methods: { increment() { this.$store.dispatch('add', this.n) }, decrement() { this.$store.dispatch('sub', this.n) }, incrementOdd() { if (this.$store.state.sum % 2) { // 如果是奇数就加 // 可以直接跳过Actions,直接将数据传递给Mutations this.$store.commit('ADD', this.n) } }, incrementWait(){ setTimeout(()=>{ this.$store.dispatch('add',this.n) }, 500) } } } </script> <style scoped> </style>>
App.vue:
<template> <div id="app"> <CountTest/> </div> </template> <script> import CountTest from './components/CountTest' export default { name: 'App', components: { CountTest }, mounted() { // console.log('APP', this) }, } </script> <style> </style>
-
Step4:测试
12.3 getters配置项
在store目录下的index.js中配置getters
// 准备getters,用于加工state中的数据
const getters = {
bigSum(state) {
return state.sum * 10
}
}
然后就可以使用 $store.getters.bigSum
获取到加工的数据了
12.4 mapState&mapGetters
首先需要在组件中引入mapState
和mapGetters
import {mapState, mapGetters} from 'vuex'
- 借助
mapState
简化计算属性(computed)中获取状态this.$store.state.a
- 借助
mapGetters
简化计算属性(computed)中获取状态this.$store.bigSum.
computed: {
// 手写计算属性
/* a() {
return this.$store.state.sum - 1
},
b() {
return this.$store.state.sum + 1
},
c() {
return this.$store.state.sum / 2
},
d() {
return this.$store.state.sum * 2
} */
// 可以通过mapState将上面那段代码进行简写(使用ES6对象展开运算符将此对象混入到外部对象中)
// ...mapState({a:'a',b:'b',c:'c',d:'d'})
// 当对象名和值相同时,可以进一步简写
...mapState(['a','b','c','d']),
/* --------------------------------------------------- */
/* bigSum(){
return this.$store.getters.bigSum
} */
// 借助mapGetters上面的代码进行简写
...mapGetters(['bigSum'])
}
12.5 mapActions&mapMutations
首先需要在组件中引入mapActions
和mapMutations
import {mapActions, mapMutations} from 'vuex'
- 借助
mapActions
简化methos
中this.$store.commit()
- 借助
mapMutations
简化methods
中this.$store.dispatch()
<button @click="increment(n)">
methods: {
// 原始写法(increment不需要传参)
/* increment() {
this.$store.commit('ADD', this.n)
}, */
// 使用mapActions属性对上面那段代码进行简写
/* increment(value) {
this.$store.commit('ADD', value)
}, */
// 需要注意使用 mapMutations 等价于上面的代码,传的值是event
// (因为他会有一个默认参数,前面我们有传参,所以就默认是事件,所以需要主动传参)
...mapMutations({ increment: 'ADD'})
}
mapActions和mapMutations一样,也有应该默认的参数,所以必须传值,否则传的就是event对象
<button @click="decrement(n)">-</button>
...mapActions({decrement:'sub'}),
实战演练:使用Vuex实现多组件间数据的共享
对于生成唯一的id,可以使用nanoid
import {nanoid} from 'nanoid' const id = id:nanoid()
12.6 Vuex中的模块化技术
前面我们通过给store对象添加Actions、Mutations、State、Getters四个对象,用于实现组件间的数据共享,但是存在应该问题,当共享的数据很多时,很让Vuex显得十分的环论,此时我们就需要使用Vuex的模块化技术了。
在/store/index.js
中对store的四个对象进行分类
const personObj = {
namespace:true,
const actions:{},
const mutations:{},
const state:{},
const getters{}
}
const carObj = {
namespace:true,
const actions:{},
const mutations:{},
const state:{},
const getters{}
}
export default new Vue.Store({
moudules:{
// personObj:personObj 使用对象的简写形式
personObj,
carObj
}
})
需要注意:
- 如果我们想要通过…mapState第一个参数读取personObj中的属性,并进行解构赋值,必须写上
namespace:true
// 不设置namespace=true,它默认是false,则只能通过下面这种方式读取
...mapState('personObj','carObj')
// 通过这种方式引入,都需要添加应该前缀personObj,比如我们要读取personObj中的a属性,就需要写成personObj.a
// 设置namespace=true
...mapState('personObj',['a','b'])
- 此时如果我们项直接调用commit中的方法,由于现在store中存在多个Mutations属性,所以我们需要指明我们要使用哪一个Mutations中的commit方法
...mapMutations('personObj',{increment:'ADD'})
// 现在就指明了要使用personObj中的Mutations属性中Commit
- 同理,我们如果想要调用Actions中的dispatch方法,也需要指明
...mapActions('personObj',{increment:'add'})
- getters也是一样
...mapGetters('personObj',['bigSum'])
- 对于非简写形式
add(){
//this.$store.personObj.state.personList
this.$store.commit('personObj/ADD',personList)
}
- getters中没有分类,所以获取getters中的数据
bigSum(){
this.$store.getters['personObj/bigSum']
}
通过5和6我们可以发现,不使用简写显得很不简洁、美观,所以推荐直接使用map的写法。但并不是map的写法就完胜非简写形式,当我们要对数据进行逻辑判断时,我们需要使用非简写形式,因为map写法无法进行数据的判断!
13、路由
13.1 路由介绍
-
什么是路由?
路由(routing)是指分组从源到目的地时,决定端到端路径的网络范围的进程。简而言之(粗略地讲)路由就是用来根据路由表转发IP数据包的一个装置,能够实现多个主机之间的互通,相当于一个快递中间站,属于硬件层面上的。
但在Vue 中,路由(Vue Router)是一个不太相同的概念,它是SPA(Single Page Application,单页面应用)的路径管理器,是WebApp的链接路径管理系统,用于页面跳转,属于软件层面上的(其实Vue Router就是一个插件库,本质是一组key-value的对应关系)。
-
什么是SPA?
SPA(Single Page Application)是单页面应用,也就是说整个应用只有一个页面
-
为什么要使用SPA?
早期的Web开发,都是多页面应用,一个应用中,存在多给页面,页面页面之间使用a标签或者
href
属性,实现页面之间的跳转,这样存在一个弊端,页面频繁跳转会导致页面抖动1。而使用路由后,就能实现一个单页面应用,这样就能够防止页面抖动,页面间的跳转都是通过路由(Vue Router)实现的,跳转编程了局部刷新 -
路由的分类
-
前端路由:
- 理解:value是component,用于展示页面的内容(就是Vue Router)
- 工作过程:当浏览器的路径发生改变时,对应组件就会显示
-
后端路由:
- 理解:value是fanction,用于处理客户提交的请求(其实就是DispatchServlet)
- 工作过程:服务器接收一个请求时,根据请求路径找到匹配的函数来处理请求,然后返回响应数据
-
13.2 快速体验
-
Step1:安装Vue-Router
npm i vue-router@3
温馨提示:2022年2月7日以后,vue-router安装默认是4版本,而vue-router的4版本只能在vue3中使用,vue-router3只能在vue2中使用(和Vuex类似)
-
Step2:将vue-router引入项目中
在
src
目录先创建router目录,然后编写index.js,内容如下:index.js:
import VueRouter from 'vue-router' import AboutTest from '../components/AboutTest' import HomeTest from '../components/HomeTest' // 创建一个路由 export default new VueRouter({ routes: [{ path: '/about', component: AboutTest }, { path: '/home', component: HomeTest } ] })
备注:这样写,在引入Vuex中已经讲过了,就JS文件在初始化时,会优先解析import对应的js文件,如果我们在main.js中引入,后面有需要使用Vue.use(VueRouter),就会报错,因为VueRouter
main.js:
import Vue from 'vue' import App from './App.vue' // 引入vue-router import VueRouter from 'vue-router' // 引入路由器对象 // import router from './router/index' // index可以省略 import router from './router' Vue.config.productionTip = false Vue.use(VueRouter) new Vue({ render: h => h(App), // router: router // 使用对象的简写形式 router }).$mount('#app')
-
Step3:编写组件
1)HomeTest.vue:
<template> <div> <h2>我是Home的内容</h2> </div> </template> <script> export default { name: 'HomeTest' } </script>
2)AboutTest.vue:
<template> <div> <h2>我是About的内容</h2> </div> </template> <script> export default { name:'AboutTest' } </script>
3)App.vue
<template> <div id="app"> <div class="row"> <div class="col-xs-offset-2 col-xs-8"> <div class="page-header"> <h2>Vue Router Demo</h2> </div> </div> </div> <div class="row"> <div class="col-xs-2 col-xs-offset-2"> <div class="list-group"> <!-- 原始使用a标签实现页面跳转(多页面应用) --> <!-- <a class="list-group-item active" href="./about.html">About</a> --> <!-- <a class="list-group-item" href="./home.html">Home</a> --> <!-- 使用vue-router实现页面跳转(单页面应用) --> <router-link class="list-group-item" active-class="active" to="/about">About</router-link> <router-link class="list-group-item" active-class="active" to="/home">Home</router-link> </div> </div> <div class="col-xs-6"> <div class="panel"> <div class="panel-body"> <!-- 指定组件呈现的位置 --> <router-view></router-view> </div> </div> </div> </div> </div> </template> <script> export default { name: 'App' } </script>
-
Step4:测试
备注:
- 没有显示的组件就销毁了。
- 按照开发规范,我们会将路由组件放在pages目录下,一般组件放在components目录下
- 每一个组件都有一个
$route
,挂载VC身上,通过$route
能够获取自己路由相关信息 - 一个应用只有一个
$router
,可以通过VC获取
实战演练:
嵌套路由,详细代码请参考博主的Github\Gitee仓库
13.3 路由嵌套
略……详情请参考博主的Github\Gitee参考库
import VueRouter from 'vue-router'
import AboutTest from '../pages/AboutTest'
import HomeTest from '../pages/HomeTest'
import NewsTest from '../pages/NewsTest'
import MessageTest from '../pages/MessageTest'
import DetailTest from '../pages/DetailTest'
// 创建一个路由
export default new VueRouter({
routes: [{
path: '/about',
component: AboutTest
},
{
path: '/home',
component: HomeTest,
children: [{
path: 'news',
component: NewsTest
},
{
path: 'message',
component: MessageTest,
children: [{
path: 'detail',
component: DetailTest
}]
}
]
}
]
})
13.4 路由传参
效果展示:
MessgeTest.vue:消息的发送者
<div>
<ul>
<li v-for="data in dataList" :key="data.id">
<!--方式一 -->
<!-- <router-link :to="`/home/message/detail?id=666&title=${data.title}`">{{data.title}}</router-link> -->
<!-- 方式二(推荐使用,虽然配置多,但是清晰明了) -->
<router-link :to="{
path:'/home/message/detail',
query:{
id:data.id,
title:data.title
}
}">
{{data.title}}
</router-link>
</li>
</ul>
<hr>
<router-view></router-view>
</div>
DetailTest.vue:消息的接收者
<template>
<div>
<ul>
<li>消息编号:{{$route.query.id}}</li>
<li>消息标题:{{$route.query.title}}</li>
</ul>
</div>
</template>
<script>
export default {
name:'DetailTest',
mounted(){
console.log(this.$route.query.id)
}
}
</script>
<style>
</style>
备注:这种参数被称为query
参数
除了query
参数,还有params
参数(这是一种rest风格的传参方式)
-
Step1:需要配置
src/router/index.js
文件中配置path,进行占位{ path: 'message', component: MessageTest, children: [{ name: 'detail', // 占位 path: 'detail/:id/:title', component: DetailTest }] }
-
Step2:进行parms方式传参,注意需要对字符串进行JS解析,同时使用模板字符串
<!-- params传参,方式一: --> <router-link :to="`/home/message/detail/${data.id}/${data.title}`">{{data.title}}</router-link> <!-- params传参,方式二:(上面没有使用name属性,因为我没有试出来🤣) --> <router-link :to="{ name: 'detail', params:{ id:data.id, title:data.title } }"> {{data.title}} </router-link>
==注意:==使用方式二,只能使用name属性来匹配路由,不能使用path,否则直接报错
-
Step3:接收
$route.params.id $route.params.title
13.5 路由的命名
这个和代理服务器中服务器名称类似(●ˇ∀ˇ●),主要起到简化路径书写的作用
routes: [{
name: 'a'
path: '/about',
component: AboutTest
}
}]
此时router-link标签中的to就可以使用以下写法了
<!--不加name属性的写法-->
<router-link to="/about"></router-link>
<!--添加name属性的写法-->
<router-link :to="{name:'about'}"></router-link>
13.6 路由的props
配置
前面我们学习了路由的两种传参方式
query
和params
,这两种方式存在一个小毛病,也就是接收参数的组件,如果想要使用,都需要通过this.$route.query.属性
或者this.$route.params.属性
来获取参数,这样写会显得较为繁琐(其实我感觉没什么大不了的,可能是Vue的开发者比较喜欢追求完美吧,毕竟别人的学艺术的),完美可以props
属性来简化这种写法
通过在路由的配置文件,也就是/src/router/index.js
中配置props,需要明确的是,那个组件需要用到这个属性,就在该组件所在路由中配置一个props属性,然后还需要再改组件中配置一个props,然后就能够直接在该组件中调用该属性了
- 方式一:传对象
{
path: 'message',
component: MessageTest,
children: [{
path: 'detail',
component: DetailTest,
// props的第一种写法,该对象中所有的key-value都以props的形式传递到DetailTest组件中
props: { a: 1 }
}],
}
DetailTest.vue组件进行接收:
首先声明 props:['a']
然后就可以直接使用了 <li>props传递值:a={{a}}</li>
不足:只能传固定值
- 方式二:传boolean
// 传递布尔值,布尔值为真,就会将该路由组件所有的params参数传递给DetailTest组件
props: true
MessageTest.vue组件发送:
<router-link :to="{
name: 'detail',
params:{
id:data.id,
title:data.title
}
}">
DetailTest.vue组件接收:
先声明: props: ['id', 'title']
然后直接使用:
<!-- 通过props:true 直接使用传递的属性 -->
<li>消息编号:{{ id }}</li>
<li>消息编号:{{title }}</li>
不足:只能传递params参数
- 方式三:传函数
// 传递函数,函数返回值中所有的key-value都会以props的形式传递给DetailTest组件
props($route) {
// 并且这个函数自带一个参数,也就是$route
console.log($route)
return { id: $route.params.id, title: $route.params.title }
}
// 使用解构赋值进行简写
props(params:{id,title}) {
// 并且这个函数自带一个参数,也就是$route
console.log($route)
return { id, title}
}
MessageTest.vue组件发送数据
DetailTest.vue组件接收数据:
先声明: props: ['id', 'title']
然后直接使用:
<!-- 通过props:true 直接使用传递的属性 -->
<li>消息编号:{{ id }}</li>
<li>消息编号:{{title }}</li>
相较于前面两种方式来说更加加完美
不足:代码写的相对较多一点
总的来讲各有优缺吧,你只要能实现就好(黑猫白猫能抓住老鼠就是好猫)
13.7 router-link的replace属性
-
router-linke默认开启的是一个push模式
push模式,就是每点击一次视图切换,都会将之前的URL进行push存入一个栈中,每点击一次回退按钮,就pop出一个URL同时回退到之前的视图,知道栈中所有的URL被pop
-
除了push模式,还有一种模式,称为replace模式,所谓的replace模式,就是指,每次进行一次视图切换,上一个视图都会直接被销毁,并不会push到栈中,所以无法进行回退
直接router-link标签中添加replace属性
// 完整写法 :replace="true" // 简写形式 replace
13.8 编程式路由导航
前面完美使用
router-link
标签实现组件间的切换,本质上router-link
是一个a标签(底层会将router-link标签转换成一个a标签),但如果我们要设置一个按钮实现跳转,又该怎么办呢?其实也可以使用router-link包裹,按钮然后实现组件间的切换,但是我们有一种更好的方式,也就是编程式路由导航
示例:
这里就暂且使用伪代码的形式进行演示了,详细代码请参考博主的Github或Gitee仓库
<!-- template -->
<button @click="jump(data)">点击跳转</button>
<!-- js -->
methods:{
jump(data){
// 使用$router.push实现网页跳转(这里表示真正的跳转,而是组件间的切换)
this.$router.push({
name:'detail' // 这里使用name属性,当然也可以直接使用path明确指定要切换的组件
// 可以给jump函数传递参数,然后通过路由传递给其它组件
query:{
id: data.id,
title: data.title
}
})
}
}
// 使用$router.back实现网页回退
this.$router.back({
console.log("网页回退")
}
})
// 使用$router.forward实现网页前进
this.$router.forward({
console.log("网页回退")
}
})
// 使用$router.go实现网页任意的前进或后退(正数表示前景,负数表示后退)
this.$router.go({
this.$router.go(-2)
console.log("网页连续后退两次")
}
})
13.9 缓存路由组件
使用<keep-alive>
包裹<router-view>
,即可实现组件中数据的缓存
<router-link to"/home"></router-link>
<router-link to"/about"></router-link>
<keep-alive include="HomeTest">
<!-- 现在用户输入home组件中的数据就能够进行进行缓存了,切换还能展示数据
但是不会缓存about组件中的数据。不加include属性就是默认缓存所有的路由组件中的数据
-->
<router-view></router-view>
</keep-alive>
也可以同时配置多个缓存
<keep-alive :include="['HomeTest','AboutTest']">
<router-view></router-view>
</keep-alive>
13.10 生命周期钩子
本小节将要学习两个全新的生命周期钩子,这两个钩子是独属于路由组件的。
-
activated()
:路由组件激活被调用。激活是指路由组件被页面展示,也就是当用户点击router-link标签跳转到对应路由组件,此时该路由组件就处于激活状态
-
deactivated()
:路由组件失活被调用失活是指路由组件被隐藏(切换),当用户点击router-link标签跳转到另一个组件,此时该路由组件被切换了,此时就处于失活状态
这个生命周期钩子常用于一下场景:
当我们想要在路由组件销毁时,触发一个方法,但该路由组件使用了缓存(
<keep-alive>
),此时该路由组件被切换不会被销毁,也就无法触发destroyed()
方法,这就需要使用deactivated()
钩子了。同理
mouted
钩子只有组件在初始化时才会被调用,加入我们切换组件且组件使用了缓存,这时是不会触发mouted
钩子的,这就需要使用activated()
钩子了
13.11 路由守卫
路由守卫就类似于一个拦截器
在src/router/index
中编写
- 全局前置路由守卫:
router.beforeEach({})
const router = new VueRouter({
routes:[
{
path: '/home',
component:HomeTest,
meta:{isAuth:true}
}
]
})
// 全局前置路由守卫(路由初始前被调用,路由切换前被调用)
router.beforeEach((to, from, next)=>{
console.log(to,from,next)
// 判断是否需要鉴权
if(to.meta.isAuth){
// 需要鉴定权限
if(localStorage.getItem('user') != null && localStorage.getItem('user') != ''){
// 满足条件放行
next()
}
}
// 放行
next()
})
export default router
- 全局后置路由守卫:
router.afterEach({})
const router = new VueRouter({
routes:[
{
path: '/home',
component:HomeTest,
meta:{title:'主页'}
}
]
})
// 全局后置路由守卫(路由初始后被调用,路由切换后被调用)
router.afterEach((to, from)=>{
console.log(to,from)
document.title = to.meta.title || '用户中心系统'
})
export default router
应用场景:Vue开发项目都是开发单页面应用,这就导致我们切换组件不会切换网页的标题,为了实现切换组件的同时切换网页标题,就需要使用后置路由守卫
- 独享路由守卫:
beforeEnter
(前置独享路由守卫,注意没有后置独享路由守卫)
export default new VueRouter({
routes:[
{
path: '/home',
component:HomeTest,
// 只针对HomeTest组件起作用(只读HomeTest进行鉴权,路由初始前被调用,路由切换前被调用)
beforeEnter((to, from, next)=>{
if(localStorage.getItem('user') != null && localStorage.getItem('user') != ''){
// 满足条件放行
next()
}
})
}
]
})
- 组件内路由守卫:
beforeRouterEnter
(进入路由守卫),beforeRouterLeave
(离开路由守卫)
在HomeTest.vue组件中进行编写
<script>
export default {
name: 'Abouttest',
// 进入如有守卫:通过路由规则进入该组件时被调用
beforeRouterEnter(to, from, next){
},
// 离开路由守卫
beforeRouterLeave(to, from, next){
}
}
</script>
注意:和全局路由守卫的区别,全局路由守卫,是组件切换时,立马执行前置路由守卫,紧接着就执行后置路由守卫;但组件内路由守卫,是组件切换时,立马执行进入路由守卫,但是并不会紧接着触发离开路由守卫,因为此时我们还没有离开该组件,只有当我们离开该组件时才会调用离开路由组件
13.12 路由的两种工作模式
vue-router 默认为 hash 模式,使用 URL 的 hash 来模拟一个完整的 URL,当 URL 改变时,页面不会重新加载;# 就是 hash符号,中文名为哈希符或者锚点,在 hash 符号后的值称为 hash 值
-
hash模式:监听浏览器地址hash值变化,执行相应的js切换网页
-
history模式:利用history API实现url地址改变,网页内容改变
-
两者的区别:
-
hash模式不美观,但是 # 后面的内容不会发送给服务器,兼容性特别好。
使用hash模式,如果地址通过第三方手机App分享,若App校验严格,则地址会被标记为不合法
-
history模式美观,但是兼容性较差,应用部署上线时,需要后端人员的出手才能解决刷新页面服务端404的问题
-
14、Vue的UI组件库
- 移动端常用的UI组件库
- PC端常用的UI组件库
如果还有其它好用的组件库,欢迎评论区补充
参考资料:
抖动:是指由于用户频繁操作,导致浏览器出现颤动的现象,抖动会影响用户体验 ↩︎