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 发生改变,路由器就会匹配当前路径,得到一个组件进行渲染。

posted @ 2020-10-21 02:46  没有想象力  阅读(1343)  评论(0编辑  收藏  举报