文章目录
- 视频讲解地址:[点击打开视频讲解地址](https://www.bilibili.com/video/BV17d4y1S7Cj/?vd_source=66e2692cc471862d6c3f85dc4b9ea5dd)
- 前言
- 一、介绍
- 二、Vue实例
- 三、模板语法
- 四、计算属性和侦听器
- 五、Class 与 Style 绑定
- 六、条件渲染
- 七、列表渲染
- 八、事件处理
- 九、表单输入绑定
- 十、组件基础
- 十一、组件注册
- 十二、Prop
- 十三、自定义事件
- 十四、插槽
- 十五、动态组件 & 异步组件
- 十六、混入
- 十七、自定义指令
- 十八、渲染函数 & JSX
- 十九、插件
- 二十、过滤器
- 二十一、Vuex
- 二十二、Vue Router路由管理器
视频讲解地址:点击打开视频讲解地址
前言
Vue2入门到精通
文章目录与[Vue官网](https://cn.vuejs.org/)目录对应
一、介绍
1. Vue是一款前端渐进式框架,可以提高前端开发效率。
2. Vue是一套构建用户界面的框架,只关注视图层,它不仅易于上手,还便于与第三方库或既有项目整合。(Vue有配套的第三方类库,可以整合起来做大型项目的开发)
3. 作者:尤雨溪
特点
遵循MVVM模式。Vue通过MVVM模式,能够实现视图与模型的双向绑定。
MVVM是前端视图层的概念,主要关注于视图层分离。
MVVM把前端的视图层分为了三部分:Model,View,VM ViewModel。
简单来说,就是数据变化的时候, 页面会自动刷新, 页面变化的时候,数据也会自动变化
编码简洁,体积小,运行效率高,适合移动/PC端开发
它本身只关注UI,可以引入其它第三方库开发项目
Vue周边库
vue-cli:vue脚手架
vue-resource 是一个非常轻量的用于处理HTTP请求的插件,Vue2.0之后,就不再对vue-resource
更新,而是推荐使用axios。
axios 处理HTTP请求的插件
vue-router:路由
vuex:状态管理
element-ui:基于vue的UI组件库(PC端)
Vue三种安装方式
CDN
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<!-- 本地导入 -->
<script src="../node_modules/vue/dist/vue.js"></script>
NPM
npm install vue
命令行工具 (CLI)脚手架
命令行创建
npm install -g @vue/cli @vue/cli-service-global
创建项目
vue create vue-demo
cd vue-demo
npm run serve
二、Vue实例
Vue生命周期(钩子函数)
概述:其实就是在特定的时期执行对应事件
生命周期图示
什么是Vue生命周期?
Vue 实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始 化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。
Vue生命周期的作用是什么?
Vue生命周期中有多个事件钩子,让我们在控制整个Vue实例过程时更容易形成好的逻辑。
Vue生命周期总共有几个阶段?
总共分为8个阶段:创建前/后, 载入前/后,更新前/后,销毁前/后。
创建阶段:
beforecreate:实例已经初始化,但不能获取DOM节点。(没有data,没有el)
created:实例已经创建,仍然不能获取DOM节点。(有data,没有el)
载入阶段:
beforemount:模板编译完成,但还没挂载到界面上。(有data,有el)
mounted:编译好的模板已挂载到页面中(数据和DOM都已经渲染出来)。
更新阶段:
beforeupdate:数据发生变化立即调用,此时data中数据是最新的,但页面上数据仍然是旧的(检测到数据更新时,但DOM更新前执行)。
updated:更新结束后执行,此时data中的值和页面上的值都是最新的。
销毁阶段:
beforedestroy:当要销毁vue实例时,在销毁之前执行。
destroy:在销毁vue实例时执行
第一次页面加载会触发哪几个钩子?
第一次页面加载时会触发 beforeCreate, created, beforeMount, mounted 这几个钩子
DOM 渲染在 哪个周期中就已经完成?
DOM 渲染在 mounted 中就已经完成了。
简单描述每个周期具体适合哪些场景?
beforecreate : 可以在此阶段加loading事件,在加载实例时触发;
created : 初始化完成时的事件写在这里,如在这结束loading事件,异步请求也适宜在这里调用;
mounted : 挂载元素,获取到DOM节点;
updated : 如果对数据统一处理,在这里写上相应函数;
beforeDestroy : 可以做一个确认停止事件的确认框;
nextTick : 更新数据后立即操作dom;
Vue基本代码结构
const vm = new Vue({
el:'#app',//所有的挂载元素会被 Vue 生成的 DOM 替换
data:{ // this->window },
methods:{ // this->vm},
//注意,不应该使用箭头函数来定义 method 函数 ,this将不再指向vm实例
props:{} ,// 可以是数组或对象类型,用于接收来自父组件的数据
//对象允许配置高级选项,如类型检测、自定义验证和设置默认值
watch:{ // this->vm},
computed:{},
render(){},
// 声明周期钩子函数
})
el与data的两种写法
<!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>el与data的两种写法</title>
<script src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<h1>Hello,{{name}}!</h1>
</div>
<script>
Vue.config.productionTip = false
//el的两种写法:
// const vm = new Vue({
// // el:'#root', //第一种写法
// data:{
// name:'JOJO'
// }
// })
// vm.$mount('#root')//第二种写法
//data的两种写法:
new Vue({
el:'#root',
//data的第一种写法:对象式
// data:{
// name:'JOJO'
// }
//data的第二种写法:函数式
data(){
return{
name:'JOJO'
}
}
})
</script>
</body>
</html>
总结:
el有2种写法:
创建Vue实例对象的时候配置el属性
先创建Vue实例,随后再通过vm.$mount('#root')指定el的值
data有2种写法:
对象式
函数式
如何选择:目前哪种写法都可以,以后学到组件时,data必须使用函数,否则会报错
概述
当一个Vue实例被创建时,它将data对象中的所有的property加入到Vue的响应式系统中。
当这些property的值发生改变时,视图将会产生 响应,即匹配更新为新的值。
例外
Vue实例外部新增的属性改变时不会更新视图。
Object.freeze(),会阻止修改现有的property,响应系统无法追踪其变化。
实例属性和方法
访问el属性:vm.$el
访问data属性:vm.$data
访问data中定义的变量:vm.a,vm.$data.a
访问methods中的方法:vm.方法名()
访问watch方法:vm.$watch()
注意:
以_或$开头的property不会被Vue实例代理,因为它们可能和Vue内置的property、API方法冲突。你可以使用例如vm.$data._property的方式访问这些property。
不要在选项property或回调上使用箭头函数,this将不会指向Vue实例 比如created: () => console.log(this.a)或vm.$watch('a', newValue => this.myMethod())。
因为箭头函数并没有this,this会作为变量一直向上级词法作用域查找,直至找到为止,经常导致Uncaught TypeError: Cannot read property of undefined或Uncaught TypeError: this.myMethod is not a function之类的错误。
vue项目中各文件说明
node_modules文件夹:项目依赖文件
public文件夹:放置静态资源(图片),webpack在打包时会原封不动的打包到dist文件夹中。
src文件夹(程序员源代码文件夹):
assets文件夹:放置静态资源(多个组件共用的静态资源),在webpack打包时会把静态资源当成
模块打包到JS文件中
components:放置非路由组件(全局组件)
APP.vue:唯一的根组件
main.js:程序入口文件,整个程序中最先执行的文件
api设计请求相关的
babel.config.js:配置文件(与babel相关),一般不去修改
package.json:相当于项目的身份证,配置信息,记录项目叫做什么、项目中有哪些
依赖、项目怎么运行,
package-lock.json:可以删除,是一个缓存文件
README.md:说明性文件
vue.config.js:项目的配置文件
三、模板语法
插值
语法:{{ 变量名/对象.属性名 }}
功能:用于解析标签体内容,且可以直接读取到data中的所有区域,插值表达式允许用户输入"JS代码片段"
注意:VSCode编辑中,使用.HTML文件,在浏览器打开,需要在插件商店安装 “open in browser”
文本插值
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ls-demo</title>
<!-- 引入本地vue依赖 -->
<script src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<h1>Message: {{ msg }}</h1>
</div>
<script type="text/javascript">
//创建vue对象
var app = new Vue({
//让vue接管div标签
el:"#app",
//定义数据,里边包含一个属性
data:{
msg:"我是文本插值吖!!!"
}
});
</script>
</body>
</html>
原始 HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ls-demo</title>
<!-- 引入本地vue依赖 -->
<script src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<h1>原始 HTML: {{ rawHtml }}</h1>
<h1>v-html 指令<span v-html="rawHtml"></span></h1>
</div>
<script type="text/javascript">
//创建vue对象
var app = new Vue({
//让vue接管div标签
el:"#app",
//定义数据,里边包含一个属性
data:{
rawHtml :'Using mustaches: <span style="color: red">This should be red.</span>'
}
});
</script>
</body>
</html>
原始 HTML效果图
原始 HTML小结
原始HTML(插值):不解析html代码,直接显示;
v-html指令:可解析HTML代码后显示;
Attribute => v-bind
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ls-demo</title>
<!-- 引入本地vue依赖 -->
<script src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div v-bind:id="dynamicId">我是作用在HTML attribute id</div>
<button v-bind:disabled="isButtonDisabled">Button</button>
</div>
<script type="text/javascript">
//创建vue对象
var app = new Vue({
//让vue接管div标签
el:"#app",
//定义数据,里边包含一个属性
data:{
dynamicId :'id',
isButtonDisabled:true,
}
});
</script>
</body>
</html>
Attribute效果图
Attribute小结
使用了v-bind后指定某个属性名,那么在这个属性的属性值当中我们就可以使用data数据进行绑定了
v-bind绑定的是一个变量
v-bind的语法糖(简写格式)为 " : "
<button :disabled="isButtonDisabled">Button</button>
使用 JavaScript 表达式
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ls-demo</title>
<!-- 引入本地vue依赖 -->
<script src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 数字相加 -->
<div>{{ number + 1 }}</div>
<!-- 使用三元表达式判断 -->
<div>{{ ok ? 'YES' : 'NO' }}</div>
<!-- 这是语句,不是表达式 -->
<!-- {{ var a = 1 }} -->
<!-- 流控制也不会生效,请使用三元表达式 -->
<!-- {{ if (ok) { return message } }} -->
<div>{{ message.split('').reverse().join('') }}</div>
<!-- message.split('') => ['m', 'o', 'c', 'h', 'e', 'n', 'g', 'x', 'i', 'y', 'a', '!', '!', '!'] -->
<!-- message.split('').reverse() => ['!', '!', '!', 'a', 'y', 'i', 'x', 'g', 'n', 'e', 'h', 'c', 'o', 'm'] -->
<!-- message.split('').reverse().join('') => '!!!ayixgnehcom' -->
<div v-bind:id="'list-' + id">list-{{id}}</div>
<!-- id为变量 => data中 bindId -->
<!-- 结果: id="list-bindId" -->
</div>
<script type="text/javascript">
//创建vue对象
var app = new Vue({
//让vue接管div标签
el:"#app",
//定义数据,里边包含一个属性
data:{
number:1,
ok:true,
message:'mochengxiya!!!',
id:'bindId'
}
});
</script>
</body>
</html>
JavaScript 表达式效果图
指令
指令 (Directives) 是带有 v- 前缀的特殊 attribute。指令 attribute 的值预期是单个 JavaScript 表达式 (v-for 是例外情况,稍后我们再讨论)。
指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
参数
一些指令能够接收一个“参数”,在指令名称之后以冒号表示。例如,v-bind 指令可以用于响应式地更新 HTML attribute。
<a v-bind:href="url">...</a>
在这里 href 是参数,告知 v-bind 指令将该元素的 href attribute 与表达式 url 的值绑定。
另一个例子是 v-on 指令,它用于监听 DOM 事件
<a v-on:click="doSomething">...</a>
动态参数
语法:[ 动态参数 ]
<template>
<div>
<a v-bind:[attributeName]="url"> 动态参数attributeName </a>
<!-- attributeName 会被作为一个 JavaScript 表达式进行动态求值,求得的值将会作为最终的参数来使用。
这个绑定将等价于 v-bind:href。 -->
<br>
<a v-on:[eventName]="doSomething"> 动态的事件名 </a>
<!-- 同样地,你可以使用动态参数为一个动态的事件名绑定处理函数 -->
<!-- 当 eventName 的值为 "click" 时,v-on:[eventName] 将等价于 v-on:click -->
</div>
</template>
<script>
export default {
data(){
return {
attributeName:'href',
url:'url',
eventName:'click'
}
},
methods:{
doSomething(){
//动态的事件
}
}
}
</script>
<style>
</style>
动态参数小结
1.对动态参数的值的约束:动态参数预期会求出一个字符串,异常情况下值为 null。
这个特殊的 null 值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。
2.对动态参数表达式的约束:某些字符,如空格和引号,放在 HTML attribute 名里是无效的。
变通的办法是使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式。
修饰符
语法:半角句号 . 指明的特殊后缀
例:.prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault()
<form v-on:submit.prevent="onSubmit">...</form>
其他修饰符总结:
.stop - 调用 event.stopPropagation()。
.prevent - 调用 event.preventDefault()。
.capture - 添加事件侦听器时使用 capture 模式。
.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
.{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
.native - 监听组件根元素的原生事件。
.once - 只触发一次回调。
.left - (2.2.0) 只当点击鼠标左键时触发。
.right - (2.2.0) 只当点击鼠标右键时触发。
.middle - (2.2.0) 只当点击鼠标中键时触发。
.passive - (2.3.0) 以 { passive: true } 模式添加侦听器
缩写
v-bind 缩写
<!-- 完整语法 -->
<a v-bind:href="url">...</a>
<!-- 缩写 -->
<a :href="url">...</a>
<!-- 动态参数的缩写 (2.6.0+) -->
<a :[key]="url"> ... </a>
v-on 缩写
<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>
<!-- 缩写 -->
<a @click="doSomething">...</a>
<!-- 动态参数的缩写 (2.6.0+) -->
<a @[event]="doSomething"> ... </a>
四、计算属性和侦听器
计算属性
概述:计算属性就是一个提前定义好的方法, 该方法可以看作是一个特殊的值, 可以在插值表达式中使用。
计算属性必须放在Vue的computed中。
基础例子
<template>
<div>
<!-- 模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护 -->
<!-- 所以,对于任何复杂逻辑,你都应当使用计算属性。 -->
<div>{{ message.split('').reverse().join('') }}</div>
<p>计算属性 (Computed) 的 getter : "{{ reversedMessage }}"</p>
姓:<input type="text" v-model="firstName"><br><br>
名:<input type="text" v-model="lastName"><br><br>
姓名:<span>{{fullName}}</span>
</div>
</template>
<script>
export default {
data(){
return {
message:'末晨曦!',
firstName:'',
lastName:''
}
},
computed:{
// 计算属性的 getter => 函数
reversedMessage () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
},
// 计算属性的 getter、setter => 对象
fullName:{
//get 获取值
get(){
return this.firstName + ' - ' + this.lastName
},
//set 修改值
set(value){
const arr = value.split('-')
this.firstName = arr[0]
this.lastName = arr[1]
}
}
},
methods:{
}
}
</script>
<style>
</style>
计算属性效果图
计算属性小结
定义:要用的属性不存在,需要通过已有属性计算得来。
原理:底层借助了Objcet.defineproperty()方法提供的getter和setter。
get函数什么时候执行?
初次读取时会执行一次
当依赖的数据发生改变时会被再次调用
优势:与方法实现相比,计算属性内部有缓存机制(复用),效率更高,调试方便
注意:
计算属性最终会出现在vm上,直接读取使用即可
如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生改变
如果计算属性确定不考虑修改,可以使用计算属性的简写形式(直接写函数形式,默认是getter )
计算属性缓存 vs 方法
<template>
<div>
<!-- 模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护 -->
<!-- 所以,对于任何复杂逻辑,你都应当使用计算属性。 -->
<div>{{ message.split('').reverse().join('') }}</div>
<p>计算属性 (Computed) 的 getter : "{{ reversedMessage }}"</p>
姓:<input type="text" v-model="firstName"><br><br>
名:<input type="text" v-model="lastName"><br><br>
姓名:<span>{{fullName}}</span>
<p>方法: "{{ reversedMessageMethod() }}"</p>
</div>
</template>
<script>
export default {
data(){
return {
message:'末晨曦!',
firstName:'',
lastName:''
}
},
computed:{
// 计算属性的 getter => 函数
reversedMessage () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
},
// 计算属性的 getter、setter => 对象
fullName:{
//get 获取值
get(){
return this.firstName + ' - ' + this.lastName
},
//set 修改值
set(value){
const arr = value.split('-')
this.firstName = arr[0]
this.lastName = arr[1]
}
}
},
methods:{
reversedMessageMethod () {
return this.message.split('').reverse().join('')
}
}
}
</script>
<style>
</style>
方法效果图
计算属性缓存 vs 方法小结
两种方式的最终结果确实是完全相同的。
然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。
只在相关响应式依赖发生改变时它们才会重新求值。
这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
计算属性 vs 侦听属性
<template>
<div>
<div id="demo">{{ fullName }}</div>
<!-- 相比侦听属性,使用计算属性更加简洁 -->
</div>
</template>
<script>
export default {
data(){
return {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
}
},
//侦听属性
watch: {
firstName(val) {
this.fullName = val + ' ' + this.lastName
},
lastName(val) {
this.fullName = this.firstName + ' ' + val
}
},
//计算属性
computed:{
// 计算属性的 getter => 函数
fullName () {
return this.firstName + ' ' + this.lastName
}
},
methods:{}
}
</script>
<style>
</style>
计算属性的 setter
概述:计算属性默认只有 getter,不过在需要时你也可以提供一个 setter
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
侦听器
概述:当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
侦听属性基本用法
<template>
<div>
<div id="demo">{{ info }}</div>
<button @click="changeLearning">改变</button>
</div>
</template>
<script>
export default {
data(){
return {
isMessage: true,
}
},
computed:{
info(){
return this.isMessage ? '侦听属性' : 'watch'
}
},
//侦听属性
watch: {
//监听message属性值,newValue代表新值,oldValue代表旧值
//正常写法
isMessage:{
immediate:true, //初始化时让handler调用一下,默认只有属性发生改变时会调用
//handler什么时候调用?当isHot发生改变时
handler(newValue,oldValue){
console.log('isMessage被修改了',newValue,oldValue);
}
},
//简写
// isMessage (newValue,oldValue) {
// console.log('isMessage被修改了',newValue,oldValue);
// },
},
methods:{
changeLearning(){
this.isMessage = !this.isMessage
}
}
}
</script>
<style>
</style>
侦听属性基本用法效果图
侦听属性基本用法小结
当被监视的属性变化时,回调函数自动调用,进行相关操作
监视的属性必须存在,才能进行监视
immediate:true, //初始化时让handler调用一下
监视有两种写法:
创建Vue时传入watch配置
通过vm.$watch监视
vm.$watch('isMessage',{
immediate:true,
handler(newValue,oldValue){
console.log('isMessage被修改了',newValue,oldValue)
}
})
深度监视
<template>
<div>
<div id="demo">姓名:{{ person.name }}</div>
<div id="demo">年龄:{{ person.age }}</div>
</div>
</template>
<script>
export default {
data(){
return {
person:{"name":"末晨曦吖", "age":18}
}
},
//侦听属性
watch: {
//监控person对象的值,对象的监控只能获取新值
person:{
//开启深度监控;监控对象中的属性值变化
deep: true,
//获取到对象的最新属性数据(obj代表新对象)
handler(obj){
console.log("name = " + obj.name + "; age=" + obj.age);
}
},
//监视多级结构中某个属性的变化
'person.name':{
handler(){
console.log('person中name被改变了')
}
}
},
}
</script>
<style>
</style>
深度监视小结
Vue中的watch默认不监测对象内部值的改变(一层)
在watch中配置 deep:true 可以监测对象内部值的改变(多层)
注意:
Vue自身可以监测对象内部值的改变,但Vue提供的watch默认不可以
使用watch时根据监视数据的具体结构,决定是否采用深度监视
五、Class 与 Style 绑定
绑定 HTML Class
对象语法
<template>
<div>
<!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
<div class="basicStyle" :class="classString" @click="changeMood">字符串写法</div>
<!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
<div class="basicStyle" :class="classObj">对象写法1</div>
<div class="basicStyle" :class="{class1:true}">对象写法2</div>
</div>
</template>
<script>
export default {
data(){
return {
classString:'string',
classObj:{
class1:false,
class2:true,
},
}
},
}
</script>
<style scoped>
.string{
color: red;
font-weight:bold;
}
.class1{
color: blue;
}
.class2{
font-weight:bold;
}
</style>
对象语法小结
写法:class="xxx",xxx可以是字符串、对象、数组
字符串写法适用于:类名不确定,要动态获取
对象写法适用于:要绑定多个样式,个数不确定,名字也不确定
数组语法
<template>
<div>
<!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
<div :class="classArr">数组写法1</div>
<!-- 渲染结果:<div class="activeClass errorClass"></div> -->
<div :class="[activeClass, errorClass]">数组写法2</div>
<!-- 渲染结果:<div class="active text-danger"></div> -->
<div :class="[isActive ? 'activeClass' : '', 'errorClass']">数组写法3</div>
<!-- 这样写将始终添加 errorClass,但是只有在 isActive 是 true 时才添加 activeClass -->
<!-- 渲染结果:<div class="activeClass errorClass"></div> -->
<div v-bind:class="[{ active: isActive4 }, 'errorClass']">数组写法4</div>
<!-- 渲染结果:<div class="active errorClass"></div> -->
<div v-bind:class="[errorClass]">数组写法5</div>
<!-- errorClass 变量 渲染data中数据 -->
<!-- 渲染结果:<div class="text-danger"></div> -->
</div>
</template>
<script>
export default {
data(){
return {
classArr:['activeClass','errorClass'],
activeClass: 'active',
errorClass: 'text-danger',
isActive:true,
isActive4:true
}
},
}
</script>
<style scoped>
</style>
数组语法小结
数组写法适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用
用在组件上
当在一个自定义组件上使用 class property 时,这些 class 将被添加到该组件的根元素上面。
这个元素上已经存在的 class 不会被覆盖。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ls-demo</title>
<script src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 组件 -->
<my-component class="baz boo"></my-component>
<!-- 渲染结果:<p class="foo bar baz boo">Hi</p> -->
<!-- 对于带数据绑定 class 也同样适用:<my-component v-bind:class="{ active: isActive }"></my-component> -->
</div>
<script>
Vue.component("my-component",{ //1.组件名为"my-component"; 2.data 写函数; 3.template 写组件的内容(元素和触发的事件)
//template 是模板的意思,在 html 里面是一个可以同时控制多个子元素的父元素。在这里定义了组件的内容
template: '<p class="foo bar">Hi</p>'
})
new Vue({
el:"#app"
});
</script>
</body>
</html>
绑定内联样式
对象语法
<template>
<div>
<!-- 绑定style样式--对象写法 -->
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }">对象写法1</div>
<div v-bind:style="styleObject">对象写法2</div>
</div>
</template>
<script>
export default {
data(){
return {
activeColor:'red',
fontSize:16,
styleObject: {
color: 'red',
fontSize: '16px'
}
}
},
}
</script>
<style scoped>
</style>
数组语法
<template>
<div>
<!-- 绑定style样式--数组写法 -->
<div v-bind:style="[baseStyles, overridingStyles]">数组写法1</div>
<div v-bind:style="sumStyles">数组写法2</div>
</div>
</template>
<script>
export default {
data(){
return {
baseStyles:{
fontSize: '40px',
color:'blue',
},
overridingStyles:{
backgroundColor:'blue',
},
sumStyles:[
{
fontSize: '40px',
color:'blue',
},
{
backgroundColor:'blue'
}
]
}
}
}
</script>
<style scoped>
</style>
自动添加前缀
当 v-bind:style 使用需要添加浏览器引擎前缀的 CSS property 时,如 transform,Vue.js 会自动侦测并添加相应的前缀。
多重值
从 2.3.0 起你可以为 style 绑定中的 property 提供一个包含多个值的数组,常用于提供多个带前缀的值
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
这样写只会渲染数组中最后一个被浏览器支持的值。
在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex。
六、条件渲染
v-if
<template>
<div>
<!-- 在 <template> 元素上使用 v-if 条件渲染分组 -->
<!-- 把<template> 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素 -->
<template v-if="ok">
<h1>Title</h1>
</template>
<!-- v-else -->
<!-- v-else 元素必须紧跟在带 v-if 或者 v-else-if 的元素的后面,否则它将不会被识别。 -->
<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else>
Now you don't
</div>
<!-- v-else-if -->
<!-- 类似于 v-else,v-else-if 也必须紧跟在带 v-if 或者 v-else-if 的元素之后。 -->
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
<!-- 用 key 管理可复用的元素 -->
<!-- Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
这么做除了使 Vue 变得非常快之外,还有其它一些好处。
例如,如果你允许用户在不同的登录方式之间切换: -->
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email-input">
</template>
<!-- 那么在上面的代码中切换 loginType 将不会清除用户已经输入的内容。
因为两个模板使用了相同的元素,<input> 不会被替换掉——仅仅是替换了它的 placeholder -->
<!-- 这样也不总是符合实际需求,所以 Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。
只需添加一个具有唯一值的 key attribute 即可 -->
<!-- 注意,<label> 元素仍然会被高效地复用,因为它们没有添加 key attribute。 -->
</div>
</template>
<script>
export default {
data(){
return {
ok:true,
type:'A',
loginType:'username'
}
}
}
</script>
<style scoped>
</style>
v-if小结
适用于:切换频率较低的场景
特点:不展示的DOM元素直接被移除
v-show
<template>
<div>
<!-- v-show 指令 -->
<!-- 不同的是带有 v-show 的元素始终会被渲染并保留在 DOM 中。
v-show 只是简单地切换元素的 CSS property display。 -->
<!-- 注意,v-show 不支持 <template> 元素,也不支持 v-else。 -->
<h1 v-show="ok">Hello!</h1>
</div>
</template>
<script>
export default {
data(){
return {
ok:true,
}
}
}
</script>
<style scoped>
</style>
v-if vs v-show
v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。
因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐
v-if 与 v-for 一起使用
当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级。
不推荐同时使用 v-if 和 v-for
七、列表渲染
数组语法:
v-for="item in items"
v-for="(item,index) in items"
items:要迭代的数组(数组名)
item:存储数组元素的变量名(数组每条对象)
index:迭代到的当前元素索引,从0开始。
用 v-for 把一个数组对应为一组元素
<template>
<div>
<ul id="example-1">
<!-- <div v-for="item of data"></div> -->
<!-- 你也可以用 of 替代 in 作为分隔符,因为它更接近 JavaScript 迭代器的语法: -->
<li v-for="(item,index) in data" :key="item.message">
当前项的索引:{{ index }}
<br>
当前对象:{{item}}
<br>
当前对象中message数据: {{ item.message }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
data: [
{ message: 'Foo' },
{ message: 'Bar' }
]
};
}
};
</script>
<style>
</style>
在 v-for 里使用对象
对象语法:
v-for="value in object"
v-for="(value,key) in object"
v-for="(value,key,index) in object"
value,对象的值
key, 对象的键
index, 索引,从0开始
<template>
<div>
<ul id="v-for-object" class="demo">
<li v-for="(value,key,index) in object" :key="index">
value值:{{ value }}
<br>
key值:{{ key }}
<br>
索引值:{{ index }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
object: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
}
};
}
};
</script>
<style>
</style>
维护状态(key的作用与原理)
<template>
<div>
<ul id="v-for-object" class="demo">
<li v-for="(p,index) in persons" :key="index">
{{p.name}} - {{p.age}}
</li>
<button @click="handerAdd">前面添加一条数据</button>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
persons:[
{id:'001',name:'张三',age:18},
{id:'002',name:'李四',age:19},
{id:'003',name:'王五',age:20}
]
};
},
methods: {
handerAdd(){
const p = {id:'004',name:'老刘',age:40}
this.persons.unshift(p)
}
},
};
</script>
<style>
</style>
key的原理
面试题:react、vue中的key有什么作用?(key的内部原理)
虚拟DOM中key的作用:
key是虚拟DOM中对象的标识,当数据发生变化时,Vue会根据【新数据】生成
【新的虚拟DOM】,随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下
对比规则:
旧虚拟DOM中找到了与新虚拟DOM相同的key
若虚拟DOM中内容没变, 直接使用之前的真实DOM
若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM
旧虚拟DOM中未找到与新虚拟DOM相同的key:创建新的真实DOM,随后渲染到到页面
用index作为key可能会引发的问题:
若对数据进行逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面
效果没问题, 但效率低
若结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题
开发中如何选择key?
最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值
如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表,使用index作为key
是没有问题的
数组更新检测
变更方法
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
<template>
<div>
<div>points:{{points}}</div>
<ul id="v-for-object" class="demo">
<li v-for="(p,index) in persons" :key="index">
{{p.name}} - {{p.age}}
</li>
<button @click="handerPush">push</button>
<button @click="handerPop">pop</button>
<button @click="handerShift">shift</button>
<button @click="handerUnshift">unshift</button>
<button @click="handerSplice">splice</button>
<button @click="handerSort">sort</button>
<button @click="handerReverse">reverse</button>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
persons:[
{id:'001',name:'张三',age:18},
{id:'002',name:'李四',age:19},
{id:'003',name:'王五',age:20}
],
points:[40,100,1,5,25,10]
};
},
methods: {
//向数组添加一条数据,从数组末尾添加
handerPush(){
let obj = { id:'004',name:'push',age:21 }
this.persons.push(obj)
},
//向数组删除一条数据,从数组末尾删除
handerPop(){
this.persons.pop()
},
//向数组删除一条数据,从数组开头删除
handerShift(){
this.persons.shift()
},
//向数组添加一条数据,从数组开头添加
handerUnshift(){
const p = {id:'000',name:'unshift',age:40}
this.persons.unshift(p)
},
//可向数组添加一条数据,还可以删除一条数据,可指定下标
handerSplice(){
// 一共三个参数:1.指定要删除或添加数据的下标;2.删除几条数据;3.要添加的数据
this.persons.splice(1,1) //删除下标为1的数据
let obj = { id:'004',name:'push',age:21 }
this.persons.splice(1,0,obj) //向下标为1的地方添加一条数据
},
//排序
handerSort(){
// this.points.sort() //结果:1,5,10,25,40,100 默认升序
this.points.sort(function(a,b){ //升序
return a - b;
})
this.points.sort(function(a,b){ //降序
return b - a;
})
},
//翻转
handerReverse(){
this.points.reverse() //结果:[10, 25, 5, 1, 100, 40]
},
},
};
</script>
<style>
</style>
总结:
Vue监视数据的原理: vue会监视data中所有层次的数据
如何监测对象中的数据?
通过setter实现监视,且要在new Vue时就传入要监测的数据
对象中后追加的属性,Vue默认不做响应式处理
如需给后添加的属性做响应式,请使用如下API:
Vue.set(target,propertyName/index,value)
vm.$set(target,propertyName/index,value)
如何监测数组中的数据?
通过包裹数组更新元素的方法实现,本质就是做了两件事:
调用原生对应的方法对数组进行更新
重新解析模板,进而更新页面
在Vue修改数组中的某个元素一定要用如下方法:
使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
Vue.set() 或 vm.$set() 或 this.$set()
特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象(data等) 添加属性
替换数组
变更方法,顾名思义,会变更调用了这些方法的原始数组。
相比之下,也有非变更方法,例如 filter()、concat() 和 slice()。
它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组。
<template>
<div>
<div>points:{{points}}</div>
<div>filter新数组:{{filterArr}}</div>
<div>concat新数组:{{concatArr}}</div>
<div>slice:{{stringObject}}</div>
<button @click="handerFilter">filter</button>
<button @click="handerConcat">concat</button>
<button @click="handerSlice">slice</button>
</div>
</template>
<script>
export default {
data() {
return {
points:[40,100,1,5,25,10],
filterArr:[],
concatArr:[],
stringObject:'',
};
},
methods: {
//filter过滤数组,不改变原数组,返回新数组
handerFilter(){
this.filterArr = this.points.filter(val => { //结果:[ 40, 100 ]
return val > 30
})
},
//concat合并数组,不改变原数组,返回新数组
handerConcat(){
let list = ['concat']
this.concatArr = this.points.concat(list) //结果:[ 40, 100, 1, 5, 25, 10, "concat" ]
},
//slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。
/**
* 拓展:
* js字符串截取函数slice()、substring()、substr()区别
* slice(): 第一个参数代表开始位置,第二个参数代表结束位置的下一个位置,截取出来的字符串的长度为第二个参数与
* 第一个参数之间的差;若参数值为负数,则将该值加上字符串长度后转为正值;若第一个参数等于大于第二个参数,
* 则返回空字符串.
substring(): 第一个参数代表开始位置,第二个参数代表结束位置的下一个位置;若参数值为负数,则将该值转为0;
两个参数中,取较小值作为开始位置,截取出来的字符串的长度为较大值与较小值之间的差.
substr(): 第一个参数代表开始位置,第二个参数代表截取的长度
*/
handerSlice(){
let string = 'ssfasdfaefasdfadswe'
this.stringObject = string.slice(1,6) //结果:sfasd
},
},
};
</script>
<style>
</style>
注意事项
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。深入响应式原理中有相关的讨论。
显示过滤/排序后的结果
有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际变更或重置原始数据。
在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。
<template>
<div>
<!-- 1.使用计算属性 -->
<li v-for="n in evenNumbers" :key="n">{{ n }}</li>
<!-- 2.使用在嵌套 v-for 循环中可以使用一个方法 -->
<ul v-for="set in sets" :key="set">
<li v-for="n in even(set)" :key="n">{{ n }}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
numbers: [ 1, 2, 3, 4, 5 ],
sets: [[ 1, 2, 3, 4, 5 ], [6, 7, 8, 9, 10]]
};
},
computed: {
evenNumbers: function () {
return this.numbers.filter(function (number) {
return number % 2 === 0
})
}
},
methods: {
even: function (numbers) {
return numbers.filter(function (number) {
return number % 2 === 0
})
}
},
};
</script>
<style>
</style>
在 v-for 里使用值范围
<div>
<span v-for="n in 10">{{ n }} </span>
</div>
结果:12345678910
在 上使用 v-for
类似于 v-if,你也可以利用带有 v-for 的 <template> 来循环渲染一段包含多个元素的内容。
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
v-for 与 v-if 一同使用
注意不推荐在同一元素上使用 v-if 和 v-for。更多细节可查阅风格指南。
当它们处于同一节点,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。
当你只想为部分项渲染节点时,这种优先级的机制会十分有用,如下:
下面的代码将只渲染未完成的 todo。
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo }}
</li>
而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if 置于外层元素 (或 <template>) 上。如:
<ul v-if="todos.length">
<li v-for="todo in todos">
{{ todo }}
</li>
</ul>
<p v-else>No todos left!</p>
在组件上使用 v-for
在自定义组件上,你可以像在任何普通元素上一样使用 v-for。
<my-component v-for="item in items" :key="item.id"></my-component>
2.2.0+ 的版本里,当在组件上使用 v-for 时,key 现在是必须的。
任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。
为了把迭代数据传递到组件里,我们要使用 prop
下面例子不自动将 item 注入到组件里的原因是,这会使得组件与 v-for 的运作紧密耦合。
明确组件数据的来源能够使组件在其他场合重复使用。
<my-component
v-for="(item, index) in items"
v-bind:item="item"
v-bind:index="index"
v-bind:key="item.id"
></my-component>
八、事件处理
监听事件
语法:
<!--完整写法-->
<button v-on:事件名="函数名/vue表达式">点我</button>
<!--简化写法-->
<button @事件名="函数名/vue表达式">点我</button>
可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。
<template>
<div>
<button v-on:click="counter += 1">Add 1</button>
<p>The button above has been clicked {{ counter }} times.</p>
</div>
</template>
<script>
export default {
data() {
return {
counter:0
};
},
methods: {
},
};
</script>
<style>
</style>
事件处理方法
<template>
<div>
<!-- `greet` 是在下面定义的方法名 -->
<button v-on:click="greet">Greet</button>
<p>{{counter}}</p>
</div>
</template>
<script>
export default {
data() {
return {
counter:0
};
},
// 在 `methods` 对象中定义方法
methods: {
greet(){
// `this` 在方法里指向当前 Vue 实例
this.counter = this.counter + 1
}
},
};
</script>
<style>
</style>
内联处理器中的方法
<template>
<div>
<button v-on:click="say('hi', $event)">Say hi</button>
<button v-on:click="say('what', $event)">Say what</button>
</div>
</template>
<script>
export default {
data() {
return {
counter:0
};
},
// 在 `methods` 对象中定义方法
methods: {
say: function (message, event) {
// 现在我们可以访问原生事件对象
if (event) {
event.preventDefault()
}
alert(message)
}
},
};
</script>
<style>
</style>
事件修饰符
在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。
尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。
语法: <button @事件名.事件修饰符="函数名/vue表达式"></button>
prevent:阻止默认事件(常用)
stop:阻止事件冒泡(常用)
once:事件只触发一次(常用)
capture:使用事件的捕获模式
self:只有event.target是当前操作的元素时才触发事件
passive:事件的默认行为立即执行,无需等待事件回调执行完毕(.passive 修饰符尤其能够提升移动端的性能。)
修饰符可以连续写,比如可以这么用:@click.prevent.stop="showInfo"
<template>
<div>
<!-- 阻止默认事件 -->
<a href="http://www.atguigu.com" @click.prevent="showInfo">点击默认事件</a>
<!-- 阻止事件冒泡 -->
<div class="demo1" @click="showInfo">
<button @click.stop="showInfo">点击事件冒泡</button>
</div>
<!-- 事件只触发一次 -->
<button @click.once="showInfo">点击只触发一次</button>
<!-- 使用事件的捕获模式 -->
<div class="box1" @click.capture="showMsg(1)">
div1
<div class="box2" @click="showMsg(2)">
div2
</div>
</div>
<!-- 只有event.target是当前操作的元素时才触发事件 -->
<div class="demo1" @click.self="showInfo">
<button @click="showInfo">点我只有event.target是当前操作的元素时才触发事件</button>
</div>
<!-- 事件的默认行为立即执行,无需等待事件回调执行完毕 -->
<!-- passive的作用 让事件的默认行为立即执行,无需等待事件回调执行完毕
@wheel.passive 让鼠标滚轮滚动的时候先触发默认行为再执行事件回调 -->
<ul @wheel.passive="demo" class="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
name:'是末晨曦吖!'
};
},
// 在 `methods` 对象中定义方法
methods: {
showInfo(e){
alert('你好!')
},
showMsg(msg){
console.log(msg)
},
demo(){
for (let i = 0; i < 10000; i++) {
console.log('#')
}
console.log('太多了,累坏了')
}
},
};
</script>
<style scoped>
.demo1{
height: 50px;
background-color: skyblue;
}
.box1{
padding: 5px;
background-color: skyblue;
}
.box2{
padding: 5px;
background-color: orange;
}
.list{
width: 200px;
height: 200px;
background-color: peru;
overflow: auto;
}
li{
height: 100px;
}
</style>
按键修饰符
Vue 允许为 v-on 在监听键盘事件时添加按键修饰符
<template>
<div>
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">
<!-- PageDown 时被调用 -->
<!-- event.key 获取发生按键事件时按下的键盘按钮 -->
<input v-on:keyup.page-down="onPageDown">
</div>
</template>
<script>
export default {
data() {
return {
};
},
// 在 `methods` 对象中定义方法
methods: {
submit(e){
alert('提交了!!!')
},
onPageDown(){
alert('点击了page-down')
},
},
};
</script>
<style scoped>
</style>
按键码
keyCode 的事件用法已经被废弃了并可能不会被最新的浏览器支持。
为了在必要的情况下支持旧浏览器,Vue 提供了绝大多数常用的按键码的别名:
.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right
<template>
<div>
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<!-- <input v-on:keyup.enter="submit"> -->
<!-- enter的键盘码 13 -->
<!-- <input v-on:keyup.13="submit"> -->
<!-- 自定义按键修饰符别名 customEnter -->
<input v-on:keyup.customEnter="submit">
<!-- PageDown 时被调用 -->
<!-- event.key 获取发生按键事件时按下的键盘按钮 -->
<input v-on:keyup.page-down="onPageDown">
</div>
</template>
<script>
import Vue from 'vue'
Vue.config.keyCodes.customEnter = 13
export default {
data() {
return {
};
},
// 在 `methods` 对象中定义方法
methods: {
submit(e){
alert('提交了!!!')
},
onPageDown(){
alert('点击了page-down')
},
},
};
</script>
<style scoped>
</style>
系统修饰键
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
.ctrl
.alt
.shift
.meta
注意:在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆)。在其他特定键盘上,尤其在 MIT 和 Lisp 机器的键盘、以及其后继产品,比如 Knight 键盘、space-cadet 键盘,meta 被标记为“META”。在 Symbolics 键盘上,meta 被标记为“META”或者“Meta”。
<template>
<div>
<!-- Alt + C -->
<input v-on:keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div v-on:click.ctrl="doSomething">Do something</div>
<!-- 请注意修饰键与常规按键不同,在和 keyup 事件一起用时,事件触发时修饰键必须处于按下状态。
换句话说,只有在按住 ctrl 的情况下释放其它按键,才能触发 keyup.ctrl。而单单释放 ctrl 也不会触发事件。
如果你想要这样的行为,请为 ctrl 换用 keyCode:keyup.17。 -->
</div>
</template>
<script>
export default {
data() {
return {
};
},
// 在 `methods` 对象中定义方法
methods: {
clear(){
alert('Alt + C 触发了哦!!')
},
doSomething(){
alert('Ctrl + Click 触发了哦!!')
},
},
};
</script>
<style scoped>
</style>
.exact 修饰符
.exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button v-on:click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button v-on:click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button v-on:click.exact="onClick">A</button>
鼠标按钮修饰符
.left
.right
.middle
这些修饰符会限制处理函数仅响应特定的鼠标按钮。
<template>
<div>
<!-- 点击鼠标右键 -->
<div v-on:click.right="doSomething">Do something</div>
</div>
</template>
<script>
export default {
data() {
return {
};
},
// 在 `methods` 对象中定义方法
methods: {
clear(){
alert('Alt + C 触发了哦!!')
},
doSomething(){
alert('点击鼠标右键 触发了哦!!')
},
},
};
</script>
<style scoped>
</style>
为什么在 HTML 中监听事件?
你可能注意到这种事件监听的方式违背了关注点分离 (separation of concern) 这个长期以来的优良传统。
但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,
它不会导致任何维护上的困难。实际上,使用 v-on 有几个好处:
扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。
因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。
当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。
九、表单输入绑定
基础用法
v-model
概述:
Vue的双向绑定可以实现: 数据变化的时候, 页面会自动刷新, 页面变化的时候,数据也会自动变化。
注意:
双向绑定, 只能绑定文本框,单选按钮,复选框,文本域,下拉列表等
文本框/单选按钮/textarea, 绑定的数据是字符串类型
单个复选框, 绑定的是boolean类型
多个复选框, 绑定的是数组
select单选对应字符串,多选对应也是数组
文本
<template>
<div>
<!-- 文本 -->
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message:''
};
},
// 在 `methods` 对象中定义方法
methods: {
},
};
</script>
<style scoped>
</style>
多行文本
在文本区域插值 (<textarea>{{text}}</textarea>) 并不会生效,应用 v-model 来代替。
<template>
<div>
<!-- 文本 -->
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
<br>
<!-- 多行文本 -->
<textarea v-model="message" placeholder="add multiple lines"></textarea>
</div>
</template>
<script>
export default {
data() {
return {
message:''
};
},
// 在 `methods` 对象中定义方法
methods: {
},
};
</script>
<style scoped>
</style>
复选框
<template>
<div>
<!-- 单个复选框,绑定到布尔值: -->
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
<br>
<!-- 多个复选框,绑定到同一个数组: -->
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<br>
<span>Checked names: {{ checkedNames }}</span>
</div>
</template>
<script>
export default {
data() {
return {
checked:false,
checkedNames:[],
};
},
// 在 `methods` 对象中定义方法
methods: {
},
};
</script>
<style scoped>
</style>
单选按钮
<template>
<div>
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<br>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<br>
<span>Picked: {{ picked }}</span>
</div>
</template>
<script>
export default {
data() {
return {
picked:''
};
},
// 在 `methods` 对象中定义方法
methods: {
},
};
</script>
<style scoped>
</style>
选择框
如果 v-model 表达式的初始值未能匹配任何选项,<select> 元素将被渲染为“未选中”状态。在 iOS 中,这会使用户无法选择第一个选项。因为这样的情况下,iOS 不会触发 change 事件。因此,更推荐像上面这样提供一个值为空的禁用选项。
<template>
<div>
<!-- 单选时: -->
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Selected: {{ selected }}</span>
<br>
<!-- 多选时 (绑定到一个数组):multiple(设置为多选) -->
<select v-model="selectedArr" multiple style="width: 50px;">
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>selectedArr: {{ selectedArr }}</span>
<br>
<!-- 用 v-for 渲染的动态选项: -->
<select v-model="selectedFor">
<option v-for="option in options" v-bind:value="option.value" :key="option.value">
{{ option.text }}
</option>
</select>
<span>selectedFor: {{ selectedFor }}</span>
</div>
</template>
<script>
export default {
data() {
return {
selected:'',
selectedArr:[],
selectedFor:'A',
options: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
]
};
},
// 在 `methods` 对象中定义方法
methods: {
},
};
</script>
<style scoped>
</style>
值绑定
<!-- 当默认选中时,`picked` 为字符串 "a" -->
<input type="radio" v-model="picked" value="a">
<!-- `toggle` 为 true 或 false -->
<input type="checkbox" v-model="toggle">
<!-- 当选中第一个选项时,`selected` 为字符串 "abc" -->
<select v-model="selected">
<option value="abc">ABC</option>
</select>
复选框
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no"
/>
// 当选中时
vm.toggle === 'yes'
// 当没有选中时
vm.toggle === 'no'
这里的 true-value 和 false-value attribute 并不会影响输入控件的 value attribute,因为浏览器在提交表单时并不会包含未被选中的复选框。如果要确保表单中这两个值中的一个能够被提交,(即“yes”或“no”),请换用单选按钮。
单选按钮
<input type="radio" v-model="pick" v-bind:value="a">
// 当选中时
vm.pick === vm.a
选择框的选项
<template>
<div>
<select v-model="selected">
<!-- 内联对象字面量 -->
<option v-bind:value="{ number: 123 }">123</option>
</select>
</div>
</template>
<script>
export default {
data() {
return {
selected:{
number: 123 //默认选中
},
};
},
// 在 `methods` 对象中定义方法
methods: {
},
};
</script>
<style scoped>
</style>
修饰符
.lazy
在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。
你可以添加 lazy 修饰符,从而转为在 change 事件_之后_进行同步
<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg">
.number
如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:
<input v-model.number="age" type="number">
.trim
如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:
<input v-model.trim="msg">
在组件上使用 v-model
v-model是v-bind和v-on的语法糖
<input :value="text" @input="text=$event.target.value">
<template>
<div id="con">
<!-- 我是父组件 -->
<p>{{ text }}</p>
<child v-model="text"></child>
</div>
</template>
<script>
import child from "./aa.vue";
export default {
name: "bb",
components: { child },
data: function () {
return {
text: "我是父组件",
};
},
};
</script>
<style scoped>
</style>
<template>
<div class="child">
<!-- 我是子组件 -->
<p>{{ msg }}</p>
<el-button @click="btnClick">点击改变父组件内容</el-button>
</div>
</template>
<script>
export default {
model: {
prop: "msg",
event: "respond",
},
props: {
msg: {
type: String,
},
},
methods: {
btnClick() {
this.$emit("respond", "我是子组件");
},
},
};
</script>
<style scoped>
</style>
注意:子组件中有个model,它是v-model通信至关重要的条件,他有两个属性,prop是传过来的属性,
event是子组件要发射的事件名称,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event。
这样就可以实现父子组件之间的通信,子组件可以拿到父组件传过来的值,子组件也可以更改值。
十、组件基础
基本实例
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ls-demo</title>
<script src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<button-counter></button-counter>
</div>
<script type="text/javascript">
//全局注册组件:在所有的vue实例中都可以使用组件
//参数1:组件名称,参数2:具体的组件
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
new Vue({
el:'#app',
data() {
return {
}
},
});
</script>
</body>
</html>
组件的复用
注意当点击按钮时,每个组件都会各自独立维护它的 count。
因为你每用一次组件,就会有一个它的新实例被创建。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ls-demo</title>
<script src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 可以将组件进行任意次数的复用 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
<script type="text/javascript">
//全局注册组件:在所有的vue实例中都可以使用组件
//参数1:组件名称,参数2:具体的组件
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
new Vue({
el:'#app',
data() {
return {
}
},
});
</script>
</body>
</html>
渲染结果:
data 必须是一个函数
当我们定义这个 <button-counter> 组件时,你可能会发现它的 data 并不是像这样直接提供一个对象:
data: {
count: 0
}
取而代之的是,一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:
data: function () {
return {
count: 0
}
}
注意:如果 Vue 没有这条规则,点击一个按钮就会一样影响到其它所有实例
组件的组织
模块与组件、模块化与组件化
模块
理解:向外提供特定功能的 js 程序,一般就是一个 js 文件
为什么:js 文件很多很复杂
作用:复用 js,简化 js 的编写,提高 js 运行效率
组件
定义:用来实现局部功能的代码和资源的集合(html/css/js/image…)
为什么:一个界面的功能很复杂
作用:复用编码,简化项目编码,提高运行效率
模块化
当应用中的 js 都以模块来编写的,那这个应用就是一个模块化的应用
组件化
当应用中的功能都是多组件的方式来编写的,那这个应用就是一个组件化的应用
非单文件组件
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ls-demo</title>
<script src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 第三步:编写组件标签 -->
<student></student>
</div>
<script type="text/javascript">
//第一步:创建student组件
const student = Vue.extend({
template:`
<div>
<h2>学生姓名:{{studentName}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
`,
data(){
return {
studentName:'末晨曦吖',
age:18
}
}
})
//创建vm
new Vue({
el:'#app',
data() {
return {
}
},
//第二步:注册组件(局部注册)
components:{
// student:'student' 可自定义组件名称,下面是简写
student
}
});
</script>
</body>
</html>
渲染结果:
总结:
Vue中使用组件的三大步骤:
定义组件(创建组件)
注册组件
使用组件(写组件标签)
如何定义一个组件?
使用Vue.extend(options)创建,其中options和new Vue(options)时传入的options几乎一样,
但也有点区别:
el不要写,因为最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器
data必须写成函数,因为避免组件被复用时,数据存在引用关系
如何注册组件?
局部注册:new Vue的时候传入components选项
全局注册:Vue.component('组件名',组件)
编写组件标签:<school></school>
关于组件名:
一个单词组成:
第一种写法(首字母小写):school
第二种写法(首字母大写):School
多个单词组成:
第一种写法(kebab-case命名):my-school
第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)
备注:
组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行
可以使用name配置项指定组件在开发者工具中呈现的名字
关于组件标签:
第一种写法:<school></school>
第二种写法:<school/>
备注:不使用脚手架时,<school/>会导致后续组件不能渲染
一个简写方式:const school = Vue.extend(options)可简写为:const school = options
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ls-demo</title>
<script src="node_modules/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 第三步:编写组件标签 -->
<student></student>
</div>
<script type="text/javascript">
//第一步:创建student组件
// const student = Vue.extend({
// template:`
// <div>
// <h2>学生姓名:{{studentName}}</h2>
// <h2>学生年龄:{{age}}</h2>
// </div>
// `,
// data(){
// return {
// studentName:'末晨曦吖',
// age:18
// }
// }
// })
// 简写 直接写options
const student = {
template:`
<div>
<h2>学生姓名:{{studentName}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
`,
data(){
return {
studentName:'末晨曦吖',
age:18
}
}
}
//创建vm
new Vue({
el:'#app',
data() {
return {
}
},
//第二步:注册组件(局部注册)
components:{
// student:'student' 可自定义组件名称,下面是简写
student
}
});
</script>
</body>
</html>
单文件组件
Student.vue
<template>
<div>
<h2>学生姓名:{{name}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
</template>
<script>
export default {
name:'Student',
data() {
return {
name:'末晨曦吖',
age:18
}
},
}
</script>
App.vue
<template>
<div>
<!-- 使用组件 -->
<School></School>
</div>
</template>
<script>
//引入组件
import Student from './Student.vue'
export default {
name:'App',
components:{
//注册组件
Student
}
}
</script>
通过 Prop 向子组件传递数据
Prop 是你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,
它就变成了那个组件实例的一个 property。为了给博文组件传递一个标题,我们可以用一个
props 选项将其包含在该组件可接受的 prop 列表中:
Vue.component('blog-post', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})
基础实例
<template>
<div id="app">
<HelloWorld msg="我是App组件传过来的数据" :text="text" />
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App', //父组件
components: {
HelloWorld
},
data(){
return{
text:'我是绑定变量'
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h1>{{ text }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
//第一种写法
// props: ['msg'],
//第二种写法
props: {
msg:{
type: String, // 定义接收类型
required: false, //是否必传
// default: '我是msg默认数据' //默认数据
default: () => {
return "我是msg默认数据";
}
},
text:{
type: String, // 定义接收类型
required: false, //是否必传
// default: '我是msg默认数据' //默认数据
default: () => {
return "我是text默认数据";
}
},
},
}
</script>
<style scoped>
</style>
单个根元素
每个组件必须只有一个根元素
//错误写法
<template>
<h1>{{ msg }}</h1>
<h1>{{ text }}</h1>
</template>
//正确写法
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h1>{{ text }}</h1>
</div>
</template>
监听子组件事件
使用事件抛出一个值
<template>
<div id="app">
<HelloWorld msg="我是App组件传过来的数据" :text="text" @changeText="changeText" />
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App', //父组件
components: {
HelloWorld
},
data(){
return{
text:'我是绑定变量'
}
},
methods:{
changeText(e){
console.log(e);
this.text = e
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h1>{{ text }}</h1>
<button @click="change">点击向父组件发射事件</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
//第一种写法
// props: ['msg'],
//第二种写法
props: {
msg:{
type: String, // 定义接收类型
required: false, //是否必传
// default: '我是msg默认数据' //默认数据
default: () => {
return "我是msg默认数据";
}
},
text:{
type: String, // 定义接收类型
required: false, //是否必传
// default: '我是msg默认数据' //默认数据
default: () => {
return "我是text默认数据";
}
},
},
methods:{
change(){
this.$emit('changeText','我是改变后的text')
}
}
}
</script>
<style scoped>
</style>
在组件上使用 v-model
自定义事件也可以用于创建支持 v-model 的自定义输入组件。记住:
<input v-model="searchText">
等价于:
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
当用在组件上时,v-model 则会这样:
<custom-input
v-bind:value="searchText"
v-on:input="searchText = $event"
></custom-input>
为了让它正常工作,这个组件内的 必须:
- 将其 value attribute 绑定到一个名叫 value 的 prop 上
- 在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出
写成代码之后是这样的:
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})
现在 v-model 就应该可以在这个组件上完美地工作起来了:
<custom-input v-model="searchText"></custom-input>
基础实例
<template>
<div id="app">
<HelloWorld v-model="searchText" />
<!-- 等价于 -->
<!-- <HelloWorld
v-bind:value="searchText"
v-on:input="searchText = $event"
></HelloWorld> -->
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App', //父组件
components: {
HelloWorld
},
data(){
return{
searchText:''
}
},
methods:{
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
<template>
<div class="hello">
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: ['value'],
data(){
return {
}
},
methods:{}
}
</script>
<style scoped>
</style>
通过插槽分发内容
和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样:
<alert-box>
Something bad happened.
</alert-box>
我们可以使用插槽实现(slot)
基础实例
<template>
<div id="app">
<HelloWorld v-model="searchText" > 我是插槽显示的数据 </HelloWorld>
<!-- 等价于 -->
<!-- <HelloWorld
v-bind:value="searchText"
v-on:input="searchText = $event"
></HelloWorld> -->
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App', //父组件
components: {
HelloWorld
},
data(){
return{
searchText:'',
}
},
created(){
},
methods:{
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
<template>
<div class="hello">
<input
placeholder="222"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
// 插槽
<slot></slot>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: ['value'],
data(){
return {
}
},
methods:{}
}
</script>
<style scoped>
</style>
动态组件
动态组件可以通过 Vue 的 <component> 元素加一个特殊的 is attribute 来实现
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>
实例:
<template>
<div id="app">
<component v-bind:is="currentTabComponent"></component>
<button @click="changeComponent">点击切换组件</button>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import IndexComponent from './components/IndexComponent.vue'
export default {
name: 'App', //父组件
components: {
HelloWorld,
IndexComponent
},
data(){
return{
currentTabComponent:'HelloWorld',
}
},
created(){
},
methods:{
changeComponent(){
//组件模板切换位 IndexComponent
this.currentTabComponent = 'IndexComponent'
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
<template>
<div class="hello">
<input
placeholder="placeholder"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: ['value'],
data(){
return {
}
},
methods:{}
}
</script>
<style scoped>
</style>
<template>
<div>
IndexComponent
</div>
</template>
<script>
export default {
name: 'IndexComponent',
}
</script>
<style>
</style>
解析 DOM 模板时的注意事项
有些 HTML 元素,诸如 <ul>、<ol>、<table> 和 <select>,对于哪些元素可以出现在其内部是有
严格限制的。而有些元素,诸如 <li>、<tr> 和 <option>,只能出现在其它某些特定的元素内部。
这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:
<table>
<blog-post-row></blog-post-row>
</table>
这个自定义组件 会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 is attribute 给了我们一个变通的办法:
<table>
<tr is="blog-post-row"></tr>
</table>
需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在的:
- 字符串 (例如:template: ‘…’)
- 单文件组件 (.vue)
<script type="text/x-template">
十一、组件注册
组件名
在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了:
Vue.component('my-component-name', { /* ... */ })
该组件名就是 Vue.component 的第一个参数 (my-component-name)。
当直接在 DOM 中使用一个组件 (而不是在字符串模板或单文件组件) 的时候,我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)
组件名大小写
定义组件名的方式有两种:
使用 kebab-case
Vue.component('my-component-name', { /* ... */ })
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 。
使用 PascalCase
Vue.component('MyComponentName', { /* ... */ })
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 和 都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
全局注册
到目前为止,我们只用过 Vue.component 来创建组件:
Vue.component('my-component-name', {
// ... 选项 ...
})
这些组件是全局注册的。
也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。
比如:
<div id="app">
<component-a></component-a>
<component-b></component-b>
<component-c></component-c>
</div>
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })
new Vue({ el: '#app' })
在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以相互使用。
局部注册
全局注册往往是不够理想的。
比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用
一个组件了,它仍然会被包含在你最终的构建结果中。
这造成了用户下载的 JavaScript 的无谓的增加。
在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
然后在 components 选项中定义你想要使用的组件:
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
对于 components 对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。
注意局部注册的组件在其子组件中不可用。
例如,如果你希望 ComponentA 在 ComponentB 中可用,则你需要这样写:
var ComponentA = { /* ... */ }
var ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}
或者如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像:
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
注意在 ES2015+ 中,在对象中放一个类似 ComponentA 的变量名其实是 ComponentA: ComponentA 的缩写,即这个变量名同时是:
- 用在模板中的自定义元素的名称
- 包含了这个组件选项的变量名
模块系统
如果你没有通过 import/require 使用一个模块系统,也许可以暂且跳过这个章节。
在模块系统中局部注册
如果你使用了,那么我们会为你提供一些特殊的使用说明和注意事项。
如果你还在阅读,说明你使用了诸如 Babel 和 webpack 的模块系统。
在这些情况下,我们推荐创建一个 components 目录,并将每个组件放置在其各自的文件中。
然后你需要在局部注册之前导入每个你想使用的组件。
例如,在一个假设的 ComponentB.js 或 ComponentB.vue 文件中:
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
},
// ...
}
现在 ComponentA 和 ComponentC 都可以在 ComponentB 的模板中使用了。
基础组件的自动化全局注册
可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。
我们有时候会把它们称为基础组件,它们会在各个组件中被频繁的用到。
所以会导致很多组件里都会有一个包含基础组件的长列表:
import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'
export default {
components: {
BaseButton,
BaseIcon,
BaseInput
}
}
而只是用于模板中的一小部分:
<BaseInput
v-model="searchText"
@keydown.enter="search"
/>
<BaseButton @click="search">
<BaseIcon name="search"/>
</BaseButton>
如果你恰好使用了 webpack (或在内部使用了 webpack 的 Vue CLI 3+),那么就可以使用 require.context 只全局注册这些非常通用的基础组件。这里有一份可以让你在应用入口文件 (比如 src/main.js) 中全局导入基础组件的示例代码:
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)
// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)
// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})
记住全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生。
十二、Prop
Prop 的大小写 (camelCase vs kebab-case)
HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。
这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的
kebab-case (短横线分隔命名) 命名:
Vue.component('blog-post', {
// 在 JavaScript 中是 camelCase 的
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>
重申一次,如果你使用字符串模板,那么这个限制就不存在了。
Prop 类型
到这里,我们只看到了以字符串数组形式列出的 prop:
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
但是,通常你希望每个 prop 都有指定的值类型。
这时,你可以以对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}
这不仅为你的组件提供了文档,还会在它们遇到错误的类型时从浏览器的 JavaScript 控制台提示用户。你会在这个页面接下来的部分看到类型检查和其它 prop 验证。
传递静态或动态 Prop
基础例子
<template>
<div id="app">
<hello-world value="我是App组件传过来的静态值"></hello-world>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App', //父组件
components: {
HelloWorld,
},
data(){
return{
}
},
methods:{
}
}
</script>
<style>
</style>
<template>
<div class="hello">
<input
placeholder="placeholder"
v-bind:value="value"
>
</div>
</template>
<script>
export default {
name: 'HelloWorld',//子组件
props: ['value'],
data(){
return {
}
},
methods:{
}
}
</script>
<style scoped>
</style>
像这样,你已经知道了可以像这样给 prop 传入一个静态的值:
<blog-post title="My journey with Vue"></blog-post>
你也知道 prop 可以通过 v-bind 动态赋值,例如:
<!-- 动态赋予一个变量的值 -->
<blog-post v-bind:title="post.title"></blog-post>
<!-- 动态赋予一个复杂表达式的值 -->
<blog-post
v-bind:title="post.title + ' by ' + post.author.name"
></blog-post>
在上述两个示例中,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个 prop。
传入一个数字
<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>
<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>
传入一个布尔值
<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>
<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>
<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>
传入一个数组
<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>
<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>
传入一个对象
<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post
v-bind:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
></blog-post>
<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>
传入一个对象的所有 property
如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind
(取代 v-bind:prop-name)。
例如,对于一个给定的对象 post:
<blog-post v-bind="post"></blog-post>
post: {
id: 1,
title: 'My Journey with Vue'
}
等价于:
<blog-post
v-bind:id="post.id"
v-bind:title="post.title"
></blog-post>
单向数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:
父级 prop 的更新会向下流动到子组件中,但是反过来则不行。
这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。
这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
这里有两种常见的试图变更一个 prop 的情形:
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。
在这种情况下,最好定义一个本地的 data property 并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
这个 prop 以一种原始的值传入且需要进行转换。
在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
Prop 验证
我们可以为组件的 prop 指定验证要求,例如你知道的这些类型。
如果有一个需求没有被满足,则 Vue 会在浏览器控制台中警告你。
这在开发一个会被别人用到的组件时尤其有帮助。
为了定制 prop 的验证方式,你可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:
Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。
注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的。
类型检查
type 可以是下列原生构造函数中的一个:
String
Number
Boolean
Array
Object
Date
Function
Symbol
额外的,type 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。例如,给定下列现成的构造函数:
function Person (firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
你可以使用:
Vue.component('blog-post', {
props: {
author: Person
}
})
来验证 author prop 的值是否是通过 new Person 创建的。
完整例子:
1、创建类型
class Person {
constructor(name, age) {
this.name = name
this.age = age}
}
2、在组件中指定test的类型
Vue.component('demo', {
props: {
test: {type: Person,}
},
template: '<div>{{test.name}}</div>',
})
3、在父组件中的计算属性必须使用"new Person();"创建这个类,使用其它类型则会报错
<div id=app>
<demo :test="Person"></demo>
</div>
var app = new Vue({
el: '#app',
computed: {
Person() {
var ts=new Person();
ts.name='yyy';
return ts;
}},
})
非 Prop 的 Attribute
一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute。
因为显式定义的 prop 适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景。这也是为什么组件可以接受任意的 attribute,而这些 attribute 会被添加到这个组件的根元素上。
例如,想象一下你通过一个 Bootstrap 插件使用了一个第三方的 组件,这个插件需要在其 上用到一个 data-date-picker attribute。我们可以将这个 attribute 添加到你的组件实例上:
<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>
然后这个 data-date-picker=“activated” attribute 就会自动添加到 的根元素上。
替换/合并已有的 Attribute
想象一下 <bootstrap-date-input> 的模板是这样的:
<input type="date" class="form-control">
为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:
<bootstrap-date-input
data-date-picker="activated"
class="date-picker-theme-dark"
></bootstrap-date-input>
在这种情况下,我们定义了两个不同的 class 的值:
- form-control,这是在组件的模板内设置好的
- date-picker-theme-dark,这是从组件的父级传入的
对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值。
所以如果传入 type=“text” 就会替换掉 type=“date” 并把它破坏!
庆幸的是,class 和 style attribute 会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control date-picker-theme-dark。
禁用 Attribute 继承
如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false
例如:
Vue.component('my-component', {
inheritAttrs: false,
// ...
})
这尤其适合配合实例的 $attrs property 使用,该 property 包含了传递给一个组件的 attribute 名和 attribute 值,例如:
{
required: true,
placeholder: 'Enter your username'
}
有了 inheritAttrs: false 和 $attrs,你就可以手动决定这些 attribute 会被赋予哪个元素。在撰写基础组件的时候是常会用到的:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`
})
注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定。
这个模式允许你在使用基础组件的时候更像是使用原始的 HTML 元素,而不会担心哪个元素是真正的根元素:
<base-input
label="Username:"
v-model="username"
required
placeholder="Enter your username"
></base-input>
十三、自定义事件
事件名
不同于组件和 prop,事件名不存在任何自动化的大小写转换。
而是触发的事件名需要完全匹配监听这个事件所用的名称。
举个例子,如果触发一个 camelCase 名字的事件:
this.$emit('myEvent')
则监听这个名字的 kebab-case 版本是不会有任何效果的:
<!-- 没有效果 -->
<my-component v-on:my-event="doSomething"></my-component>
不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCase 或 PascalCase 了。并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到。
因此,我们推荐你始终使用 kebab-case 的事件名。
自定义组件的 v-model
一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、
复选框等类型的输入控件可能会将 value attribute 用于不同的目的。
model 选项可以用来避免这样的冲突:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
现在在这个组件上使用 v-model 的时候:
<base-checkbox v-model="lovingVue"></base-checkbox>
这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新。
注意你仍然需要在组件的 props 选项里声明 checked 这个 prop。
若有不明白之处,可以看 在组件上使用 v-model
章节
将原生事件绑定到组件
你可能有很多次想要在一个组件的根元素上直接监听一个原生事件。
这时,你可以使用 v-on 的 .native 修饰符:
<base-input v-on:focus.native="onFocus"></base-input>
在有的时候这是很有用的,不过在你尝试监听一个类似 的非常特定的元素时,这并不是个好主意。
比如上述 组件可能做了如下重构,所以根元素实际上是一个
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
这时,父级的 .native 监听器将静默失败。它不会产生任何报错,但是 onFocus 处理函数不会如你预期地被调用。
为了解决这个问题,Vue 提供了一个 $listeners property,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:
{
focus: function (event) { /* ... */ }
input: function (value) { /* ... */ },
}
有了这个 l i s t e n e r s p r o p e r t y ,你就可以配合 v − o n = " listeners property,你就可以配合 v-on=" listenersproperty,你就可以配合v−on="listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 的你希望它也可以配合 v-model 工作的组件来说,为这些监听器创建一个类似下述 inputListeners 的计算属性通常是非常有用的:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign({},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 `v-model` 的工作
input: function (event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})
现在 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 元素一样使用了:所有跟它相同的 attribute 和监听器都可以工作,不必再使用 .native 监听器。
.sync 修饰符
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源。
这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:
this.$emit('update:title', newTitle)
然后父组件可以监听那个事件并根据需要更新一个本地的数据 property。例如:
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:
注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model。
当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:
<text-document v-bind.sync="doc"></text-document>
这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。
将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
若有不明白之处,这里专栏讲解.sync修饰符文章
视频教学地址:sync修饰符视频教学
十四、插槽
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。
它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。
插槽内容
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,
将 <slot> 元素作为承载分发内容的出口。
它允许你像这样合成组件:
<navigation-link url="/profile">
Your Profile
</navigation-link>
然后你在 的模板中可能会写为:
<a
v-bind:href="url"
class="nav-link"
>
<slot></slot>
</a>
当组件渲染的时候, 将会被替换为“Your Profile”。插槽内可以包含任何模板代码,包括 HTML:
<navigation-link url="/profile">
<!-- 添加一个 Font Awesome 图标 -->
<span class="fa fa-user"></span>
Your Profile
</navigation-link>
甚至其它的组件:
<navigation-link url="/profile">
<!-- 添加一个图标的组件 -->
<font-awesome-icon name="user"></font-awesome-icon>
Your Profile
</navigation-link>
如果 的 template 中没有包含一个 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
编译作用域
编译作用域很简单,就是组件写在哪个模板,作用域就是哪个模板
当你想在一个插槽中使用数据时,例如:
<navigation-link url="/profile">
Logged in as {{ user.name }}
</navigation-link>
该插槽跟模板的其它地方一样可以访问相同的实例 property (也就是相同的“作用域”),而不能访问 的作用域。例如 url 是访问不到的:
<navigation-link url="/profile">
Clicking here will send you to: {{ url }}
<!--
这里的 `url` 会是 undefined,因为其 (指该插槽的) 内容是
_传递给_ <navigation-link> 的而不是
在 <navigation-link> 组件*内部*定义的。
-->
</navigation-link>
作为一条规则,请记住:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
后备内容(匿名插槽)
有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,
它只会在没有提供内容的时候被渲染。例如在一个 <submit-button> 组件中:
<button type="submit">
<slot></slot>
</button>
我们可能希望这个 内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在 标签内:
<button type="submit">
<slot>Submit</slot>
</button>
现在当我在一个父级组件中使用 并且不提供任何插槽内容时:
<submit-button></submit-button>
后备内容“Submit”将会被渲染:
<button type="submit">
Submit
</button>
但是如果我们提供内容:
<submit-button>
Save
</submit-button>
则这个提供的内容将会被渲染从而取代后备内容:
<button type="submit">
Save
</button>
案例:
src/App.vue:
<template>
<div class="container">
<Category title="美食" >
美食
</Category>
<Category title="游戏" >
<ul>
<li v-for="(g,index) in games" :key="index">{{g}}</li>
</ul>
</Category>
//我不传,要使用插槽的默认数据
<Category title="电影">
</Category>
</div>
</template>
<script>
import Category from './components/Category'
export default {
name:'App',
components:{Category},
data() {
return {
games:['植物大战僵尸','红色警戒','空洞骑士','王国']
}
},
}
</script>
<style scoped>
.container{
display: flex;
justify-content: space-around;
}
</style>
src/components/Category.vue:
<template>
<div class="category">
<h3>{{title}}分类</h3>
<!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
<slot>我是一些默认值,当使用者没有传递具体结构时,我会出现</slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title']
}
</script>
<style scoped>
.category{
background-color: skyblue;
width: 200px;
height: 300px;
}
h3{
text-align: center;
background-color: orange;
}
</style>
具名插槽
有时我们需要多个插槽。例如对于一个带有如下模板的 组件:
<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
</footer>
</div>
对于这样的情况, 元素有一个特殊的 attribute:name。这个 attribute 可以用来定义额外的插槽:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
一个不带 name 的 出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
现在 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 中的内容都会被视为默认插槽的内容。
然而,如果你希望更明确一些,仍然可以在一个 中包裹默认插槽的内容:
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
任何一种写法都会渲染出:
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
注意 v-slot 只能添加在 上 (只有一种例外情况,独占默认插槽的缩写语法下面会讲到的),这一点和已经废弃的 slot attribute 不同。
案例:
src/App.vue:
<template>
<div class="container">
<Category title="游戏" >
<ul slot="center">
<li v-for="(g,index) in games" :key="index">{{g}}</li>
</ul>
<div class="foot" slot="footer">
<div>单机游戏</div>
</div>
</Category>
</div>
</template>
<script>
import Category from './components/Category'
export default {
name:'App',
components:{Category},
data() {
return {
games:['植物大战僵尸','红色警戒','空洞骑士','王国']
}
},
}
</script>
<style>
.container,.foot{
display: flex;
justify-content: space-around;
}
</style>
src/components/Category.vue:
<template>
<div class="category">
<h3>{{title}}分类</h3>
<!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
<slot name="center">我是一些默认值,当使用者没有传递具体结构时,我会出现1</slot>
<slot name="footer">我是一些默认值,当使用者没有传递具体结构时,我会出现2</slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title']
}
</script>
<style scoped>
.category{
background-color: skyblue;
width: 200px;
height: 300px;
}
h3{
text-align: center;
background-color: orange;
}
</style>
作用域插槽
有时让插槽内容能够访问子组件中才有的数据是很有用的。
例如,设想一个带有如下模板的 <current-user> 组件:
<span>
<slot>{{ user.lastName }}</slot>
</span>
我们可能想换掉备用内容,用名而非姓来显示。如下:
<current-user>
{{ user.firstName }}
</current-user>
然而上述代码不会正常工作,因为只有 组件可以访问到 user,而我们提供的内容是在父级渲染的。
为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 元素的一个 attribute 绑定上去:
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
绑定在 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps,但你也可以使用任意你喜欢的名字。
案例:
<template>
<div id="app">
<hello-world :value="value">
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</hello-world>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App', //父组件
components: {
HelloWorld,
},
data(){
return{
value:'电影'
}
},
methods:{
}
}
</script>
<style>
</style>
<template>
<div class="hello">
{{ value }}
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: ['value'],
data(){
return {
user:{
lastName:'lastName',
firstName:'firstName'
}
}
},
methods:{
}
}
</script>
<style scoped>
</style>
独占默认插槽的缩写语法
在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。
这样我们就可以把 v-slot 直接用在组件上:
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>
这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:
<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</current-user>
只要出现多个插槽,请始终为所有的插槽使用完整的基于 的语法:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</current-user>
解构插槽 Prop
作用域插槽的内部工作原理是将你的插槽内容包裹在一个拥有单个参数的函数里:
function (slotProps) {
// 插槽内容
}
这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。所以在支持的环境下 (单文件组件或现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 user 重命名为 person:
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>
案例:
<template>
<div id="app">
<hello-world :value="value" v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
<!-- 结果:电影 Guest -->
</hello-world>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App', //父组件
components: {
HelloWorld,
},
data(){
return{
value:'电影'
}
},
methods:{
}
}
</script>
<style>
</style>
<template>
<div class="hello">
{{ value }}
<!-- 子组件不传 -->
<!-- v-bind:user="user" -->
<slot>
{{ user.lastName }}
</slot>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: ['value'],
data(){
return {
user:{
lastName:'lastName',
firstName:'firstName'
}
}
},
methods:{
}
}
</script>
<style scoped>
</style>
动态插槽名
动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
具名插槽的缩写
跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。
例如 v-slot:header 可以被重写为 #header:
<base-layout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</template>
</base-layout>
然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:
<!-- 这样会触发一个警告 -->
<current-user #="{ user }">
{{ user.firstName }}
</current-user>
如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:
<current-user #default="{ user }">
{{ user.firstName }}
</current-user>
其它示例
插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。
这在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件时是最有用的。
例如,我们要实现一个 组件,它是一个列表且包含布局和过滤逻辑:
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
{{ todo.text }}
</li>
</ul>
我们可以将每个 todo 作为父级组件的插槽,以此通过父级组件对其进行控制,然后将 todo 作为一个插槽 prop 进行绑定:
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
<!--
我们为每个 todo 准备了一个插槽,
将 `todo` 对象作为一个插槽的 prop 传入。
-->
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
现在当我们使用 组件的时候,我们可以选择为 todo 定义一个不一样的 作为替代方案,并且可以从子组件获取数据:
<todo-list v-bind:todos="todos">
<template v-slot:todo="{ todo }">
<span v-if="todo.isComplete">✓</span>
{{ todo.text }}
</template>
</todo-list>
废弃了的语法
v-slot 指令自 Vue 2.6.0 起被引入,提供更好的支持 slot 和 slot-scope attribute 的 API
替代方案。v-slot 完整的由来参见这份 RFC。
在接下来所有的 2.x 版本中 slot 和 slot-scope attribute 仍会被支持,但已经被官方废弃且
不会出现在 Vue 3 中。
带有 slot attribute 的具名插槽
在 <template> 上使用特殊的 slot attribute,可以将内容从父级传给具名插槽 (把这里提到过
的 <base-layout> 组件作为示例):
<base-layout>
<template slot="header">
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template slot="footer">
<p>Here's some contact info</p>
</template>
</base-layout>
或者直接把 slot attribute 用在一个普通元素上:
<base-layout>
<h1 slot="header">Here might be a page title</h1>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<p slot="footer">Here's some contact info</p>
</base-layout>
这里其实还有一个未命名插槽,也就是默认插槽,捕获所有未被匹配的内容。上述两个示例的 HTML 渲染结果均为:
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
带有 slot-scope attribute 的作用域插槽
在 <template> 上使用特殊的 slot-scope attribute,可以接收传递给插槽的 prop (把这里提
到过的 <slot-example> 组件作为示例):
<slot-example>
<template slot="default" slot-scope="slotProps">
{{ slotProps.msg }}
</template>
</slot-example>
这里的 slot-scope 声明了被接收的 prop 对象会作为 slotProps 变量存在于 作用域中。你可以像命名 JavaScript 函数参数一样随意命名 slotProps。
这里的 slot=“default” 可以被忽略为隐性写法:
<slot-example>
<template slot-scope="slotProps">
{{ slotProps.msg }}
</template>
</slot-example>
slot-scope attribute 也可以直接用于非 元素 (包括组件):
<slot-example>
<span slot-scope="slotProps">
{{ slotProps.msg }}
</span>
</slot-example>
slot-scope 的值可以接收任何有效的可以出现在函数定义的参数位置上的 JavaScript 表达式。这意味着在支持的环境下 (单文件组件或现代浏览器),你也可以在表达式中使用 ES2015 解构,如下:
<slot-example>
<span slot-scope="{ msg }">
{{ msg }}
</span>
</slot-example>
使用这里描述过的 作为示例,与它等价的使用 slot-scope 的代码是:
<todo-list v-bind:todos="todos">
<template slot="todo" slot-scope="{ todo }">
<span v-if="todo.isComplete">✓</span>
{{ todo.text }}
</template>
</todo-list>
十五、动态组件 & 异步组件
在动态组件上使用 keep-alive
我们之前在一个多标签的界面中使用 is attribute 来切换不同的组件:
<component v-bind:is="currentTabComponent"></component>
案例:
<div id="dynamic-component-demo">
<button
v-for="tab in tabs"
v-bind:key="tab"
v-bind:class="['tab-button', { active: currentTab === tab }]"
v-on:click="currentTab = tab"
>
{{ tab }}
</button>
<component v-bind:is="currentTabComponent" class="tab"></component>
</div>
Vue.component("tab-home", {
template: "<div>Home component</div>"
});
Vue.component("tab-posts", {
template: "<div>Posts component</div>"
});
Vue.component("tab-archive", {
template: "<div>Archive component</div>"
});
new Vue({
el: "#dynamic-component-demo",
data: {
currentTab: "Home",
tabs: ["Home", "Posts", "Archive"]
},
computed: {
currentTabComponent: function () {
return "tab-" + this.currentTab.toLowerCase();
}
}
});
.tab {
border: 1px solid #ccc;
padding: 10px;
}
.tab-button {
padding: 6px 10px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #ccc;
cursor: pointer;
background: #f0f0f0;
margin-bottom: -1px;
margin-right: -1px;
}
.tab-button:hover {
background: #e0e0e0;
}
.tab-button.active {
background: #e0e0e0;
}
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重新渲染导致的性能问题。例如我们来展开说一说这个多标签界面:
你会注意到,如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent 实例。
重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 元素将其动态组件包裹起来。
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
现在这个 Posts 标签保持了它的状态 (被选中的文章) 甚至当它未被渲染时也是如此。
注意这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。
异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载
一个模块。
为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。
Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
例如:
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
如你所见,这个工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。这里的 setTimeout 是为了演示用的,如何获取组件取决于你自己。一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})
你也可以在工厂函数中返回一个 Promise,所以把 webpack 2 和 ES2015 语法加在一起,我们可以这样使用动态导入:
Vue.component(
'async-webpack-example',
// 这个动态导入会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})
如果你是一个 Browserify 用户同时喜欢使用异步组件,很不幸这个工具的作者明确表示异步加载“并不会被 Browserify 支持”,至少官方不会。Browserify 社区已经找到了一些变通方案,这些方案可能会对已存在的复杂应用有帮助。对于其它的场景,我们推荐直接使用 webpack,以拥有内置的头等异步支持。
处理加载状态
这里的异步组件工厂函数也可以返回一个如下格式的对象:
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
十六、混入
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
一个混入对象可以包含任意组件选项。
当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
例子:
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
局部使用混入案例:
App.vue
<template>
<div id="app"></div>
</template>
<script>
//引入混入
import { mixin } from './mixin'
export default {
name: 'App', //父组件
data(){
return{
name:'末晨曦吖',
}
},
mixins:[mixin],
mounted() {
this.showName() //这里调用的是混入总的方法
},
}
</script>
<style>
</style>
src/mixin.js:
export const mixin = {
methods: {
showName() {
alert(this.name)
}
},
mounted() {
console.log("我是混入的,你好呀~")
}
}
选项合并
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
var mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
}
}
new Vue({
mixins: [mixin],
data: function () {
return {
message: 'goodbye',
bar: 'def'
}
},
created: function () {
console.log(this.$data)
// => { message: "goodbye", foo: "abc", bar: "def" }
}
})
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
var mixin = {
created: function () {
console.log('混入对象的钩子被调用')
}
}
new Vue({
mixins: [mixin],
created: function () {
console.log('组件钩子被调用')
}
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"
值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
var mixin = {
methods: {
foo: function () {
console.log('foo')
},
conflicting: function () {
console.log('from mixin')
}
}
}
var vm = new Vue({
mixins: [mixin],
methods: {
bar: function () {
console.log('bar')
},
conflicting: function () {
console.log('from self')
}
}
})
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
注意:Vue.extend() 也使用同样的策略进行合并。
全局混入
混入也可以进行全局注册。使用时格外小心!
一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。
使用恰当时,这可以用来为自定义选项注入处理逻辑。
// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
})
// => "hello!"
请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件发布,以避免重复应用混入。
全局使用混入案例:
main.js
import Vue from 'vue'
import App from './App.vue'
import {mixin} from './mixin' //全局引入混入
Vue.config.productionTip = false
Vue.mixin(mixin)
new Vue({
render: h => h(App),
}).$mount('#app')
App.vue
<template>
<div id="app"></div>
</template>
<script>
//我们在App组件中并没引入混入,但却可以使用
//但我们却可以使用
export default {
name: 'App', //组件
data(){
return{
value:'',
name:'末晨曦吖',
}
},
mounted() {
this.showName() //这里调用的是混入总的方法
},
}
</script>
<style>
</style>
自定义选项合并策略
自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,
可以向 Vue.config.optionMergeStrategies 添加一个函数:
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合并后的值
}
对于多数值为对象的选项,可以使用与 methods 相同的合并策略:
var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.methods
可以在 Vuex 1.x 的混入策略里找到一个更高级的例子:
const merge = Vue.config.optionMergeStrategies.computed
Vue.config.optionMergeStrategies.vuex = function (toVal, fromVal) {
if (!toVal) return fromVal
if (!fromVal) return toVal
return {
getters: merge(toVal.getters, fromVal.getters),
state: merge(toVal.state, fromVal.state),
actions: merge(toVal.actions, fromVal.actions)
}
}
plugin插件
src/plugin.js:
export default {
install(Vue,x,y,z){
console.log(x,y,z)
//全局过滤器
Vue.filter('mySlice',function(value){
return value.slice(0,4)
})
//定义混入
Vue.mixin({
data() {
return {
x:100,
y:200
}
},
})
//给Vue原型上添加一个方法(vm和vc就都能用了)
Vue.prototype.hello = ()=>{alert('你好啊')}
}
}
src/main.js:
import Vue from 'vue'
import App from './App.vue'
import plugin from './plugin'
Vue.config.productionTip = false
Vue.use(plugin,1,2,3)
new Vue({
el:"#app",
render: h => h(App)
})
<template>
<div>
<h2>姓名:{{name | mySlice}}</h2>
<h2>地址:{{address}}</h2>
</div>
</template>
<script>
export default {
name:'School',
data() {
return {
name:'末晨曦',
address:'上海'
}
}
}
</script>
总结插件:
功能:用于增强Vue
本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用
者传递的数据
使用插件:Vue.use(plugin)
定义插件:
plugin.install = function (Vue, options) {
// 1. 添加全局过滤器
Vue.filter(....)
// 2. 添加全局指令
Vue.directive(....)
// 3. 配置全局混入
Vue.mixin(....)
// 4. 添加实例方法
Vue.prototype.$myMethod = function () {...}
Vue.prototype.$myProperty = xxxx
}
十七、自定义指令
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。
注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,
你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
当页面加载时,该元素将获得焦点 (注意:autofocus 在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。现在让我们用指令来实现这个功能:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
然后你可以在模板中任何元素上使用新的 v-focus property,如下:
<input v-focus>
实例案例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>自定义指令</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<!--
需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。
需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。
-->
<body>
<div id="root">
<h2>当前的n值是:<span v-text="n"></span> </h2>
<h2>放大10倍后的n值是:<span v-big="n"></span> </h2>
<button @click="n++">点我n+1</button>
<hr/>
<input type="text" v-fbind:value="n">
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
n:1
},
directives:{
//big函数何时会被调用?1.指令与元素成功绑定时(一上来) 2.指令所在的模板被重新解析时
big(element,binding){
console.log('big',this) //注意此处的this是window
element.innerText = binding.value * 10
},
fbind:{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
}
}
})
</script>
</html>
小结:
自定义指令定义语法:
1、局部指令:
new Vue({
directives:{指令名:配置对象}
})
2、局部指令:
new Vue({
directives:{指令名:回调函数}
})
3、全局指令:
Vue.directive(指令名,配置对象)
Vue.directive(指令名,回调函数)
例如:
Vue.directive('fbind',{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
})
钩子函数
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
接下来我们来看一下钩子函数的参数 (即 el、binding、vnode 和 oldVnode)。
钩子函数参数
指令钩子函数会被传入以下参数:
el:指令所绑定的元素,可以用来直接操作 DOM。
binding:一个对象,包含以下 property:
name:指令名,不包括 v- 前缀。
value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。
<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Vue.directive('demo', {
bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name: ' + s(binding.name) + '<br>' +
'value: ' + s(binding.value) + '<br>' +
'expression: ' + s(binding.expression) + '<br>' +
'argument: ' + s(binding.arg) + '<br>' +
'modifiers: ' + s(binding.modifiers) + '<br>' +
'vnode keys: ' + Object.keys(vnode).join(', ')
}
})
new Vue({
el: '#hook-arguments-example',
data: {
message: 'hello!'
}
})
动态指令参数
指令的参数可以是动态的。
例如,在 v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行
更新!这使得自定义指令可以在应用中被灵活使用。
例如你想要创建一个自定义指令,用来通过固定布局将元素固定在页面上。我们可以像这样创建一个通过指令值来更新竖直位置像素值的自定义指令:
<div id="baseexample">
<p>Scroll down the page</p>
<p v-pin="200">Stick me 200px from the top of the page</p>
</div>
Vue.directive('pin', {
bind: function (el, binding, vnode) {
el.style.position = 'fixed'
el.style.top = binding.value + 'px'
}
})
new Vue({
el: '#baseexample'
})
这会把该元素固定在距离页面顶部 200 像素的位置。但如果场景是我们需要把元素固定在左侧而不是顶部又该怎么办呢?这时使用动态参数就可以非常方便地根据每个组件实例来进行更新。
<div id="dynamicexample">
<h3>Scroll down inside this section ↓</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
Vue.directive('pin', {
bind: function (el, binding, vnode) {
el.style.position = 'fixed'
var s = (binding.arg == 'left' ? 'left' : 'top')
el.style[s] = binding.value + 'px'
}
})
new Vue({
el: '#dynamicexample',
data: function () {
return {
direction: 'left'
}
}
})
函数简写
在很多时候,你可能想在 bind 和 update 时触发相同行为,而不关心其它的钩子。比如这样写:
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
对象字面量
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
十八、渲染函数 & JSX
基础
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,
你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
让我们深入一个简单的例子,这个例子里 render 函数很实用。假设我们要生成一些带锚点的标题:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
对于上面的 HTML,你决定这样定义组件接口:
<anchored-heading :level="1">Hello world!</anchored-heading>
当开始写一个只能通过 level prop 动态生成标题 (heading) 的组件时,你可能很快想到这样实现:
<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
</script>
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了 ,在要插入锚点元素时还要再次重复。
虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render 函数重写上面的例子:
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
看起来简单多了!这样代码精简很多,但是需要非常熟悉 Vue 的实例 property。在这个例子中,你需要知道,向组件中传递不带 v-slot 指令的子节点时,比如 anchored-heading 中的 Hello world!,这些子节点被存储在组件实例中的 $slots.default 中。
节点、树以及虚拟 DOM
在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML 为例:
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>
当浏览器读到这些代码时,它会建立一个“DOM 节点”树来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。
上述 HTML 对应的 DOM 节点树如下图所示:
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
高效地更新所有这些节点会是比较困难的,不过所幸你不必手动完成这个工作。你只需要告诉 Vue 你希望页面上的 HTML 是什么,这可以是在一个模板里:
<h1>{{ blogTitle }}</h1>
或者一个渲染函数里:
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle 发生了改变。
虚拟 DOM
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:
return createElement('h1', this.blogTitle)
createElement 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
createElement 参数
接下来你需要熟悉的是如何在 createElement 函数中使用模板中的那些功能。
这里是 createElement 接受的参数:
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
深入数据对象
{
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML attribute
attrs: {
id: 'foo'
},
// 组件 prop
props: {
myProp: 'bar'
},
// DOM property
domProps: {
innerHTML: 'baz'
},
// 事件监听器在 `on` 内,
// 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
}
完整示例
有了这些知识,我们现在可以完成我们最开始想实现的组件:
var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
Vue.component('anchored-heading', {
render: function (createElement) {
// 创建 kebab-case 风格的 ID
var headingId = getChildrenTextContent(this.$slots.default)
.toLowerCase()
.replace(/\W+/g, '-')
.replace(/(^-|-$)/g, '')
return createElement(
'h' + this.level,
[
createElement('a', {
attrs: {
name: headingId,
href: '#' + headingId
}
}, this.$slots.default)
]
)
},
props: {
level: {
type: Number,
required: true
}
}
})
约束
VNode 必须唯一
组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 错误 - 重复的 VNode
myParagraphVNode, myParagraphVNode
])
}
如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
使用 JavaScript 代替模板功能
v-if 和 v-for
只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-if 和 v-for:
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
这些都可以在渲染函数中用 JavaScript 的 if/else 和 map 来重写:
props: ['items'],
render: function (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map(function (item) {
return createElement('li', item.name)
}))
} else {
return createElement('p', 'No items found.')
}
}
v-model
渲染函数中没有与 v-model 的直接对应——你必须自己实现相应的逻辑:
props: ['value'],
render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.$emit('input', event.target.value)
}
}
})
}
这就是深入底层的代价,但与 v-model 相比,这可以让你更好地控制交互细节。
事件 & 按键修饰符
对于 .passive、.capture 和 .once 这些事件修饰符,Vue 提供了相应的前缀可以用于 on:
修饰符 前缀
.passive &
.capture !
.once ~
.capture.once 或 .once.capture ~!
例如:
on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
这里是一个使用所有修饰符的例子:
on: {
keyup: function (event) {
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 如果按下去的不是 enter 键或者
// 没有同时按下 shift 键
// 则返回
if (!event.shiftKey || event.keyCode !== 13) return
// 阻止 事件冒泡
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
}
插槽
你可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:
render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:
props: ['message'],
render: function (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 字段:
render: function (createElement) {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return createElement('div', [
createElement('child', {
// 在数据对象中传递 `scopedSlots`
// 格式为 { name: props => VNode | Array<VNode> }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
JSX
如果你写了很多 render 函数,可能会觉得下面这样的代码写起来很痛苦:
createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
特别是对应的模板如此简单的情况下:
<anchored-heading :level="1">
<span>Hello</span> world!
</anchored-heading>
这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
将 h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。从 Vue 的 Babel 插件的 3.4.0 版本开始,我们会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入 const h = this.$createElement,这样你就可以去掉 (h) 参数了。对于更早版本的插件,如果 h 在当前作用域中不可用,应用会抛错。
函数式组件
之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。一个函数式组件就像这样:
Vue.component('my-component', {
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
// ...
}
})
注意:在 2.3.0 之前的版本中,如果一个函数式组件想要接收 prop,则 props 选项是必须的。在 2.3.0 或以上的版本中,你可以省略 props 选项,所有组件上的 attribute 都会被自动隐式解析为 prop。
当使用函数式组件时,该引用将会是 HTMLElement,因为他们是无状态的也是无实例的。
在 2.5.0 及以上版本中,如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:
<template functional>
</template>
组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:
- props:提供所有 prop 的对象
- children:VNode 子节点的数组
- slots:一个函数,返回了包含所有插槽的对象
- scopedSlots:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
- data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
- parent:对父组件的引用
- listeners:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
- injections:(2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的 property。
在添加 functional: true 之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context 参数,并将 this.$slots.default 更新为 context.children,然后将 this.level 更新为 context.props.level。
因为函数式组件只是函数,所以渲染开销也低很多。
在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:
- 程序化地在多个组件中选择一个来代为渲染;
- 在将 children、props、data 传递给子组件之前操作它们。
下面是一个 smart-list 组件的例子,它能根据传入 prop 的值来代为渲染更具体的组件:
var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
},
render: function (createElement, context) {
function appropriateListComponent () {
var items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
}
})
向子元素或子组件传递 attribute 和事件
在普通组件中,没有被定义为 prop 的 attribute 会自动添加到组件的根元素上,将已有的同名 attribute 进行替换或与其进行智能合并。
然而函数式组件要求你显式定义该行为:
Vue.component('my-functional-button', {
functional: true,
render: function (createElement, context) {
// 完全透传任何 attribute、事件监听器、子节点等。
return createElement('button', context.data, context.children)
}
})
通过向 createElement 传入 context.data 作为第二个参数,我们就把 my-functional-button 上面所有的 attribute 和事件监听器都传递下去了。事实上这是非常透明的,以至于那些事件甚至并不要求 .native 修饰符。
如果你使用基于模板的函数式组件,那么你还需要手动添加 attribute 和监听器。因为我们可以访问到其独立的上下文内容,所以我们可以使用 data.attrs 传递任何 HTML attribute,也可以使用 listeners (即 data.on 的别名) 传递任何事件监听器。
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners"
>
<slot/>
</button>
</template>
slots() 和 children 对比
你可能想知道为什么同时需要 slots() 和 children。slots().default 不是和 children 类似的吗?在一些场景中,是这样——但如果是如下的带有子节点的函数式组件呢?
<my-functional-component>
<p v-slot:foo>
first
</p>
<p>second</p>
</my-functional-component>
对于这个组件,children 会给你两个段落标签,而 slots().default 只会传递第二个匿名段落标签,slots().foo 会传递第一个具名段落标签。同时拥有 children 和 slots(),因此你可以选择让组件感知某个插槽机制,还是简单地通过传递 children,移交给其它组件去处理。
模板编译
你可能会有兴趣知道,Vue 的模板实际上被编译成了渲染函数。这是一个实现细节,通常不需要关心。但如果你想看看模板的功能具体是怎样被编译的,可能会发现会非常有意思。下面是一个使用 Vue.compile 来实时编译模板字符串的简单示例:
<div>
<header>
<h1>I'm a template!</h1>
</header>
<p v-if="message">{{ message }}</p>
<p v-else>No message.</p>
</div>
render:
function anonymous(
) {
with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])}
}
staticRenderFns:
_m(0): function anonymous(
) {
with(this){return _c('header',[_c('h1',[_v("I'm a template!")])])}
}
十九、插件
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
添加全局方法或者 property。如:vue-custom-element
添加全局资源:指令/过滤器/过渡等。如 vue-touch
通过全局混入来添加一些组件选项。如 vue-router
添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
使用插件
通过全局方法 Vue.use() 使用插件。它需要在你调用 new Vue() 启动应用之前完成:
// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
// ...组件选项
})
也可以传入一个可选的选项对象:
Vue.use(MyPlugin, { someOption: true })
Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。
Vue.js 官方提供的一些插件 (例如 vue-router) 在检测到 Vue 是可访问的全局变量时会自动调用 Vue.use()。然而在像 CommonJS 这样的模块环境中,你应该始终显式地调用 Vue.use():
// 用 Browserify 或 webpack 提供的 CommonJS 模块环境时
var Vue = require('vue')
var VueRouter = require('vue-router')
// 不要忘了调用此方法
Vue.use(VueRouter)
awesome-vue 集合了大量由社区贡献的插件和库。
开发插件
Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
二十、过滤器
Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
你可以在一个组件的选项中定义本地的过滤器:
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
或者在创建 Vue 实例之前全局定义过滤器:
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
new Vue({
// ...
})
当全局过滤器和局部过滤器重名时,会采用局部过滤器。
过滤器可以串联:
{{ message | filterA | filterB }}
在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。
过滤器是 JavaScript 函数,因此可以接收参数:
{{ message | filterA('arg1', arg2) }}
这里,filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 ‘arg1’ 作为第二个参数,表达式 arg2 的值作为第三个参数。
局部使用案例:
<template>
<h3>{{str | strChange}}</h3>
</template>
<script>
export default {
data (){
return {
str:'hello world'
};
},
filters:{
// toUpperCase()方法将英文字母转换为大写
strChange(value) {
console.log('value值',value)
return value.toUpperCase()
}
}
}
</script>
全局使用案例:
第一种直接在main.js中建filter
Vue.filter("upperCase", function(value) {
if (!value) return "";
value = value.toString();
return value.toUpperCase(); // 小写转为大写
});
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
第二种在外新建一个文件夹
1.新建filter文件夹,添加filter.js文件:
2.添加到Vue全局中,在main.js中引入,并且添加:
3.在组件中就可以直接使用了:
<template>
<div>
<h3>{{ message | myfilter}}</h3>
输入:<input type="text" v-bind:value='message | myfilter'>
</div>
</template>
<script>
export default {
data (){
return {message:'hello world'};
},
}
</script>
二十一、Vuex
概念:专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 vue 应用中多个组件的
共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信
Vuex的使用场景
多个组件依赖于同一状态
来自不同组件的行为需要变更同一状态
Vuex工作原理图
安装VueX
npm安装
npm install vue vuex --save
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
cdn安装
<script src="https://cdn.bootcdn.net/ajax/libs/vuex/3.5.1/vuex.min.js"></script>
开始store
每一个 Vuex 应用的核心就是 store(仓库)。
“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
Vuex 和单纯的全局对象有以下两点不同:
1.Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态
发生变化,那么相应的组件也会相应地得到高效更新。
2.你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit)
mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮
助我们更好地了解我们的应用。
最简单的 Store
安装 Vuex 之后,让我们来创建一个 store。创建过程直截了当,仅需要提供一个初始 state 对
象和一些 mutation:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
现在,你可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:
store.commit('increment')
console.log(store.state.count) // -> 1
为了在 Vue 组件中访问 this.$store property,你需要为 Vue 实例提供创建好的 store。
Vuex 提供了一个从根组件向所有子组件,以 store 选项的方式“注入”该 store 的机制:
import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'
import store from './store'
Vue.config.productionTip = false
Vue.use(Vuex)
new Vue({
el:"#app",
render: h => h(App),
store
})
现在我们可以从组件的方法提交一个变更:
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
}
}
再次强调,我们通过提交 mutation 的方式,而非直接改变 store.state.count,是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的 methods 中提交 mutation。
完整例子:
main.js
import Vue from 'vue'
import App from './App.vue'
import {mixin} from './mixin' //全局引入混入
import store from './store'
Vue.config.productionTip = false
Vue.mixin(mixin)
new Vue({
store,
render: h => h(App),
}).$mount('#app')
src>store>index.js
//引入Vue核心库
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
Vue.use(Vuex)
//准备actions对象——响应组件中用户的动作、处理业务逻辑,可进行异步操作
const actions = {}
//准备mutations对象——修改state中的数据
const mutations = {}
//准备state对象——保存具体的数据
const state = {
name:'末晨曦吖' //定义一条数据
}
//创建并暴露store
export default new Vuex.Store({
actions,
mutations,
state
})
App.vue
<template>
<div id="app">
{{ $store.state.name }}
</div>
</template>
<script>
export default {
name: 'App',
data(){
return{
}
},
mounted() {
},
}
</script>
<style>
</style>
效果:
核心对象
State
Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态,至此它便作为一个“唯一数据源
”而存在。
这也意味着,每个应用将仅仅包含一个 store 实例。
单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前
应用状态的快照。
单一状态树和模块化并不冲突,在后面的章节里我们会讨论如何将状态和状态变更事件分布到各个
子模块中。
存储在 Vuex 中的数据和 Vue 实例中的 data 遵循相同的规则,例如状态对象必须是纯粹
(plain) 的。
在 Vue 组件中获得 Vuex 状态
那么我们如何在 Vue 组件中展示状态呢?
由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
每当 store.state.count 变化的时候,都会重新求取计算属性,并且触发更新相关联的 DOM。
然而,这种模式导致组件依赖全局状态单例。
在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。
Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)):
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。
让我们更新下 Counter 的实现:
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
mapState 辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。
为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:
<template>
<div id="app">
一般写法: {{ $store.state.name }}
<br>
mapState箭头函数写法: {{ name }}
<br>
mapState传字符串参数写法: {{ countAlias }}
</div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
name: 'App',
data(){
return{
}
},
computed: mapState({
// 箭头函数可使代码更简练
name: state => state.name,
// 传字符串参数 'name' 等同于 `state => state.name`
countAlias: 'name',
}),
mounted() {
},
}
</script>
<style>
</style>
效果图:
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
<template>
<div id="app">
一般写法: {{ $store.state.name }}
<br>
mapState箭头函数写法: {{ name }}
</div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
name: 'App',
data(){
return{
}
},
computed: mapState([
// 映射 this.name 为 store.state.name
'name'
]),
mounted() {
},
}
</script>
<style>
</style>
对象展开运算符
mapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?
通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed
属性。
但是自从有了对象展开运算符,我们可以极大地简化写法:
<template>
<div id="app">
一般写法: {{ $store.state.name }}
<br>
mapState箭头函数写法: {{ name }}
<br>
{{ localComputed }}
</div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
name: 'App',
data(){
return{
}
},
computed:{
localComputed () {
return 'localComputed'
},
// 使用对象展开运算符
...mapState({
// 箭头函数可使代码更简练
name: state => state.name,
// 传字符串参数 'name' 等同于 `state => state.name`
countAlias: 'name',
})
},
mounted() {
},
}
</script>
<style>
</style>
效果图:
组件仍然保有局部状态
使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。
Getter
有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它,无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter 接受 state 作为其第一个参数:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
通过属性访问
Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getter 作为第二个参数:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
我们可以很容易地在任何组件中使用它:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
完整例子:
store>index.js
//引入Vue核心库
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
Vue.use(Vuex)
//准备actions对象——响应组件中用户的动作、处理业务逻辑,可进行异步操作
const actions = {}
//准备mutations对象——修改state中的数据
const mutations = {}
//getters
const getters = {
name : state => state.name
}
//准备state对象——保存具体的数据
const state = {
name:'末晨曦吖' //定义一条数据
}
//创建并暴露store
export default new Vuex.Store({
actions,
mutations,
getters,
state,
})
<template>
<div id="app">
一般写法: {{ $store.state.name }}
<br>
mapState箭头函数写法: {{ name }}
<br>
{{ localComputed }}
<br>
getters: {{ $store.getters.name }}
</div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
name: 'App',
data(){
return{
}
},
computed:{
localComputed () {
return 'localComputed'
},
// 使用对象展开运算符
...mapState({
// 箭头函数可使代码更简练
name: state => state.name,
// 传字符串参数 'name' 等同于 `state => state.name`
countAlias: 'name',
})
},
mounted() {
},
}
</script>
<style>
</style>
mapGetters 辅助函数
mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想将一个 getter 属性另取一个名字,使用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
完整例子
<template>
<div id="app">
getters: {{ $store.getters.name }}
<br>
name: {{ name }}
</div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapGetters
import { mapGetters } from 'vuex'
export default {
name: 'App',
data(){
return{
}
},
computed:{
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'name',
])
},
mounted() {
},
}
</script>
<style>
</style>
Mutations
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和
一个 回调函数 (handler)。
这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
你不能直接调用一个 mutation handler。
这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”
要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:
store.commit('increment')
提交载荷(Payload)
你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload):
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type 属性的对象
store.commit({
type: 'increment',
amount: 10
})
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
Mutation 需遵守 Vue 的响应规则
既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会
自动更新。
这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:
最好提前在你的 store 中初始化好所有所需属性。
当需要在对象上添加新属性时,你应该使用 Vue.set(obj, 'newProp', 123), 或者
以新对象替换老对象。
例如,利用对象展开运算符 我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }
使用常量替代 Mutation 事件类型
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。
这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作
者对整个 app 包含的 mutation 一目了然:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
用不用常量取决于你,在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。
Mutation 必须是同步函数
一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用,实质上任何在回调函数中进行的状态的改变都是不可追踪的。
在组件中提交 Mutation
你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations
辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务:
store.commit('increment')
// 任何由 "increment" 导致的状态变更都应该在此刻完成。
完整例子:
//引入Vue核心库
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
Vue.use(Vuex)
//准备actions对象——响应组件中用户的动作、处理业务逻辑,可进行异步操作
const actions = {}
//准备mutations对象——修改state中的数据
const mutations = {
changeName(state,name){
state.name = name
}
}
//getters
const getters = {
name : state => state.name
}
//准备state对象——保存具体的数据
const state = {
name:'末晨曦吖' //定义一条数据
}
//创建并暴露store
export default new Vuex.Store({
actions,
mutations,
getters,
state,
})
<template>
<div id="app">
getters: {{ $store.getters.name }}
<br>
name: {{ name }}
<br>
<button @click="changeName">改变name</button>
</div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapGetters
import { mapGetters } from 'vuex'
export default {
name: 'App',
data(){
return{
}
},
computed:{
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'name',
])
},
mounted() {
},
methods:{
changeName(){
this.$store.commit('changeName','我是改变后的name')
}
}
}
</script>
<style>
</style>
为了处理异步操作,让我们来看一看 Action。
Actions
Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。
让我们来注册一个简单的 action:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。
实践中,我们会经常用到 ES2015 的 参数解构 来简化代码(特别是我们需要调用 commit 很多次的时候):
actions: {
increment ({ commit }) {
commit('increment')
}
}
分发 Action
Action 通过 store.dispatch 方法触发:
store.dispatch('increment')
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?
实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Actions 支持同样的载荷方式和对象方式进行分发:
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
在组件中分发 Action
你在组件中使用 this.$store.dispatch(‘xxx’) 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
组合 Action
Action 通常是异步的,那么如何知道 action 什么时候结束呢?
更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?
首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,
并且 store.dispatch 仍旧返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
store.dispatch('actionA').then(() => {
// ...
})
在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
最后,如果我们利用 async / await,我们可以如下组合 action:
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
一个 store.dispatch 在不同模块中可以触发多个 action 函数。
在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
模块的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
命名空间
默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:
const store = createStore({
modules: {
account: {
namespaced: true,
// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
启用了命名空间的 getter 和 action 会收到局部化的 getter,dispatch 和 commit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。
在带命名空间的模块内访问全局内容(Global Assets)
如果你希望使用全局 state 和 getter,rootState 和 rootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。
若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。
modules: {
foo: {
namespaced: true,
getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
rootGetters['bar/someOtherGetter'] // -> 'bar/someOtherGetter'
},
someOtherGetter: state => { ... }
},
actions: {
// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
rootGetters['bar/someGetter'] // -> 'bar/someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}
在带命名空间的模块注册全局 action
若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
带命名空间的绑定函数
当使用 mapState、mapGetters、mapActions 和 mapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
}),
...mapGetters([
'some/nested/module/someGetter', // -> this['some/nested/module/someGetter']
'some/nested/module/someOtherGetter', // -> this['some/nested/module/someOtherGetter']
])
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
}),
...mapGetters('some/nested/module', [
'someGetter', // -> this.someGetter
'someOtherGetter', // -> this.someOtherGetter
])
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions([
'foo',
'bar'
])
}
}
给插件开发者的注意事项
如果你开发的插件(Plugin)提供了模块并允许用户将其添加到 Vuex store,可能需要考虑模块的空间名称问题。对于这种情况,你可以通过插件的参数对象来允许用户指定空间名称:
// 通过插件的参数对象得到空间名称
// 然后返回 Vuex 插件函数
export function createPlugin (options = {}) {
return function (store) {
// 把空间名字添加到插件模块的类型(type)中去
const namespace = options.namespace || ''
store.dispatch(namespace + 'pluginAction')
}
}
模块动态注册
在 store 创建之后,你可以使用 store.registerModule 方法注册模块:
import { createStore } from 'vuex'
const store = createStore({ /* 选项 */ })
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
之后就可以通过 store.state.myModule 和 store.state.nested.myModule 访问模块的状态。
模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如,vuex-router-sync 插件就是通过动态注册模块将 Vue Router 和 Vuex 结合在一起,实现应用的路由状态管理。
你也可以使用 store.unregisterModule(moduleName) 来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store 时声明的模块)。
注意,你可以通过 store.hasModule(moduleName) 方法检查该模块是否已经被注册到 store。需要记住的是,嵌套模块应该以数组形式传递给 registerModule 和 hasModule,而不是以路径字符串的形式传递给 module。
保留 state
在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过 preserveState 选项将其归档:store.registerModule(‘a’, module, { preserveState: true })。
当你设置 preserveState: true 时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是 state 不会。这里假设 store 的 state 已经包含了这个 module 的 state 并且你不希望将其覆写。
模块重用
有时我们可能需要创建一个模块的多个实例,例如:
创建多个 store,他们公用同一个模块 (例如当 runInNewContext 选项是 false 或 'once' 时,
为了在服务端渲染中避免有状态的单例)
在一个 store 中多次注册同一个模块
如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。
实际上这和 Vue 组件内的 data 是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅 2.3.0+ 支持):
const MyReusableModule = {
state: () => ({
foo: 'bar'
}),
// mutation、action 和 getter 等等...
}
二十二、Vue Router路由管理器
vue-router的理解
vue 的一个插件库,专门用来实现SPA 应用
对SPA应用的理解
单页 Web 应用(single page web application,SPA)
整个应用只有一个完整的页面
点击页面中的导航链接不会刷新页面,只会做页面的局部更新
数据需要通过ajax或axios请求获取
路由的理解
什么是路由?
一个路由就是一组映射关系(key - value)
key 为路径,value 可能是 function 或 component
路由分类
后端路由:
理解:value 是 function,用于处理客户端提交的请求
工作过程:服务器接收到一个请求时,根据请求路径找到匹配的函数来处理请求,返回响应数据
前端路由:
理解:value 是 component,用于展示页面内容
工作过程:当浏览器的路径改变时,对应的组件就会显示
安装方式
npm安装
npm install vue vue-router
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
cdn安装
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.4.8/vue-router.min.js"></script>
基本案例
src/router/index.js:
//该文件专门用于创建整个应用的路由器
import VueRouter from "vue-router";
//引入组件
import Home from '../components/Home'
import About from '../components/About'
//创建并暴露一个路由器
export default new VueRouter({
routes:[
{
path:'/about',
component:About
},
{
path:'/home',
component:Home
}
]
})
src/main.js:
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import router from './router'
Vue.config.productionTip = false
Vue.use(VueRouter)
new Vue({
el:"#app",
render: h => h(App),
router
})
src/App.vue:
<template>
<div>
<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">
<!-- 原始html中我们使用a标签实现页面跳转 -->
<!-- <a class="list-group-item active" href="./about.html">About</a>
<a class="list-group-item" href="./home.html">Home</a> -->
<!-- Vue中借助router-link标签实现路由的切换 -->
<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>
src/components/Home.vue:
<template>
<h2>我是Home组件的内容</h2>
</template>
<script>
export default {
name:'Home'
}
</script>
src/components/About.vue:
<template>
<h2>我是About组件的内容</h2>
</template>
<script>
export default {
name:'About'
}
</script>
总结:
1.安装vue-router,命令:npm i vue-router
2.应用插件:Vue.use(VueRouter)
3.编写router配置项:
//引入VueRouter
import VueRouter from 'vue-router'
//引入Luyou 组件
import About from '../components/About'
import Home from '../components/Home'
//创建router实例对象,去管理一组一组的路由规则
const router = new VueRouter({
routes:[
{
path:'/about',
component:About
},
{
path:'/home',
component:Home
}
]
})
//暴露router
export default router
4.实现切换(active-class可配置高亮样式):
<router-link active-class="active" to="/about">About</router-link>
5.指定展示位:
6.通过注入路由器,我们可以在任何组件内通过 this.
r
o
u
t
e
r
访问路由器,也可以通过
t
h
i
s
.
router 访问路由器,也可以通过 this.
router访问路由器,也可以通过this.route 访问当前路由。
该文档通篇都常使用 router 实例。留意一下 this.$router 和 router 使用起来完全一样。
我们使用 this.$router 的原因是我们并不想在每个独立需要封装路由的组件中都导入路由。
要注意,当 对应的路由匹配成功,将自动设置 class 属性值 .router-link-active。
动态路由匹配
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:
const User = {
template: '<div>User</div>'
}
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/user/:id', component: User }
]
})
在呢,像 /user/foo 和 /user/bar 都将映射到相同的路由。
一个“路径参数”使用冒号 : 标记。
当匹配到一个路由时,参数值会被设置到 this.$route.params,可以在每个组件内使用。
于是,我们可以更新 User 的模板,输出当前用户的 ID:
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
你可以在一个路由中设置多段“路径参数”,对应的值都会设置到 $route.params 中。例如:
除了
r
o
u
t
e
.
p
a
r
a
m
s
外,
route.params 外,
route.params外,route 对象还提供了其它有用的信息。
例如, r o u t e . q u e r y ( 如果 U R L 中有查询参数 ) 、 route.query (如果 URL 中有查询参数)、 route.query(如果URL中有查询参数)、route.hash 等等。
响应路由参数的变化
当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。
因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的
生命周期钩子不会再被调用。
复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route 对象
const User = {
template: '...',
watch: {
$route(to, from) {
// 对路由变化作出响应...
}
}
}
或者使用 beforeRouteUpdate 导航守卫:
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// react to route changes...
// don't forget to call next()
}
}
捕获所有路由或 404 Not found 路由
常规参数只会匹配被 / 分隔的 URL 片段中的字符。如果想匹配任意路径,我们可以使用通配符 (*):
{
// 会匹配所有路径
path: '*'
}
{
// 会匹配以 `/user-` 开头的任意路径
path: '/user-*'
}
当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。
路由 { path: ‘*’ } 通常用于客户端 404 错误。如果你使用了History 模式,请确保正确配置你的服务器。
当使用一个通配符时,$route.params 内会自动添加一个名为 pathMatch 参数。它包含了 URL 通过通配符被匹配的部分:
// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-admin')
this.$route.params.pathMatch // 'admin'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'
高级匹配模式
vue-router 使用 path-to-regexp 作为路径匹配引擎,所以支持很多高级的匹配模式,例如:可选的动态路径参数、匹配零个或多个、一个或多个,甚至是自定义正则匹配。查看它的文档学习高阶的路径匹配,还有这个例子 展示 vue-router 怎么使用这类匹配。
匹配优先级
有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:谁先定义的,谁的优先级就最高。
嵌套路由
实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件,例如:
/user/foo/profile /user/foo/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
借助 vue-router,使用嵌套路由配置,就可以很简单地表达这种关系。
接着上节创建的 app:
<div id="app">
<router-view></router-view>
</div>
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [{ path: '/user/:id', component: User }]
})
这里的 <router-view>
是最顶层的出口,渲染最高级路由匹配到的组件。
同样地,一个被渲染组件同样可以包含自己的嵌套 <router-view>
。
例如,在 User 组件的模板添加一个 <router-view>
:
const User = {
template: `
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`
}
要在嵌套的出口中渲染组件,需要在 VueRouter 的参数中使用 children 配置:
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
path: 'profile',
component: UserProfile
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 会被渲染在 User 的 <router-view> 中
path: 'posts',
component: UserPosts
}
]
}
]
})
要注意,以 / 开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。
你会发现,children 配置就是像 routes 配置一样的路由配置数组,所以呢,你可以嵌套多层路由。
此时,基于上面的配置,当你访问 /user/foo 时,User 的出口是不会渲染任何东西,这是因为没有匹配到合适的子路由。
如果你想要渲染点什么,可以提供一个空的子路由:
const router = new VueRouter({
routes: [
{
path: '/user/:id', component: User,
children: [
// 当 /user/:id 匹配成功,
// UserHome 会被渲染在 User 的 <router-view> 中
{ path: '', component: UserHome },
// ...其他子路由
]
}
]
})
案例:
src/pages/Home.vue:
<template>
<div>
<h2>Home组件内容</h2>
<div>
<ul class="nav nav-tabs">
<li>
<router-link class="list-group-item" active-class="active" to="/home/news">
News
</router-link>
</li>
<li>
<router-link class="list-group-item" active-class="active" to="/home/message">
Message
</router-link>
</li>
</ul>
<router-view></router-view>
</div>
</div>
</template>
<script>
export default {
name:'Home'
}
</script>
src/pages/News.vue:
<template>
<ul>
<li>news001</li>
<li>news002</li>
<li>news003</li>
</ul>
</template>
<script>
export default {
name:'News'
}
</script>
src/pages/Message.vue:
<template>
<ul>
<li>
<a href="/message1">message001</a>
</li>
<li>
<a href="/message2">message002</a>
</li>
<li>
<a href="/message/3">message003</a>
</li>
</ul>
</template>
<script>
export default {
name:'News'
}
</script>
src/router/index.js:
//该文件专门用于创建整个应用的路由器
import VueRouter from "vue-router";
//引入组件
import Home from '../pages/Home'
import About from '../pages/About'
import News from '../pages/News'
import Message from '../pages/Message'
//创建并暴露一个路由器
export default new VueRouter({
routes:[
{
path:'/about',
component:About
},
{
path:'/home',
component:Home,
children:[
{
path:'news',
component:News
},
{
path:'message',
component:Message
}
]
}
]
})
效果:
注意:跳转(要写完整路径)
<router-link to="/home/news">News</router-link>
编程式的导航
声明式导航 router-link
首先我们在看下声明式导航
<router-link>
组件支持用户在具有路由功能的应用中 (点击) 导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 标签,可以通过配置 tag 属性生成别的标签.。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。
好处:
无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动。
在 HTML5 history 模式下,router-link 会拦截点击事件,让浏览器不在重新加载页面。
当你在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写(基路径)了。
<router-link to="/login">登陆</router-link>
<router-link to="/register">注册</router-link>
<router-link>默认渲染为一个<a>标签
假如我们想把<router-link>渲染成其他标签,可修改其属性tag,如:
<router-link to="/login" tag=“span”>登陆</router-link>
<router-link to="/register">注册</router-link>
表示目标路由的链接。当被点击后,内部会立刻把 to 的值传到 router.push(),所以这个值可以是一个字符串或者是描述目标位置的对象
<!-- 字符串 -->
<router-link to="home">Home</router-link>
<!-- 渲染结果 -->
<a href="home">Home</a>
<!-- 使用 v-bind 的 JS 表达式 -->
<router-link v-bind:to="'home'">Home</router-link>
<!-- 不写 v-bind 也可以,就像绑定别的属性一样 -->
<router-link :to="'home'">Home</router-link>
<!-- 同上 -->
<router-link :to="{ path: 'home' }">Home</router-link>
<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
<!-- 带查询参数,下面的结果为 /register?plan=private -->
<router-link :to="{ path: 'register', query: { plan: 'private' }}">Register</router-link>
除了使用 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
router.push(location, onComplete?, onAbort?)
注意:在 Vue 实例内部,你可以通过 r o u t e r 访问路由实例。因此你可以调用 t h i s . router 访问路由实例。因此你可以调用 this. router访问路由实例。因此你可以调用this.router.push。
想要导航到不同的 URL,则使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
当你点击 时,这个方法会在内部调用,所以说,点击 <router-link :to="...">
等同于调用 router.push(…)
该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
params接收参数
$route.params.userId
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }}
query接收参数
$route.query.plan
注意:如果提供了 path,params 会被忽略,上述例子中的 query 并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name 或手写完整的带有参数的 path:
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
同样的规则也适用于 router-link 组件的 to 属性。
在 2.2.0+,可选的在 router.push 或 router.replace 中提供 onComplete 和 onAbort 回调作为第二个和第三个参数。这些回调将会在导航成功完成 (在所有的异步钩子被解析之后) 或终止 (导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由) 的时候进行相应的调用。
在 3.1.0+,可以省略第二个和第三个参数,此时如果支持 Promise,router.push 或 router.replace 将返回一个 Promise。
注意: 如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1 -> /users/2),你需要使用 beforeRouteUpdate 来响应这个变化 (比如抓取用户信息)。
router.replace(location, onComplete?, onAbort?)
跟 router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样,替换掉当前的 history 记录。
router.go(n)
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,
类似 window.history.go(n)。
例子
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
// 后退一步记录,等同于 history.back()
router.go(-1)
// 前进 3 步记录
router.go(3)
// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)
操作 History
你也许注意到 router.push、 router.replace 和 router.go 跟 window.history.pushState、 window.history.replaceState 和 window.history.go (opens new window)好像, 实际上它们确实是效仿 window.history API 的。
因此,如果你已经熟悉 Browser History APIs (opens new window),那么在 Vue Router 中操作 history 就是超级简单的。
还有值得提及的,Vue Router 的导航方法 (push、 replace、 go) 在各类路由模式 (history、 hash 和 abstract) 下表现一致。
命名路由
有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些
跳转的时候。你可以在创建 Router 实例的时候,在 routes 配置中给某个路由设置名称。
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
要链接到一个命名路由,可以给 router-link 的 to 属性传一个对象:
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
这跟代码调用 router.push() 是一回事:
router.push({ name: 'user', params: { userId: 123 } })
这两种方式都会把路由导航到 /user/123 路径。
命名视图
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置 (带上 s):
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
嵌套命名视图
我们也有可能使用命名视图创建嵌套视图的复杂布局。这时你也需要命名用到的嵌套 router-view 组件。我们以一个设置面板为例:
/settings/emails /settings/profile
+-----------------------------------+ +------------------------------+
| UserSettings | | UserSettings |
| +-----+-------------------------+ | | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | | +------------> | | Nav | UserProfile | |
| | +-------------------------+ | | | +--------------------+ |
| | | | | | | | UserProfilePreview | |
| +-----+-------------------------+ | | +-----+--------------------+ |
+-----------------------------------+ +------------------------------+
- Nav 只是一个常规组件。
- UserSettings 是一个视图组件。
- UserEmailsSubscriptions、UserProfile、UserProfilePreview 是嵌套的视图组件。
注意:我们先忘记 HTML/CSS 具体的布局的样子,只专注在用到的组件上。
UserSettings 组件的 部分应该是类似下面的这段代码:
<!-- UserSettings.vue -->
<div>
<h1>User Settings</h1>
<NavBar/>
<router-view/>
<router-view name="helper"/>
</div>
然后你可以用这个路由配置完成该布局:
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}
重定向和别名
重定向
重定向也是通过 routes 配置来完成,下面例子是从 /a 重定向到 /b:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: '/b' }
]
})
重定向的目标也可以是一个命名的路由:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
})
甚至是一个方法,动态返回重定向目标:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
]
})
注意导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上
别名
“重定向”的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,那么“别名”又是什么呢?
/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。
上面对应的路由配置为:
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
“别名”的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。
路由组件传参
在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上
使用,限制了其灵活性。
使用 props 将组件和路由解耦:
取代与 $route 的耦合
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [{ path: '/user/:id', component: User }]
})
通过 props 解耦
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})
这样你便可以在任何地方使用该组件,使得该组件更易于重用和测试。
布尔模式
如果 props 被设置为 true,route.params 将会被设置为组件属性。
对象模式
如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。
const router = new VueRouter({
routes: [
{
path: '/promotion/from-newsletter',
component: Promotion,
props: { newsletterPopup: false }
}
]
})
函数模式
你可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。
const router = new VueRouter({
routes: [
{
path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
}
]
})
URL /search?q=vue 会将 {query: ‘vue’} 作为属性传递给 SearchUser 组件。
请尽可能保持 props 函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 Vue 才可以对状态变化做出反应。
完整使用例子
src/pages/Message.vue:
<template>
<div>
<ul>
<li v-for="m in messageList" :key="m.id">
<router-link :to="{
name:'xiangqing',
params:{
id:m.id,
title:m.title
}
}">
{{m.title}}
</router-link>
</li>
</ul>
<hr/>
<router-view></router-view>
</div>
</template>
<script>
export default {
name:'News',
data(){
return{
messageList:[
{id:'001',title:'消息001'},
{id:'002',title:'消息002'},
{id:'003',title:'消息003'}
]
}
}
}
</script>
src/router/index.js:
//该文件专门用于创建整个应用的路由器
import VueRouter from "vue-router";
//引入组件
import Home from '../pages/Home'
import About from '../pages/About'
import News from '../pages/News'
import Message from '../pages/Message'
import Detail from '../pages/Detail'
//创建并暴露一个路由器
export default new VueRouter({
routes:[
{
path:'/about',
component:About
},
{
path:'/home',
component:Home,
children:[
{
path:'news',
component:News
},
{
path:'message',
component:Message,
children:[
{
name:'xiangqing',
path:'detail/:id/:title',
component:Detail,
//props的第一种写法,值为对象,该对象中的所有key-value都会以props的形式传给Detail组件。
// props:{a:1,b:'hello'}
//props的第二种写法,值为布尔值,若布尔值为真,就会把该路由组件收到的所有params参数,以props的形式传给Detail组件。
// props:true
//props的第三种写法,值为函数
props($route){
return {
id:$route.params.id,
title:$route.params.title,
}
}
}
]
}
]
}
]
})
src/pages/Detail.vue:
<template>
<ul>
<li>消息编号:{{id}}</li>
<li>消息标题:{{title}}</li>
</ul>
</template>
<script>
export default {
name:'Detail',
props:['id','title']
}
</script>
HTML5 History 模式
vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。
const router = new VueRouter({
mode: 'history',
routes: [...]
})
当你使用 history 模式时,URL 就像正常的 url,例如 http://yoursite.com/user/id,也好看!
不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。
导航守卫
“导航”表示路由正在发生改变。
正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。
全局前置守卫
你可以使用 router.beforeEach 注册一个全局前置守卫:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
每个守卫方法接收三个参数:
to: Route: 即将要进入的目标 路由对象
from: Route: 当前导航正要离开的路由
next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的
调用参数。
next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是
confirmed (确认的)。
next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器
后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后
进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、
name: 'home' 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的
选项。
next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该
错误会被传递给 router.onError() 注册过的回调。
确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到 /login 的示例:
// BAD 错误用法
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// 如果用户未能验证身份,则 `next` 会被调用两次
next()
})
// GOOD 正确用法
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
全局解析守卫
在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
全局后置钩子
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
router.afterEach((to, from) => {
// ...
})
案例:
//该文件专门用于创建整个应用的路由器
import VueRouter from "vue-router";
//引入组件
import Home from '../pages/Home'
import About from '../pages/About'
import News from '../pages/News'
import Message from '../pages/Message'
import Detail from '../pages/Detail'
//创建一个路由器
const router = new VueRouter({
routes:[
{
name:'guanyv',
path:'/about',
component:About,
meta:{title:'关于'}
},
{
name:'zhuye',
path:'/home',
component:Home,
meta:{title:'主页'},
children:[
{
name:'xinwen',
path:'news',
component:News,
meta:{isAuth:true,title:'新闻'}
},
{
name:'xiaoxi',
path:'message',
component:Message,
meta:{isAuth:true,title:'消息'},
children:[
{
name:'xiangqing',
path:'detail',
component:Detail,
meta:{isAuth:true,title:'详情'},
props($route){
return {
id:$route.query.id,
title:$route.query.title,
}
}
}
]
}
]
}
]
})
//全局前置路由守卫————初始化的时候、每次路由切换之前被调用
router.beforeEach((to,from,next) => {
console.log('前置路由守卫',to,from)
if(to.meta.isAuth){
if(localStorage.getItem('school')==='atguigu'){
next()
}else{
alert('学校名不对,无权限查看!')
}
}else{
next()
}
})
//全局后置路由守卫————初始化的时候被调用、每次路由切换之后被调用
router.afterEach((to,from)=>{
console.log('后置路由守卫',to,from)
document.title = to.meta.title || '硅谷系统'
})
//导出路由器
export default router
路由独享的守卫
你可以在路由配置上直接定义 beforeEnter 守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
这些守卫与全局前置守卫的方法参数是一样的
组件内的守卫
最后,你可以在路由组件内直接定义以下路由导航守卫:
beforeRouteEnter
beforeRouteUpdate (2.2 新增)
beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
next()
}
这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
完整的导航解析流程
导航被触发。
在失活的组件里调用 beforeRouteLeave 守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
路由器的两种工作模式
对于一个url来说,什么是hash值?—— #及其后面的内容就是hash值
hash值不会包含在 HTTP 请求中,即:hash值不会带给服务器
hash模式:
地址中永远带着#号,不美观
若以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法
兼容性较好
history模式:
地址干净,美观
兼容性和hash模式相比略差
应用部署上线时需要后端人员支持,解决刷新页面服务端404的问题
路由元信息
定义路由的时候可以配置 meta 字段:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
那么如何访问这个 meta 字段呢?
首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录
例如,根据上面的路由配置,/foo/bar 这个 URL 将会匹配父路由记录以及子路由记录。
一个路由匹配到的所有路由记录会暴露为 $route 对象 (还有在导航守卫中的路由对象) 的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。
下面例子展示在全局导航守卫中检查元字段:
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
过渡动效
<router-view>
是基本的动态组件,所以我们可以用<transition>
组件给它添加一些过渡效果
<transition>
<router-view></router-view>
</transition>
Transition 的所有功能 (opens new window)在这里同样适用
单个路由的过渡
上面的用法会给所有路由设置一样的过渡效果,如果你想让每个路由组件有各自的过渡效果,可以在各路由组件内使用 并设置不同的 name。
const Foo = {
template: `
<transition name="slide">
<div class="foo">...</div>
</transition>
`
}
const Bar = {
template: `
<transition name="fade">
<div class="bar">...</div>
</transition>
`
}
基于路由的动态过渡
还可以基于当前路由与目标路由的变化关系,动态设置过渡效果:
<!-- 使用动态的 transition name -->
<transition :name="transitionName">
<router-view></router-view>
</transition>
// 接着在父组件内
// watch $route 决定使用哪种过渡
watch: {
'$route' (to, from) {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
}
}
数据获取
有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:
- 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示。
- 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。
从技术角度讲,两种方式都不错 —— 就看你想要的用户体验是哪种。
导航完成后获取数据
当你使用这种方式时,我们会马上导航和渲染组件,然后在组件的 created 钩子中获取数据。这让我们有机会在数据获取期间展示一个 loading 状态,还可以在不同视图间展示不同的 loading 状态。
假设我们有一个 Post 组件,需要基于 $route.params.id 获取文章数据:
<template>
<div class="post">
<div v-if="loading" class="loading">
Loading...
</div>
<div v-if="error" class="error">
{{ error }}
</div>
<div v-if="post" class="content">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
</div>
</template>
export default {
data () {
return {
loading: false,
post: null,
error: null
}
},
created () {
// 组件创建完后获取数据,
// 此时 data 已经被 observed 了
this.fetchData()
},
watch: {
// 如果路由有变化,会再次执行该方法
'$route': 'fetchData'
},
methods: {
fetchData () {
this.error = this.post = null
this.loading = true
// replace getPost with your data fetching util / API wrapper
getPost(this.$route.params.id, (err, post) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.post = post
}
})
}
}
}
在导航完成前获取数据
通过这种方式,我们在导航转入新的路由前获取数据。我们可以在接下来的组件的 beforeRouteEnter 守卫中获取数据,当数据获取成功后只调用 next 方法。
export default {
data () {
return {
post: null,
error: null
}
},
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// 路由改变前,组件就已经渲染完了
// 逻辑稍稍不同
beforeRouteUpdate (to, from, next) {
this.post = null
getPost(to.params.id, (err, post) => {
this.setData(err, post)
next()
})
},
methods: {
setData (err, post) {
if (err) {
this.error = err.toString()
} else {
this.post = post
}
}
}
}
在为后面的视图获取数据时,用户会停留在当前的界面,因此建议在数据获取期间,显示一些进度条或者别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。
滚动行为
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
注意: 这个功能只在支持 history.pushState 的浏览器中可用。
当创建一个 Router 实例,你可以提供一个 scrollBehavior 方法:
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
}
})
scrollBehavior 方法接收 to 和 from 路由对象。第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。
这个方法返回滚动位置的对象信息,长这样:
- { x: number, y: number }
- { selector: string, offset? : { x: number, y: number }} (offset 只在 2.6.0+ 支持)
如果返回一个 falsy (译者注:falsy 不是 false,参考这里 (opens new window))的值,或者是一个空对象,那么不会发生滚动。
举例:
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
对于所有路由导航,简单地让页面滚动到顶部。
返回 savedPosition,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样:
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
如果你要模拟“滚动到锚点”的行为:
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash
}
}
}
我们还可以利用路由元信息更细颗粒度地控制滚动。
异步滚动
你也可以返回一个 Promise 来得出预期的位置描述:
scrollBehavior (to, from, savedPosition) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 500)
})
}
将其挂载到从页面级别的过渡组件的事件上,令其滚动行为和页面过渡一起良好运行是可能的。但是考虑到用例的多样性和复杂性,我们仅提供这个原始的接口,以支持不同用户场景的具体实现。
平滑滚动
只需将 behavior 选项添加到 scrollBehavior 内部返回的对象中,就可以为支持它的浏览器 (opens new window)启用原生平滑滚动:
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash,
behavior: 'smooth',
}
}
}
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件 (opens new window)和 Webpack 的代码分割功能 (opens new window),轻松实现路由组件的懒加载。
首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):
const Foo = () =>
Promise.resolve({
/* 组件定义对象 */
})
第二,在 Webpack 2 中,我们可以使用动态 import (opens new window)语法来定义代码分块点 (split point):
import('./Foo.vue') // 返回 Promise
结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。
const Foo = () => import('./Foo.vue')
在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:
const router = new VueRouter({
routes: [{ path: '/foo', component: Foo }]
})
把组件按组分块
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk (opens new window),一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
导航故障
当使用 router-link 组件时,Vue Router 会自动调用 router.push 来触发一次导航。 虽然大多数链接的预期行为是将用户导航到一个新页面,但也有少数情况下用户将留在同一页面上:
- 用户已经位于他们正在尝试导航到的页面
- 一个导航守卫通过调用 next(false) 中断了这次导航
- 一个导航守卫抛出了一个错误,或者调用了 next(new Error())
当使用 router-link 组件时,这些失败都不会打印出错误。然而,如果你使用 router.push 或者 router.replace 的话,可能会在控制台看到一条 “Uncaught (in promise) Error” 这样的错误,后面跟着一条更具体的消息。让我们来了解一下如何区分导航故障。
检测导航故障
导航故障是一个 Error 实例,附带了一些额外的属性。要检查一个错误是否来自于路由器,可以使用 isNavigationFailure 函数:
import VueRouter from 'vue-router'
const { isNavigationFailure, NavigationFailureType } = VueRouter
// 正在尝试访问 admin 页面
router.push('/admin').catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
// 向用户显示一个小通知
showToast('Login in order to access the admin panel')
}
})
提示:如果你忽略第二个参数:isNavigationFailure(failure),那么就只会检查这个错误是不是一个导航故障。
NavigationFailureType
NavigationFailureType 可以帮助开发者来区分不同类型的导航故障。有四种不同的类型:
- redirected:在导航守卫中调用了 next(newLocation) 重定向到了其他地方。
- aborted:在导航守卫中调用了 next(false) 中断了本次导航。
- cancelled:在当前导航还没有完成之前又有了一个新的导航。比如,在等待导航守卫的过程中又调用了 router.push。
- duplicated:导航被阻止,因为我们已经在目标位置了
导航故障的属性
所有的导航故障都会有 to 和 from 属性,分别用来表达这次失败的导航的目标位置和当前位置。
// 正在尝试访问 admin 页面
router.push('/admin').catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
failure.to.path // '/admin'
failure.from.path // '/'
}
})
在所有情况下,to 和 from 都是规范化的路由位置。