零、介绍
Vue:用于构建用户界面的渐进式JavaScript框架
Vue允许开发者将网页分割成多个可复用的组件,每个组件都包含各自的html,css,js
Vue特点:
- 组件化,一个组件即为.vue文件,包括html、css、js;
- 声明式代码,编码人员无需直接操作DOM;
- 使用虚拟DOM和优秀的Diff算法,尽量复用DOM节点;
一、Vue核心
vue.js通过script 标签引入,Vue
会被注册为一个全局变量,可以理解为这时候Vue就是一个类,可以通过Vue构造函数来新建Vue实例。
简单例子eg:
<head>
...
<!--导入vue.js -->
<script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!-- html中需要一个容器来与vue实例绑定-->
<div id="root">
<h1>
Hello!{{name}}
</h1>
</div>
<script>
<!--实例化一个vue对象,构造函数是一个配置对象-->
var demon = new vue(
{
<!--通过id选择器与上面的容器进行绑定-->
el:"#root",
data:{
name:"world"
}
}
)
</script>
</body>
- Vue实例中,构造函数需要传入的参数是一个对象,称为配置对象,改对象中包括el、data等属性
- 与Vue实例绑定的容器内的代码,称为Vue模板,代码主要还是html格式,但会涉及到一些模板语法,如{{…}}
1.1 模板语法
插值语法一般用于标签体内容;<>外
指令语法一般用于标签的属性;<>内
插值语法
上述的{{name}}就是插值语法;
-
功能:用于解析标签体内容
-
写法:{{xxx}},xxx是js表达式,且可以直接读取data中所有属性;
指令语法
- 功能:用于解析标签(包括:标签属性、标签体内容、绑定事件)
- 举例:v-bind:href=“xxx”,xxx同样写为js表达式
假设vue实例如下:
new Vue({
el:#root,
data:{
name:'jack',
url:'http://www.baidu.com'
}
})
那么可通过指令语法将url与a标签中的url绑定;
<div id="root">
<a v-bind:href="url">百度一下</a>
</div>
-
如果没有v-bind: 。那么<a href=“…” 就是一个普通的html属性
-
加上了v-bind,“”里的内容就当成了JS表达式去执行,即读取url属性
-
” v-bind:“可以简写为“ : ”
-
vue的指令语法一般都是以v-开头,
1.2 数据绑定
单向绑定:
上面举的v-bind的都是单向绑定,即data中的数据改变,会影响到页面中的值;但是页面值的改变并不会影响vue实例的数据;
双向绑定
通过v-model指令,可以实现双向绑定;
<body>
<div id="demo">
单向绑定:<input type="text" v-bind:value="name"><br>
双向绑定:<input type="text" v-model="name"><br>
<h1>vue实例中的数据情况:name:{{name}}</h1>
</div>
<script type="text/javascript" src="../vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#demo',
data: {
name:'君'
}
})
</script>
</body>
注意:v-model只能用于表单类元素上;v-model:value="xxx"可以简写为v-model=“name”,因为v-model默认收集的就是value值;
1.3MVVM模型
M:Model 模型:data中的数据
V:View 视图:模板代码
VM:ViewModel 视图模型:Vue实例
- data中的所有属性,最后都出现在了vm(vue实例)上;
- vm上所有的属性,以及Vue原型上的所有属性,在模板代码中都可以直接使用;
1.4 数据代理
Object.defineProperty()方法:定义(添加)对象属性的方法;
Object.defineProperty(对象, ‘属性名’,配置对象)
eg:
var student={
name:'张三',
sex:'男'
}
Object.defineProperty(student,'age',{
value:18, //设置数组值;
enumerable:true, //控制属性是否可以枚举,默认false
writable:true, //控制属性是否可以被修改,默认false
configurable:ture, //控制属性是否可以被删除,魔法false
get(){ //读取属性值,当有人试图输出student.age时,get()就会被调用
...;
return xxx;
}
set(value){ //设置属性值,当有人试图修改age属性值时,set()就会被调用
xxx = value;
}
})
数据代理:通过一个对象,代理对另一个对象中属性的操作(读写)
使用Object.defineProperty方法来实现;
简单举例:
var obj1 ={x:100};
var obj2 ={y:200};
Object.defineProperty(obj2,'x',{
get(){
return obj1.x;
}
set(value){
obj1.x = value;
}
})
//即通过obj2来读写obj1的x
转念想一想,为什么vue实例vm可以直接通过vm.name来访问data里的name,其实这也是数据代理。
Vue中的数据代理:即通过vm对象来代理data对象中属性的操作(读写)
Vue.set()方法
后期为了能够添加响应式属性(),需要通过Vue.set方法添加属性;
Vue.set(待加入属性的对象,‘需要添加的属性’,‘添加的属性值’)
Vue.set( target, propertyName/index, value )
eg:
Vue.set(vm._data.info,'sex','男')
它必须用于向响应式对象上添加新 property,注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。即target不能是vm,或vm._data
1.5 事件处理
通过v-on指令来传达事件处理函数;
<body>
<div id="demo">
<button v-on:click="sayHello">点我一下</button>
</div>
<script type="text/javascript" src="../vue.js"></script>
<script>
new Vue({
el:'#demo',
data:{
name:'君',
age:21
},
methods:{
sayHello:function(){
alert('你好,我叫'+this.name+',今年我'+this.age);
}
}
})
</script>
</body>
- 使用v-on:xxx来绑定事件,或者简写为@xxx,xxx是事件名;
- 事件的回调定义在vue实例的methods对象中,最终会在vm上(此处不是数据代理)
- methods中配置的函数不要使用箭头函数,否则this指向window不是vm
- methods中配置的函数都是被vue管理的函数。this指向vm或组件实例对象;
- @click=“demo"与@click="demo($event)"效果一致,但后者可以传参;
事件修饰符
- prevent:阻止默认事件;(常用)
- stop:阻止事件冒泡;(常用)应用于子元素
- once:事件只触发一次;(常用)
- capture:使用事件的捕获模式;在事件捕获阶段就开始处理
- self:只有event.target是当前操作的元素才触发事件,使用这个也能有阻止冒泡效果,应用在父元素
- passive:事件的默认行为立即执行。无需等待事件回调执行完毕;正常流程是先执行完回调事件再执行默认事件;
<!--a标签默认点击事件是打开超链接,此处可以阻止默认事件,发送用户定义事件-->
<a herf="http://www.baidu.com" @click.prevent="sayHello">我才不是超链接</a>
事件冒泡 :当一个元素接收到事件的时候 会把他接收到的事件传给自己的父级,一直到window 。(注意这里传递的仅仅是事件 并不传递所绑定的事件函数。所以如果父级没有绑定事件函数,就算传递了事件 也不会有什么表现 但事件确实传递了。)
事件修饰符可以连着写,如click.prevent.stop,先阻止默认行为,在阻止冒泡
键盘事件
keydown,按下按键触发,不需要抬起;
keyup,按下按键,直至抬起才触发;
-
Vue中常用键盘的别名:
- 回车:enter
- 删除:delete
- 退出:esc
- 空格:space
- 换行:tab(特殊,必须配合keydown使用)
- 上、下、左、右:up down left right
-
vue未提供的别名,可以使用原始key值,双子母的key值转为首字小写加短横线连接的形式,如caps-lock
-
系统修饰键(用法特殊):ctrl、alt、shift、win键或meta键
- 配合keyup时,需要按下修饰键的同时,按住其他键,随后释放其他键才能触发
- 配合keydown时,正常触发;
-
可以通过keyCode指定按键,不过此特性将要被废弃,不建议使用
-
Vue.config.keyCodes.自定义键名 = 键码 ;来定制按键别名;
1.6 计算属性
组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。
-
计算属性写在配置对象的computed对象中;
-
通过已有的属性计算得来;
-
底层采用了Object.defineProperty方法提供getter和setter;
-
与methods相比,内部采用了缓存机制(复用),效率更高;
-
计算属性最终会出现在vm上,可直接读取;
-
如果计算属性要被修改,那么必须写set函数,且要修改计算时所依赖的数据
eg:
var vm = new Vue({
el:"#demo",
data:{
firstName:"张",
lastName:"三"
},
computed:{
fullName:{
get(){
return this.firstName + '-' + this.lastName;
}
set(value){
const arr = value.split('-');
this.firstName = arr[0];
this.lastName = arr[1];
}
}
}
})
get在何时被调用?
- 用户初次读取fullName时;
- 所依赖的数据发送改变时(例中为firstName和lastName)
set(value)何时被调用?
- fullname被修改时
计算属性简写
当计算属性只读不改时,可以采用简写形式:
即不需要再写get(),直接将计算属性中的对象写成一个有返回值函数的形式;调用依然通过计算属性名(或叫函数名)调用,不需要加()
eg:
var vm = new Vue({
el:"#demo",
data:{
firstName:"张",
lastName:"三"
},
computed:{
fullName:function(){
return this.firstName + '-' + this.lastName;
}
}
})
1.7 监视属性
顾名思义,这个属性能个监视vue实例的属性值的变化;
格式:
watch:{
需要被监视的属性名:监视配置对象
}
配置对象里的属性有:
- handler(newValue,oldValue){…} 函数,此函数在被监视属性的值发送变化时被调用,newValue和oldValue只是形参,前者是变化后的监视属性值,后者是变化前的值;只写一个形参时指变化后的值
- immediate:true(或者flase),该属性值为true时,初始化时handler就会被调用一次(即不需要等待监视属性的变化)
- deep:true,深度监视,监视多级结构中,某个属性的变化,比如说监视的属性,其属性值是一个对象,正常情况下该对象的某个值发生改变时,handler函数并不会被调用。
也可以通过vue实例来监视:格式如下:
vm.$watch('需要被监视的属性名',监视配置对象)
数据监听原理
监视属性简写
但配置项中只有handler函数时,可以采用简写,格式如下:
watch:{
需要被监视的属性名(newValue,oldValue){...}
}
vm.$watch('需要被监视的属性名',function(newValue,oldValue){...})
computed和lwatch之间的区别:
- computed能完成的功能,watch都可以完成。
- watch能完成的功能,computed不一定能完成,例如: watch可以进行异步操作。
两个重要的小原则:
- 所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm或组件实例对象。
- 所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数、promise回调函数等),最好写成箭头函数,这样this的指向才是vm 或组件实例对象。
1.8 过滤器属性
使用配置对象属性:filter
定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。
语法:
1.注册过滤器:
- 全局的过滤器:Vue.filter(name,callback)
- 局部的过滤器:new Vue(filters:{过滤器名:函数体})
2.使用过滤器:{{ xxx|过滤器名}}或v-bind:属性=“xxx|过滤器名"
备注:
1.过滤器也可以接收额外参数、多个过滤器也可以串联
2.并没有改变原本的数据,是产生新的对应的数据
1.9 class与style绑定
class绑定
通过v-bind,样式名与vue实例中的数据绑定
绑定class样式:
-
字符串写法:适用于样式类名不确定,需要动态指定:
<div class="basic" :class="mood" > </div> ... ... data:{ mood:"style1" }
-
数组写法,适用于要绑定的样式格式不确定、名字也不确定:
<div class="basic" :class="classArr"> </div> //classArr中的样式全部会被使用 ... ... data:{ classArr:['style1','style2','style3'] }
-
对象写法:适用于要绑定的样式个数确定,名字也确定,但要动态决定用不用
<div class="basic" :class="classObj"> </div> ... ... data:{ classObj:{ style1:true, style2:false, style3:true } }
style内联样式绑定
v-bind后面的语句不能使用诸如font-size:40px;此类css语法,因为这不是JS语句,需要写成对象的形式,并且font-size这类属性需要改成fontSize形式;
<div :style="{fontSize: size + 'px'}">
...
</div>
...
...
data:{
...
size:40
}
或者采用对象写法:
<div :style="classObj">
...
</div>
...
...
data:{
classObj{
fontSize:'40px',
color:'red'
}
}
1.10 条件渲染
-
v-show指令,控制标签元素的显示,底层是通过display:none;来实现的;
<p v-show="flase"> //不会显示,但结构还在 hello! </p>
-
v-if指令,v-if=“false”,连结构都不在了
1.11 列表渲染
通过v-for指令来遍历数组或对象,渲染列表
:key建议绑定数据里的唯一标识,对于固定不变的数据,index固然可行,但是对于可能会发生变化的数据尤其是插入、删除数据此类的变化,则应该绑定数据列表中用户定义的数据列,可以理解为数据库表里的主键
数组格式:(遍历各个元素)
<ul>
<!--这里的p和index只是一个形参,index可以看成一个唯一索引,数组index返回0、1、2等值-->
<li v-for="(p,index) in persons" :key="index">
{{index}}-{{p.name}}-{{p.age}}
</li>
</ul>
...
...
data:{
persons[
{name:'张三',age:18},
{name:'李四',age:19},
{name:'王五',age:18}
]
}
对象格式:(遍历各个属性)
<ul>
<!--对象index返回属性名-->
<li v-for="(p,index) in persons" :key="index">
{{index}}-{{p}}
</li>
</ul>
...
...
data:{
persons:{name:'张三',age:18,sex:'男'}
}
也可以用来遍历字符串和指定次数;(用得少))
key的原理
列表过滤
filter函数
1.12 其他指令
其他内置指令
-
v-test
<p>{{name}}</p> <p v-test="name"></p>
-
v-html
与v-test作用相似,但是v-html支持结构式解析,即name可以是带标签的文本,并且标签能够被渲染
-
v-clock
- 特殊属性,Vue实例创建完毕并接管容器后,会删掉v-clock属性
- 使用css的属性选择器,配合v-clock可以解决网速慢时页面展示{{name}}的问题
-
v-once
-
v-once所在节点在初次动态渲染后,就视为静态内容了。
-
以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能。
<p v-once>初始值为:{{n}}</p> <p>当前值为:{{n}}</p> <button @click="n++">点我n+1</button>
-
-
v-pre
- 跳过其所在节点的编译过程。
- 可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译。
自定义指令
使用配置对象属性:directives
1.13 Vue生命周期
二、Vue组件化编程
组件就是一块砖,哪里需要哪里搬
vue组件可分为:
- 非单文件组件:一个文件中含有n个组件
- 单文件组件:一个文件只包含一个组件
2.1 非单文件组件
创建→注册→使用
2.1.1 基本使用
创建
格式:
const 组件名 = Vue.extend(配置对象)
//简写形式
const 组件名 = 配置对象
这个配置对象和vue示例的配置对象大体一致,除了:
- 组件定义时,配置对象里不能写el属性,组件是可复用的,不需要和容器绑定;
- data属性不能写成对象形式,必须写成函数形式(函数返回值为对象);
- 组件的html结构片段,x写在组件配置对象的template属性中,注意,此属性中只能写一个根标签,一般为div;
const stu = Vue.extend({
data(){
return{
stuName:'张三',
age:18
}
},
template:'
<div>
<h2> 学生姓名:{{stuName}}</h2>
<h2> 学生年龄:{{age}}</h2>
</div>
'
})
const sch = Vue.extend({
data(){
return{
schName:'合肥工业大学',
address:'安徽合肥'
}
}
template:'
<div>
<h2> 学校名:{{schName}}</h2>
<h2> 学校地址:{{address}}</h2>
</div>
'
})
注册
在vue实例中注册,需要用到配置对象的components属性;
-
局部注册:注册在vue实例中
格式:
var vm = new Vue({ ..., ..., components:{ 正式组件名:创建阶段的组件名, //两者一致时可以直接写一个 ... } })
eg:
var vm = new Vue({
el:'#demo',
data:{},
components:{
student:stu,
school:sch
}
})
-
全局注册:在实例外通过Vue.component方法注册
格式:
Vue.component('正式组件名',组件对象名或组件配置对象)
使用
直接在vue实例所绑定的容器中调用组件标签即可;
eg:接上例
<div id="demo">
<student></student>
<school></school>
</div>
注意事项
- 关于组件名:
- 一个单词组成:小写,首字大小写都可;
- 多个单词组成:单词小写并且通过-来连接(JS代码下要’'引起来),或者所有单词首字大写(这种只能在脚手架环境下使用)
- 关于组件标签:
- 组件名尽可能回避已有标签名或与其相似的组件名,如P,Div,div,
- 组件标签的名字使用的是注册的名字
- 在脚手架环境下,可以采用自闭合形式的标签(即单标签)
2.1.2 组件的嵌套
先把子组件创建好,再把它注册到父组件的创建配置对象中,属性也是components,子组件的标签也要写在父组件的template属性中,最后再把父组件注册到Vue实例或全局,调用父组件标签即可。
格式:
假设A>B:
<div id='demo'>
<A></A>
<div>
...
const com_b = Vue.extend({...});
const com_a = Vue.extend({
...,
tamplate:'
...
<B></B>
...
',
components:{
B:com_b
}
})
var vm = new Vue({
el:'...',
...,
components:{
A:com_a
}
})
app组件是所有组件的父组件,app又在vue实例容器之下。
2.2 Vue与组件
组件的本质
- 创建的组件本质上并不是普通的对象,而是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的;
- 我们写的组件标签,Vue解析时会帮我们创建他们的实例对象
Vue实例与组件实例
一个重要的内置关系:VueComponent的原型对象的原型对象,指向的是Vue的原型对象,而不是直接指向Object对象
2.3 单文件组件
单文件组件,即.vue文件;
1、创建单文件组件
命名建议采用Java中的类名命名的样式,即单词首字大写
.vue文件的构成:
- template标签:组件的结构
- style标签:组件的样式
- script标签:组件的行为交互
script标签中不可直接按照非单文件组件的样式来创建组件,需要(School.vue):
-
由于外面要导入此组件,所以在定义时需要暴露,使用export关键字,三种形式选择一种即可,一般使用默认暴露
<script> //1、统一暴露 const School = Vue.extend({ ... }) export {School} //2、分别暴露 export const School = Vue.extend({ ... }) //3、默认暴露 export default{ name:'School', ... } </script>
2、创建App.vue
写完单文件组件后,必须要有App.vue文件,该文件的作用是汇总所有组件,
App.vue文件中,格式如下:
<template>
<div class='style1'>
<School></School>
</div>
</template>
<script>
//引入组件
import School from './School.vue'
import ... from '...'
export default{
name:'App',
//注册组件
components:{
School,
...
}
}
</script>
<style>
.style1{
...
}
</style>
3、创建main.js 来创建vm实例
格式如下:
import App from './App.vue'
new Vue({
el:'root',
components:{
App
},
...
})
4、创建index.html容器
容器内需要引入vue.js和mian.js,main.js内的代码就需要用到Vue,所以vue.js需要先引入。考虑到JS阻塞,可以将js从body中引入
<body>
<div id='root'></div>
<script type="text/javascript" src="../js/vue.js"></script>
<script type="text/javascript" src="./main.js"></script>
</body>
三、脚手架 CLI
模板项目的结构:
-
node_modules
-
public
- favicon.ico 页签图标
- index.html 主页面
-
src
- assets 存放静态资源
- logo.png
- component 存放组件
- HelloWorld.vue
- App.vue: 汇总所有组件
- main.js 入口文件
- assets 存放静态资源
-
.gitignore: git 版本管制忽略的配置
-
babel.config.js babel 的配置文件
-
package.json 应用包配置文件
-
README.md 应用描述文件
-
package-lock.json 包版本控制文件
package.json :
- serve:运行
- build:构建(完工后运行)
- lint:语法检查
1、render函数
脚手架中mian.js代码一般为:
import Vue from 'vue';
new Vue({
el: '#app',
render: h => h(App)
});
这个render何方神圣呢?
当使用常规写法时,即el、template、components属性都写上,import Vue from ‘vue’ 其实引入的是残缺版的vue,并不是直接引用vue.js,使用会造成页面加载不出来
2、ref标签属性
可以代替id属性,对于设置了ref属性的标签。vue可以通过this.$ref来获取标签相关的内容:
- 如果是普通标签设置了ref属性,如h1,a,p之类的,那么this.$ref可以获取到这个标签html结构
- 如果是子组件标签。那么this.$ref可以获取到子组件vc对象。进而可以读取到子组件的属性和方法。
3、vue的props属性
props属性可以用来进行父子组件的通信;
在父组件中,对其子组件标签设置标签属性,这些标签属性可以被子组件接收,前提是这些标签属性都是存在于子组件的props属性中。
子组件可以像访问自己data里数据一样读取这些从父组件传过来的值,但是不能直接修改(会警告)。(可以拷贝至data里,再对data进行操作)
eg:
<!--父组件-->
<tamplate>
<MyCom name='zj' age='18'></MyCom>
</tamplate>
<!--子组件-->
new Vue({
...,
...,
props:['name','age']
})
注意:
-
上例中,age=“18” 这个是典型的标签属性的写法,里面的值都当成String处理,就算是这个18必须是number型数据,也不能写成 age = 18 ,那么可以通过v-bind的形式,将“”里的内容当成js代码来处理。即 :age=“18” ,这时候18就算number类型的数据了
-
这里的标签属性的取名不要和特殊的标签属性撞车了,像ref,key之类的都是无效的
-
props属性值也可以写成配置对象的形式
//限制类型 props:{ name:String, age:number } //限制类型、必要性、默认值等等 props:{ name:{ type:String, requierd:true, default:'张三' }, age:{ ... } }
4、mixin混入
不同组件相同的methods方法或者data数据,都可以可以抽离出一个js文件,再分别配置到各个组件的mixins属性里去,
- 功能:可以把多个组件共用的配置提取成一个混入对象;
- 使用:
- 全局:Vue.mixin(xxx)
- 局部:mixins:[‘xxx’]
5、插件
联想一下项目中的,element.js,其实就是一个插件,他能像外挂一下给Vue添加全局的功能或方法,像this. a l e r t 、 t h i s . alert、this. alert、this.message等elementUI里的弹框和消息框。
- 功能:用于增强Vue
- 本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据。
6、组件化编程流程
- 最好按功能点拆分组件
- 先实现静态组件:把壳做出来
- 再实现动态数据
- 数据类型、名称
- 数据保存在哪个组件
- 最后实现交互:绑定事件监听
7、组件的自定义事件
7.1绑定自定义事件
<child @myFun="getS"></child>
....
....
methods:{
getS(value){
... = value
}
}
这里是给child子组件的实例对象自定义了一个myFun事件,并与父组件绑定。当子组件中的myFun事件被触发时,就会触发所绑定的父组件中的getS回调方法,其中value是回调值
在子组件child中,可以通过下面代码来触发写在父组件中的myFun事件
this.$emit('myFun',data)
第一个是事件名,第二个是要传递的值
父组件中也可以这样写来实现事件绑定:
<child ref="ch"></child>
...
...
this.$refs.ch.$on("myFun",this.getS)
7.2解绑自定义事件
解绑函数:找到绑定的子组件实例对象,子组件直接掉this,父组件就通过ref来调用this. r e f s . c h . refs.ch. refs.ch.off
//解绑一个
this.$off("event")
//解绑多个要写成数组形式
this.$off(["event1","event2","event3",...])
//不写形参,全部解绑
this.$off()
当组件被销毁时,(this.$destory),所有的自定义事件都不奏效
8、全局事件总线
上面的例子中,父组件绑定了一个自定义事件a(附加回调函数f),这个自定义事件是在子组件的vue实例中的,当子组件触发这个事件a时(携带数据d),可以唤起父组件中的回调函数f,并将携带的数据d作为回调函数f的形参; e m i t 可以理解为触发, emit可以理解为触发, emit可以理解为触发,on可以理解为绑定事件,执行回调,一个自定义事件只能写在一个vue实例中,就是说 e m i t 和 emit和 emit和on由同一个vue实例来执行
全局事件总线可以实现任意组件之间的通信;其原理是:
- 假设存在组件A和B,A需要向B发送数据d,现引入一个第三方vue实例对象eventBus;
- 在B组件中,调用EventBus对象,绑定一个自定义事件sentAB,同时设置一个回调函数,自定义事件被触发时执行回调,即event.$on(“sentAB”,this.setData)
- A组件同样调用EventBus对象,触发自定义事件sentAB,同时携带A组件的数据d,即event.$emit(“sentAB”,this.d)
- 这时B组件的回调函数就会收到数据d作为形参,执行回调函数,将A的数据d存到了B中,实现了组件A和B的通信
事件总线的必须满足:
- 所有需要通信的组件必须都能调用得到;
- 能够调用 o n 、 on、 on、emit、$off
一个重要的内置关系:VueComponent的原型对象的原型对象,指向的是Vue的原型对象,而不是直接指向Object对象
也就是说,所有vc对象都可以访问的地方,就是Vue的原型对象;
另外,有趣的是,vc对象和其原型对象(vc的原型对象的类型是Vue)上都不存在 e m i t 和 emit和 emit和on方法,这些方法其实是在Vue的原型对象中
因此可以得出:
-
事件总线对象适合放在Vue的原型对象当中,这样的话所有组件都能访问到这个对象;(在main.js里设置全局对象也行)
-
由于 e m i t 和 emit和 emit和on等方法也存在于Vue的原型对象里,能调用Vue原型对象的就是vm实例和vc实例了;因此事件总线必须是vc实例或者vm实例
在vue脚手架里,可以通过以下方案来安装全局事件总线
8.1 事件总线安装方案一
可以在main.js中新建一个全局的vm实例对象,命名为eventBus:
const eventBus = new Vue();
在需要通信的组件里导入eventBus对象:
import {eventBus} from "@/main";
调用:
eventBus.$on("xxx",xxx)
eventBus.$emit("xxx",xxx)
8.2 事件总线安装方案二
也可以在Vue原型对象里添加vm实例本身来作为事件总线(习惯命名为$bus),但是必须要在vm生成前添加:main.js代码如下:
new Vue({
el: '#app',
render: h => h(App),
beforeCreate() {
Vue.protoType.$bus = this;
}
});
现在 b u s 就是一个唯一的对象了,同时它又是 v m 对象,可以调用 bus就是一个唯一的对象了,同时它又是vm对象,可以调用 bus就是一个唯一的对象了,同时它又是vm对象,可以调用emit等方法,所以为什么不直接将vm作为事件总线呢,const bus = vm;,这是因为要将vm本身赋值给事件总线,那么要等vm构建完成才能执行这段代码,但是vm构建过程中都已经要用到事件总线了,自相矛盾了属于是。
调用
this.$bus.$emit("xxx",xxx)
this.$bus.$on("xxx",xxx)
这种方案不需要导入对象;$bus是Vue原型对象,所有vc实例都能顺藤摸瓜找到他(即data没有就找原型对象,原型对象没有就找原型对象的原型对象)
最好在beforeDestory钩子中,解绑当前组件所使用的事件;
9、消息订阅与发布
任意组件直接的通信,这是导入第三方库来实现;可以安装pubsub-js
安装:
npm i pubsub-js
在需要通信的组件里导入:
import pubsub from 'pubsub-js'
然后就可以直接通过pubsub对象名来调用方法了
使用方式:
订阅消息(对比$on):
import pubsub from 'pubsub-js'
...
setData(msgName,data){ //注意,与事件总线不同的是,这个回调函数会收到两个参数,第一个是方法名,第二个才是数据
this.xxx = data;
}
...
this.pubId = pubsub.subcribe("msgName",setData) //事件名,回调函数
发布消息(对比$emit)
import pub from 'pubsub-js'
...
pubsub.publish("msgName",data)
关闭订阅:
//与定时器类似,不能直接通过消息名来关闭,而是通过将订阅存入变量,通过关这个变量来关闭
pubsub.unsubcribe(this.pubId)
10、过渡与动画
10.1 显示/隐藏 按钮的实现
<button @click="isShow = !isShow" >显示/隐藏</button>
<Component v-show="isShow"></Component>
...
...
data(){
return{
isShow:ture
}
}
10.2 简单动画效果
动画关键帧:@keyfarmes 这个要回头看看css或者js
@keyfarmes myD{
from{
transform:translateX(-100%); //整个元素从左侧处出来
}
to{
transform:translateX(0px);
}
}
如何使用呢?
定义两个动画的类名,显示的动画可以叫come ,隐藏的动画可以叫go
.come{
animation:myD,1s,linear; //动画名,时间,匀速
}
.go{
animation:myD,1s,reverse; //加上reverse表示反转
}
将.come或.go应用于Component,但是这样只能生效一个动画,这时我们可以将需要动画展示的标签用transition标签(过渡)包裹起来,
另外不能在简单的命名为.come或.go,在vue里,默认命名为.v-enter-active和.v-leave-active,如果transition有name属性值,假设为hello,那就要写成hello-enter-active
.v-enter-active{
animation:myD 1s linear; //动画名,时间,匀速
}
.v-leave-active{
animation:myD 1s reverse; //加上reverse表示反转
}
<transition>
<Component v-show="isShow"></Component>
</transition>
10.3 过渡效果
动画也可能用Vue的过渡来写:(假设过渡名为hello)
/*进入的起点*/
.hello-enter{
transform: translateX(-1800px);
}
/*进入的终点*/
.hello-enter-to{
transform: translateX(0);
}
/*离开的起点*/
.hello-leave{
transform: translateX(0);
}
/*离开的终点*/
.hello-leave-to{
transform: translateX(-1800px);
}
/*上面的代码明显0可以这样优化*/
/*进入的起点,离开的终点*/
.hello-enter,.hello-leave-to{
transform: translateX(-1800px);
}
/*进入的终点,离开的起点*/
.hello-enter-to,.hello-leave{
transform: translateX(0);
}
另外在需要应用动画的标签上,加上transition属性来调整动画;
<transition name="hello" appear>
<Child1 v-show="isShow" class="my-animation"></Child1>
</transition>
...
.my-animation{
transition: 2s linear;
}
也可以写在active里:
/*进入和离开过程中*/
.hello-enter-active,.hello-leave-active{
transition:2s linear;
}
10.4 多个元素的过渡
transition过渡标签只能包裹一个标签元素,要想应用多个过渡,那应该使用transition-group标签,而且每个子标签需要添加key属性;
<transition-group name="hello" appear>
<Child1 v-show="isShow" key="1"></Child1>
<Child1 v-show="isShow" key="2"></Child1>
</transition-group>
10.5 集成动画库
老师推荐:npm上的animate.css
网站主页:Animate.css | A cross-browser library of CSS animations.
使用方式:主页上有介绍;
11 配置代理
97-97
四、slot插槽、vuex
4.1 slot插槽
在模板中挖个坑,等着组件的使用者去填
默认插槽
假设有父组件parent.vue和子组件chird.vue
<!--子组件-->
<template>
<div>
<h1>hello</h1>
<slot>甚至插槽标签的内容体也可以写东西,这些是默认值,当父组件没有往插槽里写内容时呈现</slot>
</div>
</template>
<!--父组件-->
<template>
<child>
<h3>world</h3>
</child>
</template>
就是说,组件的使用者可以在自定义组件标签的内容体里添加内容,前提是该子组件在其模板内留有插槽
注意:
- child标签内容里的东西是解析完成后再传到slot里的,也就是说可以在父组件里为这些内容添加样式
具名插槽
子组件(child)插槽格式:
<slot name="center"></slot>
<slot name="footer"></slot>
父组件插入格式:
<template>
<child>
<h1 slot="center">hello</h1>
<span slot="footer">world</span>
</child>
</template>
注意:
-
可以通过div将需要使用同一个插槽的内容抱起来,但这样最终会生成一个div元素,这时候可以使用template标签将它们包起来,不会生成结构元素,这时候可以使用slot=“xxx”的写法。也可以使用v-slot:xxx写法(仅限template标签)
<template> <child> <h1 slot="center">hello</h1> <template v-slot:footer> ... </template> </child> </template>
作用域插槽
需求:假设子组件有一个数组,父组件需要分三种方式来展示这个数组,比如ul、ol、或者p、h1等等
即:数据在子组件中,但这些使用这些数据的结构需要由父组件(子组件的使用者)来决定
可以用作用域插槽来解决这类需求,子组件中写好数据和slot插槽,父组件(使用者)在子组件标签内写好结构和要使用的数据;至于父组件如何获取这些数据,可以在子组件插槽中这样写:
<slot :自定义数据名称A="子组件中的数据名a"></slot>
相当于子组件将数据a传给了slot插槽,一旦子组件的使用者(父组件)给插槽赋予了结构,那么父组件就能通过A收到子组件的数据a
另外父组件要想获取这个数据,必须需要用template标签包裹起来,标签内还要写上scope属性,属性值是用户自定义的一个对象名X,因为数据是会以对象的形式传过来(意味着可以传多个数据),结构内就可以通过X.A的形式调用a数据了
<template>
<child>
<template scope="X">
<ul>
<li v-for="item in X.A">
{{item}}
</li>
</ul>
</template>
</child>
</template>
注:新的API将scope属性改成了slot-scope
4.2 vuex
1、概述
专门在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。
官网:https://github.com/vuejs/vuex
使用场景:
- 多个组件依赖于同一状态
- 来自不同组件的行为需要变更同一状态
2、Vuex工作原理图
- dispatch(派遣)和commit(提交)是人为操作的,而mutate(转换)和render(渲染)是自动执行的
- Bcakend API :后端接口 ,可以将业务逻辑写在actions对象中
搭建Vuex环境
安装
npm i vuex
使用
import Vuex from 'vuex'
Vue.use(Vuex)
原理图中的三者必须封装在一个store对象里,而且这个store对象,所以的vc组件都能访问
搭建步骤:
-
在src文件夹里,创建store文件夹,并在store文件夹里创建index.js文件
-
index.js文件内容如下:
//引入Vuex import Vuex from 'vuex' //应用Vuex插件 import Vue from 'vue' Vue.use(Vuex) //准备actions,用于响应组件中的动作 const actions = {} //准备mutations,用于操作数据 const mutations = {} //准备state,用于存储数据 const state = {} //创建并暴露store对象 (配置对象里,key和值重名了,可以采用简写) export default new Vuex.Store({ actions, mutations, state })
-
然后在main.js中,导入store
import store from './store' ... new Vue({ ... render:h=>h(App), store, //对象简写形式 ... })
3、Vuex的使用
- 在store文件夹里的index.js里,将需要共享的数据写到state对象里
const state = {
data1:{
......
},
data2:{
......
}
}
- 在store文件夹里的index.js里,将需要调用的方法(业务逻辑)写到actions对象里
- context对象是一个mini版的store对象,可以获取state数据和调用commit,甚至可以再次调用dispatch
- 调用dispatch意味着,actions的方法可以链接起来,方法1走完了dispatch方法2,再dispatch方法3…最终commit到mutations中去操作数据
- actions如果没有业务逻辑操作,那么就只起到一个转发的作用,那么这部分就可以不写,直接在组件里调用mutation方法
const actions = {
方法名fun1:function(context,value){
//可以对数据进行一番操作,再将值传给mutations,这里没有处理,直接传value了
....
context.commit('FUN1',value);
},
方法名fun2:function(){
....
},
}
- 在store文件夹里的index.js里,将操作数据的方法写到mutations对象里
- 一般将actions里对应的方法,在mutations转为大写
const mutations = {
方法名FUN1:function(state,value){
//这时可以利用传过来的value,对stare中的数据进行修改
....
},
}
组件使用state中数据
<p>
{{$store.state.data1.xxx}}
</p>
组件中通过dispatch传递一个方法和数据到actions
this.$store.dispatch('方法名fun1',数据) //数据将传到actions方法的value形参中
组件中通过commit跳过actions直接和mutations交互
this.$store.commit("方法名FUN1",数据)
4、Vuex开发者工具的使用
Vuex开发者工具就在vue开发者工具当中;菜单栏的第2项!
通过原理图可以发现,Devtools是检测mutation的
-
Base State:基础状态,
-
下载图标:将当前选中的状态转为基础状态,意味着之前的状态将清除
-
禁止图标:将当前选中的状态操作取消,这样意味着它之后的操作也会取消,因为这些操作都是链接式的跟在它后面,Dom界面会回退到它的上一个状态
-
时间图标:可以将Dom回退到当前选中状态
-
右上角的下载图标:将最终状态作为基础状态(Dom也处于该状态),清空所有操作
-
右上角的禁止图标:保留基础状态,清除全部操作
-
右上角红点:Vuex开发者工作的开启和关闭
时间图标:
下载图标:
禁止图标:
下半部分的右上角还有导入导出功能,可以导出当前选中状态的state数据到剪贴板,导入可以将剪贴板内容导入
5、其他配置
getters配置项
用于将state中的数据进行加工;在store,index.js中,可以加入getters配置项,类似于计算属性
const getters = {
fun1(state){
return state.xxx......
}
}
export default new Vuex.Store({
...
...
...,
getters
})
mapState和mapGetters
每次调用vuex的数据的时候,都要写this. s t o r e . s t a t e 和 t h i s . store.state和this. store.state和this.store.getters,相对比较麻烦,vuex提供mapState和mapGetters,简化上面那种写法,直接当成计算属性来写,(本质上就是映射)
假设state数据如下:
const state = {
school:'xxx',,
student:'xxx',
age:'xxx'
}
以mapState为例:
首先需要导入mapState
import {mapState} from 'vuex'
对象写法:
//不能简写
computed:{
...mapState({
school:'school',
student:'student',
age:'age'
})
}
数组写法:
computed:{
...mapState(['school','student','age'])
}
mapActions与mapMutations
原理和上面类似,不过这俩需要写到methods里。而且需要注意的是,仅仅绑定方法名还不行,还要留意形参
例如下面这种:
methods:{
subtract(){
this.$store.dispatch('subtract',this.n)
}
}
可以写成:
methods:{
...mapActions(['subtract'])
}
但是调用的时候,必须写成
subtract(this.n);
6、vuex模块化
虽说vuex可以实现数据共享,但是只要共享的数据比较多,比较杂乱,各自操作数据的方法交织在一起,就会显得很乱,没有条理,开发效率降低;
这时就可以将vuex进行模块化的管理,将相关的数据和操作(actions、mutations、state、getters)放在一起组成一个模块,
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
import Vue from 'vue'
Vue.use(Vuex)
//和xx有关的配置
const xxOptions = {
namespaced:true, //开启命名空间,mapState就可以通过对象名来绑定里面的数据
actions:{},
mutations:{},
state:{},
getters:{}
}
//和xxx有关的配置
const xxxOptions = {
namespaced:true,
actions:{},
mutations:{},
state:{},
getters:{}
}
//创建并暴露store对象 (配置对象里,key和值重名了,可以采用简写)
export default new Vuex.Store({
modules:{
a:xxOptions,
b:xxxOptions
}
})
通过上面的操作,a和b都会存放到$store的state里
假设以下数据存放在xxOptions中,即a的state中
state:{
school:'xxx',,
student:'xxx',
age:'xxx'
}
那么mapState就需要通过以下方式来绑定:
computed:{
...mapState('a',['school','student','age'])
...mapState('b',['xxx','xxx','xxx'....])
}
同理:mapActions也需要指明模块:
methods:{
...mapActions('a',{fun1:'xxx1',...}) //对象写法
...mapActions('b',['xxx1','xxx2'...]) //数组写法
}
注意:
- 如果不使用mapActions和mapMutations,使用原始的方法,方法名前要加模块对象名和/
this.$store.dispatch('a/fun1',xxx)
- state的数据是按a,b两个模块划分的,但是getters中的数据是按a/xxx 、 b/xxx的形式划分的,所以不能通过this. s t o r e . g e t t e r . a . x x x 来获取,而是需要通过 t h i s . store.getter.a.xxx来获取,而是需要通过this. store.getter.a.xxx来获取,而是需要通过this.store.getter[‘a/xxx’]来获取
五、路由
5.1 概述
路由:
- 一个路由就是一组映射关系(key-value)
- key为路径,value可能是function或component
前端路由:
- value是component,用于展示页面内容;
- 工作过程:当浏览器的路径改变时,对应的组件就会显示;
后端路由:
- value是function,用于处理客户端提交的请求
- 工作过程:服务器收到一个请求时,根据请求路径找到匹配的函数来处理请求,返回响应数据
5.2 基本使用
- 安装
npm i vue-router
- 在src文件中创建router文件夹,再到router文件夹里创建index.js文件,index.js文件代码如下:
//引入路由
import VueRouter from 'vue-router'
//引入组件
import xxx from 'xxx'
...
//创建并暴露一个路由器
export default new VueRouter({
routers:[
{path:'/xxx',component:xxx},
{path:'/xxx',component:xxx},
...
]
})
- 在main.js中引入并使用:
import Vue from 'vue'
//引入VueRouter
import VueRouter from 'vue-router'
//应用插件
Vue.use(VueRouter)
//引入路由器
import router from './router'
new Vue({
...
...
router:router,
...
})
- 使用router-link标签,点击它来跳转路由(本质上是a标签)
- 特有属性to,属性值为跳转路由的路径
<router-link to="/xxx">点我跳转xxx</router-link>
- 使用router-view标签来承接路由组件的内容
<router-view></router-view>
5.3 路由使用注意点
- 注册路由的组件可以称为路由组件,放在page文件夹里,其他组件称为一般组件,放在component文件夹里
- 当路由组件被切换走了,这个组件就会被销毁(这里要注意,路由组件之间的通信用全局事件总线交互的时候很可能会失败)
-
r
o
u
t
e
属性和
route属性和
route属性和router属性
- 每个组件都有自己的$route属性,存储着自己的路由信息
- 整个应用只有一个$router属性
5.4 嵌套(多级)路由
在一级路由中使用children属性项。
子路由配置如下:(router/index.js)
- 和router配置项一样,children配置项用数组包裹,里面是子路由对象
- children路由中,路径不加/
//引入路由
import VueRouter from 'vue-router'
//引入组件
import Home from './page'
import xxx from 'xxx'
...
//创建并暴露一个路由器
export default new VueRouter({
routers:[
{
path:'/home',
component:Home,
children:[
{
path:'message',
component:Massage
},
{
path:'news',
component:News
}
]
},
{
path:'/about',
component:xxx
},
...
]
})
在网页中,home组件和about的父组件正常写路由,而message和news需要写在home组件当中,写法大体一致,不过需要注意以下几点
- 跳转路径要加上父组件路径,(后期有方法省略)
<router-link to="/home/message">xxx</router-link>
5.5 路由传参
将router-link的数据或者说父级组件的数据传到router-view中;
1、采用跳转路由携带query参数的方法
-
跳转路由携带query参数,to的字符串写法
-
发送数据(仅仅是字符串)
<router-link to="/home/message/detail?id=001&title=hello"></router-link>
-
发送数据(变量)模板写法
x <router-link :to="`/home/message/detail?id=${this.xxx}&title=${this.xxx}`"></router-link>
-
-
跳转路由携带query参数,to的对象写法
<router-link :to="{ path:'/home/message/detail', query:{ id:this.xxx, title:this.xxx } }"> xxxx </router-link>
-
接收数据,在router-view对应的组件里,调用this.$route.query
2、采用params参数的方法
定义路由的时候,在路径后面补上:声明接收参数
path:'/xxx/:id/:title'
这样就表示允许在路由跳转的传入两个参数:
<!--字符串写法-->
<router-link to="/xxx/0001/你好">xx</router-link>
<!--模板写法-->
<router-link :to="`/xxx/${this.xx}/${this.xx}">xx</router-link>
<!--对象写法-->
<router-link :to="{
//path:'/home/message/detail',
name:'xxx', //不能写path。必须写name
params:{
id:this.xxx,
title:this.xxx
}
}">
然后通过this.$route.params调用
3、props配置项
路由配置中的props属性,
//props的第一种写法,值为对象,对象中的所有键:值会以props的形式传递给component里注册的组件,这种写法下,每次调用的都是固定的数据
...
component:xxx,
props:{a:1, b:2}
//props的第二种写法,值为布尔值,布尔值为true,则会把该路由组件收到的所有params参数,以props的形式传递给component里注册的组件,也就是说可以直接当成外来参数的形式调用,而不用再写this.$route.params.xxx和计算属性了
...
component:xxx,
props:true
//props的第三种写法,值为函数,上面写法只能接收params参数,而不能收query的参数,这时候可以考虑使用函数写法,将query作为形参回调,调用query.xxx,赋予一个属性a,再到对应的组件的设置props:[a]
...
component:Message,
props(query){
return {id:query.id,title:query.title}
}
//第三种写法。然后Message组件里,写上props属性
...
data...
props:['id','title']
5.6命名路由
vue可以在路由器配置里给路由加上name的属性,一般是语义化的名字;
...
router:[
{
name:'xxxx',
path:'/xxx',
component:xxx
}
]
具名的好处之一是,在to的对象化写法中,可以直接写name属性来跳转,而不用写一大堆的xxx/xxxx/xx/x/xx/xx
5.7 router-link 的replace属性
router-link默认是一种push模式
在router-link标签里加:replace:ture(简写直接写replace也可)可以切换为replace模式,replace模式每次进入新路由,都会替代掉上一次的路由地址,而不是压栈