Vue基础篇--自定义组件机制详解
从页面整体架构来说:组件化开发是把页面是以一个个组件为单位组合而成,每个部分实现各自的功能,然后再由这些部分拼接为整个网页。这样可以各部分独立开发,还可以提高代码的复用性(组件模板复用)便于后期维护管理(例如修改组件模板,所有组件自动全部修改)
从组件的组成来说,组件实际上就是一小部分html标签,css样式和javascript逻辑组成的一个小单元。使用上,可以理解为我们自定义了一个可以使用的新标签,这个新标签和html自带的标签没有区别。
Vue组件化的组织
页面由多个组件组合而成,而组件由各个小组件组合而成。大组件嵌套小组件,于是就形成了一个组件树。
可以清晰的了解页面的组织,并且相同功能的组件可以使用相同的组件模板。
定义组件模板
组件模板也分为三个部分
- html
- css
- javascript
定义组件模板:使用Vue.extend方法创建一个组件对象, Vue.component方法将组件注册到Vue全局对象中(方式一),也可以直接使用component方法,通过传入一个对象创建并注册(方式二)
<script>
// 方式一 var comp1 = Vue.extend({ template: "<p> 这是一个组件的html内容 </p>", data:function(){ return {} } }) Vue.compomnet("myComp1", comp1) // myComp1 为组件的名字,直接将该名字的标签放入页面的元素中即可 // 方式二 Vue.compoment("组件名字", { template: "<p> 这是一个组件的html内容 </p>", data:function(){ return {} }, }) </script> // 使用组件 方式一 <div> <my-comp1> </my-comp1> // 如果名字为驼峰写法,需要改为-分割,例如 myComp -> my-comp </div>
// 使用组件方式二
<div>
<div is="myComp"> </div> // 使用is=myComp的方式,整个div标签位置将渲染为一个myComp组件
</div>
组件的信息是一个对象,这个对象的属性和Vue实例的属性基本相同:
- template:组件的html字符串,必须只有一个根元素。内部支持Vue的模板语法。
- data:同 Vue中data属性,定义组件响应式数据地方,但响应式数据对象需要通过一个function返回,保证该模板创建出的多个组件对象之间的响应式数据相互独立。
- methods:定义逻辑方法。
- 生命周期函数:同Vue生命周期函数
- 计算属性监听器
如果没有特殊声明,组件内部定义的各个属性为组件私有。
组件模板创建后必须注册才可以使用该组件,Vue.component()就是将组件进行全局注册。也可以局部注册,只有注册了组件的Vue实例才能使用组件
comp1 = {/....组件一..../} comp2 = {/....组件二..../} comp3 = {/....组件三..../} new Vue({ componet:{ "c1": comp1, // 注册组件1和组件2,并使用c1 和 c2两个标识 "c2": comp2 } })
这个Vue对象只能使用comp1和comp2组件,comp3由于没有注册而无法使用
为代码美观,使用一个template标签将html代码独立出来。而后使用一个选择器定位该标签并绑定到template属性上
<template id="app"> <div> 这是一个组建template内容 </div> </template> <script> var comp = Vue.extend({ template:"#app", // id选择器绑定template的内容 })
Vue.component("comp", comp) </script>
props 自定义属性
porps使用介绍
props中可以定义任意多个属性变量名,这些属性变量的值是通使用组件时从外部传入的。等同于函数的形参,形参的值是在函数调用时从函数外部传入。下面定义了 Msg1 和 Msg2 两个props
var comp2 = Vue.extend({ template: "<div> 这是子组件,父组件信息 {{ Msg1 }}</div>", // 使用Msg1变量数据 props:["Msg1", "Msg2"] }) Vue.component("my-child", comp2)
使用这个组件并为props指定值,my-child 组件内部使用porps中属性的方式和使用data中响应式数据的方式相同。但是props中的数据的更改不会触发页面渲染。
<my-child v-bind:Msg1="aaa" :Msg2="111"></my-child> <my-child v-bind:Msg1="bbb" :Msg2="222"></my-child>
props单向数据流
所有的 prop 在父子组件之间形成了一个单向下行绑定。父级 prop 的更新会向下流动到子组件中,但不要通过更新子组件props去影响父组件props。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
如果组件内部需要自己更新这个porps值,可以在接收这个props的值后,重新定义一个data变量赋予该初始值来使用
props: ['initCounter'], data: function () { return { counter: this.initCounter // 将 initCounter这个props接收的值保存到组件data,之后使用data中的数据实时进行渲染 } }
如果组件内部需要实时的跟随父组件props的变化而变化,可以定义一个计算属性,这个计算属性将会实时随父组件更新。
props: ['size'], computed: { formatSize: function () { return this.size + "px" // props中size的改变,将会触发formatSize的重新计算 } }
如果props传递的是数组或者对象类型的值,子组件和父组件使用的实际为同一个引用对象,子组件中对数组或对象的内部修改会影响到父组件中该对象的值
props验证
在定义props属性时,可以预定义类型和验证规则,限制外部组件传入组件中的值。此时props使用一个对象指定。porps中每个属性可以有
- type: 类型验证,指定一个类型,也可以是自定义类型
- required:要求必须指定该参数
- default:指定默认值,数值类型指定指定,数组和对象类型需要使用函数返回值指定,原理同data
- validator:自定义验证函数,参数为需要进行验证的值,返回true表示验证通过
var comp2 = Vue.extend({ template: "<div> 这是子组件 </div>", props:{ propA: Number, // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证) propB: [String, Number], // 多个可能的类型 propC: { type: String, // 字符串类型 required: true // 必填
default: "c" // 默认值
}, propE: { type: Object, // 当默认值为对象或数组时 都必须从是函数返回值指定,原理同data default: function () { return { message: 'hello' } } }, // 自定义验证函数,返回true 通过,否则验证失败 propF: { validator: function (value) { // 这个值必须匹配下列字符串中的一个 return ['success', 'warning', 'danger'].indexOf(value) !== -1 } } } }) Vue.component("my-child", comp2)
非props中属性的处理
组件标签中自定义的属性会被props中对应的同名属性接收。如果标签中制定了 props中没有定义的属性名,该如何处理?
<my-comp :a="1" :b="2" v-bind:c="3" v-bind:d="4" > </my-comp>
Vue.component("my-comp", { el: "<div> ...嵌套一些html元素... <div>", props: ['a', 'c'], })
my-comp中只定义了a, c两个属性,标签中传入的b,d属性无法被接收,这两个属性将会被赋值到这个组件的html根元素上,也就是上面el中的 div根元素。这个过程被叫做 组件的html根元素 继承 组件属性。
如果不希望组件的根元素继承 这些属性,可以在组件的选项中设置 inheritAttrs: false
Vue.component("my-comp", { inheritAttrs: false, el: "<div> ...嵌套一些html元素... <div>", props: ['a', 'c'], })
那么如何访问这两个属性呢:可以使用 组件的 $attrs。$attrs中包含了所有不能直接在 组件对象 和 组件根元素对象上访问的属性。
// 组件中 this指向组件对象自己 this.$attrs.b this.$attrs.d
$attrs在定义基础组件时常用到,例如上面的 my-comp 组件不使用 b,d属性,但是它的子组件需要这个属性,就可以从$attrs中访问这两个属性指定
Vue.component("my-comp", { inheritAttrs: false, el: "<div> <child v-bind=$attrs> </child> // attrs中有b,d两个属性会传递给child标签。 等同于 <child :b=$arrts.b :c=$attrs.c> </child>
</div>",
props: ['a', 'c'],
})
上面的使用方式中,props中b, d两个属性忽略了 my-comp 组件的存在,将属性传递到了 下一级组件标签中。
组件自定义事件
自定义组件可以像一般组件那样监听事件,使用 v-on 指令即可
<my-component v-on:my-event="doSomething"> </my-component>
这里监听了 my-component 组件的 my-event 的事件,当 my-event 事件触发,doSomething函数将会被执行。
那么my-event事件如何被触发?-- 使用组件内部的emit方法并指定一个事件名即可。
Vue.component('blog-post', { props: ['post'], template: ` <div> <button v-on:click="$emit('my-event')"> 点击 </button> // 组件中这个按钮的点击事件将会执行 emit("my-event"),my-event被触发 </div> ` })
换一个编码方式可能更清晰
Vue.component('blog-post', { props: ['post'], template: ` <div> <button v-on:click="clickedEvent"> 点击 </button> </div> ` methods: { clickedEvent() {this.$emit('my-event')} } })
使用emit方法时候也可以继续跟随参数值,会作为事件处理函数的参数。
this.$emit('my-event', "abc", "123")
此时的doSomething函数就可以定义两个形式参数接收两个参数值。
使用示例:例如我们可以使用父组件方法作为子组件事件的回调函数。
<script> // 父组件,定义了一个show函数,并将这个show函数传递给子组件 var comp1 = Vue.extend({ template: "<parent> <child v-bind:dataEvent="showData"></child> // 监听子组件dataEvent事件,事件触发组件showData函数调用 </parent>",
data(){
return {
childData:{}
}
} // 父组件定义showData方法
methods:{ showData(data){ console.log(data) this.data.childData = data // 获取到子组件的数据 } } }) Vue.component("my-parent", comp1) // 定义一个子组件,并定义一个tocall方法,方法中触发 dataEvent事件 var comp2 = Vue.extend({ template: "<div> 这是子组件 </div>", methods:{ toCall(){ this.$emit("dataEvent", this.data) // 子组件触发dataEvent事件,并将自己的打他作为参数传递 } } props:["func"] }) Vue.component("my-child", comp2) </script>
父组件监听了子组件事件,子组件就可以通过触发事件的方式,将自己持有的状态向上层父组件传递。父组件由此触发了函数参数,可以执行一些操作,也由此得到了子组件传递过来的数据。这个数据与props从上向下的状态传递相反,是一种可行的数据向上传递的方案。
自定义v-model指令
v-model指定实现的是数据的双向绑定,标签数据改变触发data数据改变,data数据改变也会触发标签数据的改变。在一个input标签上使用v-model
<input v-model="text">
等价于:
<input v-bind: value="text" v-on: input="text= $event.target.value" >
data中text变量的改变会自动修改标签值,而通过监听输入框的input方法,在标签value值改变时候,实时修改data中text。实现了双向数据绑定。
了解v-model的原理,先写出自定义组件使用v-model标签时的等价式。
<custom-input v-bind:value="text" v-on:input="text= $event" // 此处的 $event 是custom-input组件input事件的返回的 第一参数值,也就是下面的 $event.target.value 值
></custom-input>
根据这个等价式,可以知道,custom-input内部需要定义一个value的props,需要在输入每次修改时,触发一次input事件并将最新修改的值返回。所以定义如下
Vue.component('custom-input', { props: ['value'], // 接收上层传入的value,作为内部input标签的值 template: ` <input v-bind:value="value" // 接收的上层 value v-on:input="$emit('input', $event.target.value)" // 通过input标签触发的input事件,将最新修改数据向上层传递。 >` })
由此,可以使用v-model指令实现双向绑定了,父组件中text的变化会改变 custom-input 标签中的内容,custom-input 标签中的输入也会实时修改 text 的值
<custom-input v-model="text"></custom-input>
组件切换
组件切换的实现
组件切换也是一种必要的组织组件的方式,例如登陆注册组件之间的切换,在页面同一个位置上,根据用户选择的不同,渲染不同的组件。这是最简单的组件切换场景
有多种方式实现组件的切换
- 使用 v-if 和 v-else 条件判断的方式选择性渲染。
- 使用组件的 is 属性,is=组件名,则对应展示该组件。修改组件名后,该标签将会被渲染为其他组件
- 使用 Vue 中提供的路由。这是利于url中的哈希实现的组件切换,详细见Vue路由篇。
第一种:点击按钮,通过控制flag为true或false
<c1 v-if='flag'> 组件1 </c1> <c2 v-else> 组件2 </c2> <button @click="flag = !flag"> 点击切换 </button> new Vue({ data() { flag: ture } })
第二种方式,点击按钮,会触发事件修改组件名,实现两个组件的切换
// 点击按钮切换,设置不同view值即可 <a href="" @click="view='v-a'"> 切换到a组件</a> <a href="" @click="view='v-b'"> 切换到b组件</a> <component :is="view"></component> // view值可以被修改为 v-a 或 v-b var = new Vue({ data:{ view:"v-a" } componments:{ "v-a":{ template:"<h1>组件v-a</h1>", }, "v-b":{ template:"<h1>组件v-b</h1>", } } })
缓存切换组件
多个组件切换时,在同一位置只会显示一个组件,未展示的组件我们认为它是不存在于内存中的,组件切换时,会立刻创建将要展示的组件进行显示,同时删除掉当前显示的组件对象。
如果不希望组件被删除而是继续缓存在内存中,使用keep-alive标签即可
<keep-alive > <component :is="view"></component> </keep-alive>
data : () => {
return {
view: "comp1"
}
}
这个component位置上所有创建后没有展示的组件,都不会删除,下次需要展示时直接使用。这样做的得失显而易见,缓存组件意味消耗更多内存,但可以避免重新创建而节省了时间和计算资源。根据实际场景选择。
在这些组件中,当前被渲染的组件状态为 active,而没有渲染的组件为 deactive,组件的切换会改变相应组件的状态。每个组件可以定义两个生命周期函数,actived 和 deactived 函数,分别在组件展示和隐藏时调用。
使用<keep-alive> 缓存组件时需要注意以下几点:
- 在<keep-alive> 内部进行切换组件,每个组件都需要有自己的名字,首先会检查组件的name 属性,也支持使用 vue.component("name", comp) 的方式注册的组件,但不能是没有注册的匿名组件。
- 代码中的<keep-alive>标签只是一个缓存的标识,真实的页面的不会出现在这个标签。
- <keep-alive> 内部只可能同时渲染展示一个根组件,其余均为 deactive 状态。
- <keep-alive> 下的使用 v-for 展示的内容不会被缓存。
keep-alive的常用参数
使用 keep-alive 标签时候,可以添加几个属性。
- include 和 exclude:指定被部分组件使用缓存或者不使用缓存。匹配可以用逗号分隔字符串、正则表达式或一个数组。匹配的是组件的 name 或者组件局部注册的名称,匿名组件不能被匹配。
<!-- 逗号分隔字符串 --> // 只会缓存 a,b 两个组件,其余组件不会缓存 <keep-alive include="a,b"> <component :is="view"></component> </keep-alive> <!-- 正则表达式 (使用 `v-bind`) --> <keep-alive :include="/a|b/"> <component :is="view"></component> </keep-alive> <!-- 数组 (使用 `v-bind`) --> <keep-alive :include="['a', 'b']"> <component :is="view"></component> </keep-alive>
- max:指定缓存的组件的最大数量,一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
<keep-alive :max="10"> // 最多缓存10个 <component :is="view"></component> </keep-alive>
插槽slot
槽可以看作是定义组件模板时预留的预留的一个位置,在使用组件时,可以根据具体需要向这个位置注入一个想要的内容。槽使用 <slot></slot>标签定义。
例如定义一个comp组件模板定义如下:
<div id=app> <p> 标题 </p> <slot></slot> <p> 结束 </p> </div>
Vue.component("comp", {
el: "#app",
data: function(){return {}}
})
使用该comp组件。就可以向槽中注入内容
<comp> <p> 内容1 </p> </comp>
<comp> <p> 内容2 </p> </comp>
<comp>标签内的任意内容都会替换到模板中的<slot>位置并展示。只需要这个内容只存在一个根元素即可
也可以在槽上定义默认的内容,当组件在使用时没有向槽注入内容,则会展示默认的信息
<div id="app"> <p> 标题 </p> <slot> 这是默认内容</slot> <p> 结束 </p> </div>
Vue.component("comp", {
el: "#app",
data: function(){return {}}
})
<comp> </comp> // 未注入任何内容,展示默认数据
命名插槽
一个组件中可以定义多个槽,定义多个时需要为这个插槽命名以示区分,最多只能有一个没有命名的插槽,这个插槽回默认插槽。使用时,如果没有指定注入槽的名字,就会注入到默认的插槽中。
指定插槽的名字使用name属性,例如 lay 组件中定义了三个槽
<div id="container"> <slot name="header"></slot> <slot> </slot> <slot name="footer"></slot> </div>
Vue.component("lay", {
el: "#container",
data: function(){return {}}
})
向槽内插入内容时,使用 v-slot 指令指定插入插槽的名字,未指定名字的内容则插入到默认插槽中。每个插槽的内容都只能有一个根元素。
v-slot属性指定指定到 <template>标签上,2.x版本可以使用slot指令,且可以添加到随意标签。但在vue3.0后只能使用v-slot
<lay> <template v-slot:header> <p> 这是header </p> </template> <template v-slot:footer> <p> 这是footer</p> </template> <template > <p> content 内容 </p> </template> </lay>
这三个标签会按照插槽名字分别插入到 lay 组件的三个槽中,未指定名字的元素插入到默认槽中。
指定插槽名时,可以使用缩写的方式指定。使用#表示,如果默认插槽使用了缩写符号,就必须写为 #defalut 而不能只写 #
<lay> <template #header> <p> 这是header </p> </template> <template #footer> <p> 这是footer</p> </template> <template #default > <p> content 内容 </p> </template> </lay>
还可以指定一个动态的插槽名,根据名字不同,将这个内容动态的插入到插槽中
<base-layout> <template v-slot:[dynamicSlotName]> ... </template> </base-layout>
插槽作用域和v-slot的使用
插槽的作用域为组件内部。使用组件时,向插槽中插入内容的作用域在组件外部,该内容无法直接访问组件的内部属性。
<div id='account'> <slot> {{ user.name }} </slot> </div> Vue.component("xxx": { el: "#account", data() { return { user: {id:1, name:"tom", age: "18"} } } })
组件data中定义了一个user,在插槽中,可以访问到组件中的data,即可以使用user.name,而如果在向组件插入内容时,尝试去访问组件内部的变量,则无法办到,例如使用xxx组件时,想使用xxx组件中的user
<xxx> <template> {{ user.name }} </template> </xxx>
上面的内容虽然会插入到xxx组件的槽中,但由于变量作用域的限制,却无法访问到 xxx 组件内部的任何变量。此处user变量的作用域应该属于xxx的父组件。
想让此处的user可以访问到xxx组件内部是中常见的用法。此时通过v-slot指定插槽的props即可。
首先,定义插槽时,需要在插槽上绑定需要向父级提供的属性。定义一个槽属性person,并指向user变量
<div id='account'> <slot v-bind:person="user"> {{ user.name }} </slot> </div>
使用组件时,使用v-slot="slotAllProps"的方式指定一个可用变量slotAllprops,slotAllProps会收集slot上所有的槽属性和值,例如下面slotAllprops的值结构为: slotallProps => { "person" : user}
<xxx v-slot="slotAllProps"> <template v-slot="sslotAllProps"> {{ slotAllProps.person.name}} </template> </xxx>
总结为:定义插槽时使用v-bind将属性绑定到slot上,向插槽插入内容时,通过v-slot指定一个变量收集所有slot上绑定的属性,就可以借用这个变量访问组件内部属性。
当有多个插槽时,多个插槽的变量相互独立
<div id="container"> <slot name="header" v-bind:x="a"></slot> <slot v-bind:y="b"> </slot> <slot name="footer" v-bind:z="c"></slot> </div> Vue.component("lay", { el: "#container", data: function(){ return { a: 1, b: 2, c: 3 } } })
定义三个slot分别绑定属性,下面使用三个变量收集每个slot的属性,第三个插槽,使用了js对象解构的特性,直接解构出默认插槽中的 y 属性使用
<lay> <template v-slot:header="headerProps"> <p> 这是header: {{ headerProps.x}} </p> </template> <template v-slot:footer="footerProps"> <p> 这是footer: {{ footerProps.z }} </p> </template> <template v-slot:defalut="{ y }"> <p> content 内容 {{ y }} </p> </template> </lay>
当一个组件只有一个默认插槽时,可以忽略template标签而直接将v-slot指定在组件标签上。以下两种写法等效
<xxx v-slot:default="slotProps"> {{ slotProps.user.name}} </xxx> <xxx v-slot:default="slotProps"> <template > {{ slotProps.user.name}} </template> </xxx>
插槽列表渲染
可以通过 v-for 实现插槽的批量定义,并可以将每个插槽属性暴露给上层
<ul id="app"> <li v-for="todo in todoList" :key="todo.id"> <slot name="todo" v-bind:todo="todo"> </slot> </li> </ul>
vue.component("my-comp", {
props: ["todoList"]
el: "#app",
})
todoList来自上层传入的props属性,再根据todoList的元素个数依次定义<slot>,并又将各个元素依次绑定到了每个slot的属性上。上层可以获得每个slot上绑定的todo属性
<my-compv-bind:todoList="todos"> // 通过属性props向组件注入todos列表
<template v-slot:todo="{ todo }"> // 每个todo槽上都绑定了todo属性,通过解构获得 <span v-if="todo.isComplete">✓</span> {{ todo.text }} </template>
</my-comp>
new Vue( {
data:(){ return { todos:[{id:1, text:"a", isComplate: true}, {id:2, text:"b", isComplate: false}] } }
})
异步组件
路由 vue-router
简介
这里的路由是指前端的路由,也就是通过url 的不同来 选择性的渲染不同的组件。url 的改变只会让组件的进行切换,而不是从后端获取新的页面。
Vue 的前端路由主要是基于hash 来实现的,也就是url 上 #
标识符之后的部分。例如`http://vue-router/zh/#/user`这样一个url,#标识符后的路径为/user,我们可以设置规则匹配这个路径,使其对应的渲染指定的组件。
导包顺序
同样的,vue-router基于vue对象,所以必须在Vue包之后导入,确保当前环境中已经存在Vue对象。
直接在html中引入
<script src="https://unpkg.com/vue/dist/vue.js"></script> // 导入的vue <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> // 导入 vue-router
使用webpack 工程化项目时的导入方式
import Vue from 'vue' // 导入vue import Router from 'vue-router' // 导入vue-router
路由器对象
路由就是想要匹配当前访问的url,然后根据当前的 url 选择渲染对应的组件。于是我们需要一个url路径和组件之间的映射关系, vue中在router对象中定义映射关系,即路由器对象
var router = new VueRouter({ // 一个路由对象,内部配置 路径映射关系
routes:[
{ path: '/foo', component: Foo }, // path 指定路径,component指定对应的组件的对象。
{ path: '/bar', component: Bar }
]
})
// 定义好路由器后,还需要将其与对应的Vue对象进行绑定,Vue中的配置对象中有对应的router这个key来绑定路由器。
var vm = new Vue({
el:"#app",
data:{ },
methods:{
show(){
console.log(this.$router)
console.log(this.$route)
}
},
router: router, // 绑定到 Vue 对象上,这个 router 只能在 #app 这个节点内部使用。
})
通过向Vue对象注入路由器,我们可以在Vue对象已经路由对象中使用的任何组件内通过 this.$router
访问路由器对象,获得内部的配置信息,也可以通过 this.$route
访问当前匹配到的路由。上面的方法中尝试去获取这个路由。
挂载到Vue上的路由器,会自动根据当前url中hash指定的 路径匹配到对应的组件,并将这个组件用于展示。但是我们并未向vue说明,我们要在哪里展示匹配到的组件,这就需要一个标记,在这个vue管理的 dom 节点中使用一个router-view标签即可。
<router-view> 标签
可以直接在页面中添加<router-view> 标签,vue 解析该标签时,会在自己对象上找到路由器对象,根据当前的url 匹配到组件,然后把这个组件渲染到这个标签的位置,这就是 <router-view> 标签的作用。
<div id="app"> <p> 这是app </p> <router-view> </router-view> // 当前url 匹配到的组件将展示到这个位置 <router-view> </router-view> // 又有一个该标签,则会展示一次,两个组件完全一样 </div> <script> var router = new VueRouter({ // 一个路由对象,内部配置 路径映射关系 routes:[ { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] }) var vm = new Vue({ el:"#app", data:{}, methods:{}, router: router, // 绑定到Vue对象上,这个router 只能子 #app 这个节点内部使用。 }) </script>
在html 中凡是有 <router-view> 标签的地方,都会使用vue 对象中绑定的路由器匹配到的组件进行填充,所以上面使用了两次router-view,则会显示两个 匹配到的组件。
命名组件
实际上,一个路径可以匹配到的组件可以不止一个,可以有很多个,当匹配到多个组件时需要对每一个组件命名进行区分,使用时按照组件的名字将组件渲染到指定的位置。而用来渲染的组件的 router-view 组件也有对应的 name 属性来指定该标签只会渲染该名字的组件。
<div id="app"> <p> 这是app </p> <router-view > </router-view> // 没有指定name属性,渲染名字为default的组件 <router-view name="a"> </router-view> // 根据name 属性,查看当前被匹配的所有的组件中是否有 name为 "a"的组件,有则渲染 <router-view name="b"> </router-view> </div> <script> var router = new VueRouter({ // 一个路由对象,内部配置 路径映射关系 routes:[ { path: '/foo', components: { // 匹配到多个组件 default: comp1, // key为组件在 路由中的名字,会对应 router-view 中的name属性进行匹配 a: comp2, b: comp3, } }, { path: '/bar', components: {} }, ] }) var vm = new Vue({ el:"#app", data:{}, methods:{}, router: router, }) </script>
嵌套路由
/user/profile /user/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
官方的展示很形象,当访问/user/ 前缀时,会渲染这个user 的组件,而在user组件内部,有一个小的组件,这个组件的内容由 /user/之后的路劲进行匹配,渲染匹配到的子组件。vue 使用了嵌套路由的方式实现这样的功能。
<div id="app"> <router-view></router-view> // 外层用于放置 user 这一层组件的位置。内层组件的坑显然应该在外层组价(也就是user组件)内部定义 </div> <script> var comp = Vue.component("comp", { template:"<div> <router-view> </router-view> <div>" // comp 组件内部 渲染路由组件。 }) var router = new VueRouter({ // 一个路由对象,内部配置 路径映射关系 routes:[ { path: '/user', component:comp, // 当匹配到/user,会将comp 渲染到 #app 中的router-view位置。 children: [ { path: 'profile', // 继续匹配到profile,也就是/user/profile时,会将UserProfile 渲染到 comp 中的router-view component: UserProfile }, { path: 'posts', component: UserPosts }, }, ] }) var vm = Vue({ var vm = new Vue({ el:"#app", data:{}, methods:{}, router: router, }) }) </script>
通过指定 children 属性的方式,可以再定义一层路由的匹配规则,这些规则匹配到的组件会再其父组件的router-view中渲染,路由的嵌套,也是组件的嵌套。
children
属性的配置和 routes
配置结构是相同的,可以在 children 中再定义children,实现多层的嵌套,同时children中的每一个路由项目,也是可以映射多个组件,并使用命名组件的。
注意:子路由规则前面不要使用"/",使用后将会按照从url的根开始匹配,而不是从父路由匹配结束位置继续匹配。
定义路由的规则及参数
动态匹配参数
上面的路由是使用字符串的形式完全匹配的,但是有时候url中的路径是一些变化的值, 例如用户/user/id这种url
/user/1
/user/2
上面的url都应该指向同一个组件对象,当时组件对象又应该获取他们不同id值。vue提供了这种动态路由匹配的方式。我们如此定义路由即可
var router = new VueRouter({ // 一个路由对象,内部配置 路径映射关系
routes:[
{ path: '/user/:id ', component:comp}, // 匹配 /user/ 前缀的,后面部分的值由id接收
]
})
:id
会进行动态的匹配任意值,接收的值会放入router对象的params属性中,属性值为一个对象。当我们访问 /user/1
这个url时候,将会展示comp这个组件,并且在这个组件的内部,我们可以查看其router.params属性,可以得到一个 {"id":"1"}
这样的对象,id的值会随着匹配到的url中的值改变,这样就实现动态路由。:id 这样的匹配方式会以 / 为边际,不会匹配超过 / 边界的内容。例如 /user/1/profile 的 id 匹配到 1 而不会是 1/profile。如果要匹配多个可以使用多个这样的动态参数,路由规则可可以定义为 /user/:id/:foo
,这样匹配 /user/1/profile的结果则是, $route.params = { id:"1", foo:"profile"}
路由参数
url 中可以使用查询字符串的方式,查询字符串不会影响 路由的匹配,只是在匹配后向组件添加了一些参数(/user/?id=1&name=tom 和 /user/ 在匹配时的表现是相同的),这些参数可以在组件this.$route.query对象中查看。
例如访问的url的hash为: /user/?id=1&name=tom var router = new VueRouter({ routes:[ { path: '/user', component:comp}, // 匹配规则 ] })
通过上面的url访问,我们可以在路由的query中得到这样的值, this.$route.query => { "id": "1", name: "tom" }
,无论是使用动态路由,访问$route.params 对象还是使用 查询字符串,访问$route.query 对象,都可以实现url 对组件内部传值的的功能。
匹配规则中还可以使用通配符来匹配路径
{ path: '*'} // 会匹配所有路径
{ path: '/user-*'} // 会匹配以 `/user-` 开头的任意路径
这样的匹配规则通常用来捕捉url的错误,当上面所有路由都没有匹配到时,将会匹配到该项。
重定向和别名
// 重定向,访问'/a' 时,将用户访问重新定位到访问 '/b' 上,有一个重定向的过程
const router = new VueRouter({
routes: [
{ path: '/a', redirect: '/b' } // url匹配到/a 路径,会重定向到 /b
{ path: '/c', redirect: from => { return '/b'} } // from 参数为'/c', return 值为重定向的路径。
{ path: '/b', redirect: comp}
]
})
// 别名:访问 "/b" 只是/a 的一个别名,两个规则都指向同一个组件,所以访问两个url 都想过都相同,不会发生重定向。
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
路由导航标签 router-link
定义好了路由对象和对应的匹配规则,我们还需要能快速跳转到这些 url 的方式,vue使用了 router-link 标签来 实现导航链接,这些导航链接标签会被默认渲染为 一个 <a> 标签。可以通过 该标签的 tag 属性指定 渲染的标签名。当然也可以使用 <a> 标签,注意使用时 href 属性的指定的路径应该是hash值中的路径,也就是 <a href="#/user/profile"> </a>
,而不是直接跳转 url 中的路径,所以 应该带上 #
。
使用 router-link 的原因主要是更加的方便和灵活,例如可以向<a>标签那样指定跳转路径
<router-link to="/user" tag="button"> 用户模块 </router-link> // to 指定跳转的路径, tag指定渲染为一个button按钮 <router-link to="/user/123" tag="button"> 用户模块 </router-link> <router-link to="/user/?id=123" tag="button"> 用户模块 </router-link>
可以使用命名路由的名字,并携带参数。使用这种方式需要我们为每一个路由项添加一个name属性
const router = new VueRouter({ routes: [ { path: '/user/:userId', name: 'a', // 该路由项的name 为 a,所以跳转时候,只需要指定该名即可指向这条路由 component: User } ] }) <router-link :to="{ name: 'a', params: { userId: 123 }}" tag="button"> 用户模块 </router-link> // 指定跳转到 路由项目为 name 为a 的这条路由上,并通过params: { userId: 123 }注入参数,还可以使用query:{"id":123} 注入数据 // to 指定的一个对象,这里需要对 to 进行属性绑定,也就是 v-bind, to 的值才不会被解析为一个字符串。
代码中实现url的跳转
代码中实现url跳转的方式就是手动的在 Vue对象的history对象中 push 或者 pop url页面信息,当history 栈顶发生变化,页面也会对应变为栈顶对应的页面。
当我们点击 router-link 标签时,当前url将会改变为这个标签中to属性指向的路径,从而页面发生改变或者刷新,而点击标签能够发生跳转的动作,实际上是通过点击事件向 一个 history 栈 中添加了这个新的url 记录实现的。这里的history栈 是当页面加载后,vue中生成的一个记录所有页面跳转的栈的数据结构,这个栈会从一开始记录所有的url访问记录,且栈中最新的元素也就当前页面的信息,当我们需要展示新的页面时,将最新的内容push 到这个 history 中,由于栈的最新元素发生了改变,所以页面跳转了,同理,如果点击浏览器的后退按钮,将会从这个history 栈中 pop 最新的元素,所以页面 会回到上一个操作的页面。
所以如果我们想要在代码中实现页面的跳转,push 最新的页面内容即可,vue 为我们在做好了封装而方便我们去操作这个histroy从而在代码层面实现页面的跳转,push 的对象 同router-link中使用的对象即可。例如
// 有这样一个路由器,内部只定义了一条路由规则 const router = new VueRouter({ routes: [ { path: '/user/:userId', name: 'a', component: User } ] }) var vm = new Vue({ el:"#app", data: {}, router:router, methods:{ optHistory(){ // 调用router中的push方法,将新的元素也就是router-link 中的 to 属性的值,添加到 history 的最新页面中即可。 // router 内部会解析并找到对应的路由信息。 this.$router.push('user/123') this.$router.push({path:"'/user/123'"}) this.$router.push({ name:"a", params: { userId: 123 }}) // 向history 中添加新的路由信息,跳转到新的页面 } } }) // 这是一个点击调转的按钮,如果点击,将会根据 路由项 的name,匹配到上面那一条路由,页面发生跳转。 <router-link :to="{ name: 'a', params: { userId: 123 }}" tag="button"> 用户模块 </router-link> // 我们已经知道了页面跳转实际上是history栈中添加了新的元素,而history 栈由router内部,所以,我们可以调用上面method 中的 // optHistory方法,也可以实现点击跳转按钮的功能。
除了push 方法,还可以调用replace(),将最新的记录替换,而不是进行添加。同样的我们也可以访问栈中原来的元素实现页面回退
router.go(-1) // 回退一个记录
router.go(1) // 前进一个记录
router.go(-3) // 回退三个记录
总结路由的使用
这是路由器的基本使用形式,主要包括几个步骤
- 创建路由器并在内部建立路径和组件的映射关系, 嵌套路由使用children指定,匹配路径时可以使用 ":username" 的动态匹配方式。
- 路由对象创建完成后,将这个路由器挂载到 vue 对象上,vue对象即可使用。
- 在这个view对象管理的区域使用 <router-view> 标签指定一个渲染组件的位置,这个vue对象会将根据当前路径匹配到的组件渲染到这个位置,如果时嵌套路由,子路由匹配的组件应该在父路由对应的组件中定义。
- 页面中使用 router-link 标签实现url 的改变,类似于<a>标签,但使用更灵活
每次url 发生改变,路由器就会匹配当前路径,得到一个组件进行渲染。