0%

Vue-Router基本使用及hash模式和history模式实现原理

Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue.js 过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5 历史模式或 hash 模式,在 IE9 中自动降级
  • 自定义的滚动条行为

创建目录

根据vue-cli脚手架自动生成一个简单的带有router文件夹的项目目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue CLI v4.5.9
Failed to check for updates
? Please pick a preset: Manually select features
? Check the features needed for your project:
( ) Choose Vue version
(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
(*) Router
( ) Vuex
( ) CSS Pre-processors
>( ) Linter / Formatter
( ) Unit Testing
( ) E2E Testing

Vue Router使用

  • 1.注册路由插件
  • 2.创建一个router对象,创建过程中配置一些路由规则
  • 3.注册router对象,也就是在创建vue实例的时候,要在选项里面,来配置我们创建好的router对象
  • 4.创建路由组件占位和跳转链接

    注册路由插件

    在 router/index.js 文件中
    1
    2
    3
    4
    5
    6
    7
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Home from '../views/Home.vue'

    // 1.注册路由插件
    // Vue.use() 是用来注册插件,会调用传入对象的install方法
    Vue.use(VueRouter)

    创建router对象配置路由规则

    在 router/index.js 文件中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 路由规则
    const routes = [
    {
    path: '/',
    name: 'Home',
    component: Home
    },
    {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
    }
    ]

    // 2.创建router路由对象
    const router = new VueRouter({
    routes
    })

    export default router

    注册router对象

    在main.js文件中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'

    Vue.config.productionTip = false

    new Vue({
    // 3. 注册router 对象
    router,
    render: h => h(App)
    }).$mount('#app')

    创建路由组件占位和跳转链接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div id="app">
    <div id="nav">
    <!-- 创建路由连接 -->
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
    </div>
    <!-- 创建路由占位 -->
    <router-view/>
    </div>

$route和$router区别

在main.js中

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import App from './App.vue'
// import router from './router'

Vue.config.productionTip = false

const vm = new Vue({
// 3. 注册router 对象
// router,
render: h => h(App)
}).$mount('#app')

console.log(vm)

先将router注释,查看打印结果发现没有$route$router

放开注释后,会发现多了$route$router
route

  • $route: 表示路由规则,存储的是当前路由的一些路由数据,存储了一些路径
    route

  • $router: 表示路由对象,就是vuerouter的一个实例,这个路由对象中会提供一些跟路由相关的一些方法
    router

需要注意的是$router里面有一个currentRoute,这个当前路由规则,有的时候不方便获取$route,比如在一个插件里面没有办法获取到$route,可以想办法获取到$router,然后获取cunrrentRoute,这样也就获取到了当前路由的规则

动态路由传参

动态传参连接跳转

  • query会在地址栏出现 id='XX' eg: http://localhost:8080/#/?id=1
  • params会在地址栏 /xx,eg: http://localhost:8080/#/detail/1,props方式需要用这个方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div id="app">
    <div id="nav">
    <!-- 创建路由连接 -->
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link> |
    <!-- <router-link to="/detail">Detail</router-link> -->
    <!-- <router-link :to="{name:'Detail',query:{id:1}}">Detail</router-link> -->
    <router-link :to="{name:'Detail',params:{id:1}}">Detail</router-link>
    </div>
    <!-- 创建占位 -->
    <router-view/>
    </div>

    路由配置

    在router/index.js文件中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 路由规则
    const routes = [
    {
    path: '/',
    name: 'Home',
    component: Home
    },
    {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
    },
    {
    path: '/detail/:id',
    name: 'Detail',
    // 开启props会把url中的参数传递给组件
    // 当为true时,会将url中的参数传递给相应的组件,在组件中通过props来接收就可以了
    props: true,
    component: () => import(/* webpackChunkName: "about" */ '../views/Detail.vue')
    }
    ]

    接收方式

    两种方式接收(方式1需要强依赖,更推荐方式2):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- Detail.vue内容 -->

    <template>
    <div class="detail">
    <h1>This is an Detail page</h1>
    <!-- 方式1: 通过路由规则获取 -->
    <div>路由规则获取query方式:{{$route.query.id}}</div>
    <div>路由规则获取params方式:{{$route.params.id}}</div>

    <!-- 方式2: 通过prop获取 -->
    <div>props方式获取:{{id}}</div>
    </div>
    </template>
    <script>
    export default {
    name:'Detail',
    props:['id']
    }
    </script>

    嵌套路由

    在router/index.js文件中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 路由规则
    const routes = [
    {
    path: '/',
    name: 'Home',
    component: Home
    },
    {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
    children: [
    {
    path: 'detail/:id',
    // 开启props会把url中的参数传递给组件
    // 当为true时,会将url中的参数传递给相应的组件,在组件中通过props来接收就可以了
    props: true,
    component: () => import(/* webpackChunkName: "about" */ '../views/Detail.vue')
    }
    ]
    }
    ]

    编程式导航

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    this.$router.push('/')

    this.$router.push({name:'Home'})

    this.$router.push({name:'Home',params:{id:1}})

    this.$router.push({path:'/home',query:{id:1}})

    // 不会记录本次历史
    this.$router.replace('/home')

    // 跳转到历史的某一次,如果是负数就是后退
    this.$router.go(-2)

    Hash模式 和 History模式区别

    无论那种模式,都是客户端路由的实现方式,也就是当路径发生变化之后不会向服务器发送请求,使用JS监视路径的变化,然后根据不同的地址渲染不同的内容,如果需要服务器端内容的话会发送ajax请求去获取。

    表现形式的区别

  • hash模式:
    1
    https://music.163.com/#/playlist?id=121212
  • history模式:
    1
    https://music.163.com/playlist/31032232

    原理区别

    Hash模式
    hash模式是基于锚点,以及onhashchange事件,通过锚点的值作为路由地址,地址发生变化出发onhashchange事件,在根据路径决定页面上呈现的内容
    History模式
    history是基于HTML5中的History API
  • history.pushState()
  • history.replaceState()
    history.pushState和 history.push区别
  • history.pushState() 不会向服务器发送请求,只会改变浏览器地址栏中的地址,并且把地址记录到历史记录中,所以可以实现客户端路由 另外,IE10 以后才支持
  • history.push() 路径发生变化,需要向服务器发送请求

    History模式使用

  • history需要服务器的支持
  • 单页面应用中,只有一个index.html这样的页面,服务端不存在http://www.test.com/login 这样的地址,服务端不存在/login这样的页面
  • 在服务端应该除了静态资源外都返回单页应用的index.html
    1
    2
    3
    4
    5
    // 2.创建router对象
    const router = new VueRouter({
    mode: 'history', //默认是hash
    routes
    })
    因为vue-cli自带的服务器已经配置好了上面说的内容,下面将代码打包部署的到node服务器中进行一个测试
  • Node服务器配置 history模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const path = require('path')

    // 导入history模式的模块
    const history = require('connect-history-api-fallback')

    // 导入express
    const express = require('express')
    const app = express()

    // 注册处理history模式的中间件
    // app.use(history())

    // 处理静态资源的中间件 网站根目录 ../web
    app.use(express.static(path.join(__dirname,'../web')))

    // 开启服务
    app.listen(3000, ()=>{
    console.log('服务器开启,端口:3000')
    })

关闭history刷新页面
这是先关闭处理history(注释app.use(history())),当前求地址后,刷新页面就会出错了

如下放开注释,打开history支持,再去刷新页面,就可以了

1
2
// 注册处理history模式的中间件
app.use(history())

加载过程是:
服务器会判断当前请求的页面,服务器上没有会把我们单页应用默认的index.html返回给浏览器,浏览器接收到这个index.html,会再去判断路由地址,然后根据路由去加载对应配置的组件,这就是加载的过程。

将打包好的前端项目,复制到html中
Nginx配置2

启动nginx点击访问可以,但是一旦刷新,就会出现如下问题
Nginx配置3

这个时候就需要去nginx.config里面配置一下
Nginx配置4

1
try_files $uri $uri/ /index.html;   #新加一个 

try_files 试着去请求当前浏览器请求的路径所对应的文件,没找到就去找这个目录下的默认首页,如果没有找到就去单文件应用下的首页(/index.html) 重启nginx,现在访问的时候再刷新页面就不会出现上面的404了

实现原理

hash模式
  • URL中#后面的内容作为路径地址
  • 监听hashchange事件
  • 根据当前路由地址找到对应的组件重新渲染
history模式
  • 通过history.pushState()方法改变地址栏
  • 监听popstate事件
  • 根据当前路由地址找到对应组件重新渲染

实现一个自己的vue-router

原文件router/index.js中生成的默认vue-router如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 注册插件
Vue.use(VueRouter)

// 创建路由对象
const router = new VueRouter({
routes:[
{ name: 'home', path: '/', component: homeComponent }
]
})
// main.js
// 创建Vue实例 注册router对象
new Vue({
router,
reder: h => h(App)
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
let _Vue = null
export default class VueRouter {
// 导出一个静态方法 install
// Vue.use() 中调用install方法时候,会传递两个参数,一个是vue构造函数 另一个是 可选的选项对象
static install (Vue) { //这里传递构造函数
// 1. 判断当前插件是否已经被安装
if(VueRouter.install.installed){
return
}
VueRouter.install.installed = true
// 2. 把vue构造函数记录到全局变量
_Vue = Vue
// 3. 把创建vue实例时候传入的router 对象注入到vue实例上
// 混入
_Vue.mixin({
beforeCreate(){
// 只有vue $options里面才会有router 组件没有
if(this.$options.router){
_Vue.prototype.$router = this.$options.router
// 先找到route对象,然后再调用init方法
this.$options.router.init()
}
}
})
}

// 创建一个构造函数
constructor(options){
this.options = options //作用就是记录构造函数传入的options路由规则
this.routeMap = {} // routeMap里面是一个键值对形式,健存的就是路由地址,值就是路由组件 将来在router-view这个组件里面会根据当前的路由地址到routeMap里面找到对应的组件然后渲染到浏览器中
this.data = _Vue.observable({ // 响应式对象
current: '/' // current用来记录当前路由地址,默认情况下是 / 斜杠
})
}

// 创建一个初始化方法 方便调用
init () {
this.createRouteMap()
this.initComponents(_Vue) // 传入构造函数
}

// 这个方法的作用是去遍历所有的路由规则,把这些路由规则解析成键值对的形式存储到routeMap中
createRouteMap(){
// this.options.routes // 所有的路由规则
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}

initComponents (Vue) { // 参数是vue构造函数
// 创建router-link 组件
Vue.component('router-link', {
props: {
to: String
},
template: `<a :href = "to"><slot></slot></a>`
})
}
}

引入自己写的vueRouter

这个时候页面直接去引入这个方法,

1
2
3
4
import Vue from 'vue'
// import VueRouter from 'vue-router'
import VueRouter from '../vuerouter' // 这里引入自己写的vuerouter
import Home from '../views/Home.vue'

然后发现页面会有报错
router_error

因为Vue的构建版本分为运行时版完整版

  • 运行时版:不支持template模版,需要打包的时候提前编译
  • 完整版:包含运行时和编译器,体积比运行时版本大10K左右,程序运行的时候把模版转换成render函数

我们的vue-vli创建的项目默认的是运行时版,所有就出现上面的错误了,解决如下:

  • 方法一:通过完整版来解决问题,https://cli.vuejs.org/zh/config/#全局-cli-配置
    router_error
    在根目录下创建一个vue.config.js文件

    1
    2
    3
    module.exports = {
    runtimeCompiler: true
    }

    这样配置后,页面中router-link就能显示了,还有一个报错,是因为自己手写时候还没有写router-view 的组件

  • 方法二:通过运行时版来解决问题,运行时版本的vue不带编译器,也就是不支持组件中的template选项,编译器的作用就是将template编译成render函数,所有直接写render函数来解决

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    initComponents (Vue) {   // 参数是vue构造函数
    // 创建router-link 组件
    Vue.component('router-link', {
    props: {
    to: String
    },
    // template: `<a :href = "to"><slot></slot></a>`
    render (h){
    return h('a',{ // h函数 可以接收三个参数,1创建元素的选择器 2.设置属性 3.生成元素的子元素所以数组的形式
    attrs: {
    href: this.to // 传的props中的 to
    }
    }, [this.$slots.default]) //this.$slots.default获取默认插槽的内容
    }
    })
    }

    创建router-view 这个组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    initComponents (Vue) {   // 参数是vue构造函数
    // 创建router-link 组件
    Vue.component('router-link', {
    props: {
    to: String
    },
    // template: `<a :href = "to"><slot></slot></a>`
    render (h){
    return h('a',{ // h函数 可以接收三个参数,1创建元素的选择器 2.设置属性 3.生成元素的子元素所以数组的形式
    attrs: {
    href: this.to // 传的props中的 to
    }
    }, [this.$slots.default]) //this.$slots.default获取默认插槽的内容
    }
    })
    const self = this
    // 创建router-view 组件
    Vue.component('router-view',{
    render(h){ // h作用是创建虚拟DOM
    // 找到当前路由的地址 根据当前路由地址,去routeMap对象中,找到对应的组件,然后再调用h函数,把找到的组件转换成虚拟DOM直接返回
    const component = self.routeMap[self.data.current] //获取组件
    return h(component) // 调用h函数,返回虚拟DOM ,h函数还可以直接把组件转换成虚拟DOM
    }
    })
    }

    这个时候去页面查看,没有报错了,可以显示了,但是当点击切换的时候,页面会刷新,需要给这个超链接注册一个点击时间,取消后续执行,还需要将地址栏中的值,改为超链接href中的值,就需要history.pushState()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 创建router-link 组件
    Vue.component('router-link', {
    props: {
    to: String
    },
    // template: `<a :href = "to"><slot></slot></a>`
    render (h){
    return h('a',{ // h函数 可以接收三个参数,1创建元素的选择器 2.设置属性 3.生成元素的子元素所以数组的形式
    attrs: {
    href: this.to // 传的props中的 to
    },
    // 给a标签添加事件
    on: {
    click: this.clickHandler
    }
    }, [this.$slots.default]) //this.$slots.default获取默认插槽的内容
    },
    methods:{
    clickHandler(e){
    history.pushState({},'', this.to) //三个参数:1.data 将来触发pushState这个事件对象的参数 2.title网页标题 3.url地址

    // 将当前的路径,记录到data.current里面
    this.$router.data.current = this.to

    // 阻止跳转默认行为
    e.preventDefault()
    }
    }
    })

    现在就可以点击了,不会刷新了,地址栏也会跟着点击标签的href所改变了,虽然现在可以,但是当我们点击浏览器前进,后退的时候,地址栏发生了变化,但是组件没有发生变化,因为前进后退的时候,没有重新加载地址栏对应的组件,下面通过popstate进行改进,当历史发生变化触发,到这里模拟router的history模式就搞定了

    1
    2
    3
    4
    5
    6
    // 解决前进后退 地址栏变化组件不变化的问题
    initEvent(){
    window.addEventListener('popstate', ()=> {
    this.data.current = window.location.pathname //路径部分
    })
    }

    完整代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    let _Vue = null
    export default class VueRouter {
    // 导出一个静态方法 install
    // Vue.use() 中调用install方法时候,会传递两个参数,一个是vue构造函数 另一个是 可选的选项对象
    static install (Vue) { //这里传递构造函数
    // 1. 判断当前插件是否已经被安装
    if(VueRouter.install.installed){
    return
    }
    VueRouter.install.installed = true
    // 2. 把vue构造函数记录到全局变量
    _Vue = Vue
    // 3. 把创建vue实例时候传入的router 对象注入到vue实例上
    // 混入
    _Vue.mixin({
    beforeCreate(){
    // 只有vue $options里面才会有router 组件没有
    if(this.$options.router){
    _Vue.prototype.$router = this.$options.router
    // 先找到route对象,然后再调用init方法
    this.$options.router.init()
    }
    }
    })
    }

    // 创建一个构造函数
    constructor(options){
    this.options = options //作用就是记录构造函数传入的options路由规则
    this.routeMap = {} // routeMap里面是一个键值对形式,健存的就是路由地址,值就是路由组件 将来在router-view这个组件里面会根据当前的路由地址到routeMap里面找到对应的组件然后渲染到浏览器中
    this.data = _Vue.observable({ // 响应式对象
    current: '/' // current用来记录当前路由地址,默认情况下是 / 斜杠
    })
    }

    // 创建一个初始化方法 方便调用
    init () {
    this.createRouteMap()
    this.initComponents(_Vue) // 传入构造函数
    this.initEvent()
    }

    // 这个方法的作用是去遍历所有的路由规则,把这些路由规则解析成键值对的形式存储到routeMap中
    createRouteMap(){
    // this.options.routes // 所有的路由规则
    this.options.routes.forEach(route => {
    this.routeMap[route.path] = route.component
    })
    }

    initComponents (Vue) { // 参数是vue构造函数
    // 创建router-link 组件
    Vue.component('router-link', {
    props: {
    to: String
    },
    // template: `<a :href = "to"><slot></slot></a>`
    render (h){
    return h('a',{ // h函数 可以接收三个参数,1创建元素的选择器 2.设置属性 3.生成元素的子元素所以数组的形式
    attrs: {
    href: this.to // 传的props中的 to
    },
    // 给a标签添加事件
    on: {
    click: this.clickHandler
    }
    }, [this.$slots.default]) //this.$slots.default获取默认插槽的内容
    },
    methods:{
    clickHandler(e){
    history.pushState({},'', this.to) //三个参数:1.data 将来触发pushState这个事件对象的参数 2.title网页标题 3.url地址

    // 将当前的路径,记录到data.current里面
    this.$router.data.current = this.to

    // 阻止跳转默认行为
    e.preventDefault()
    }
    }
    })
    const self = this
    // 创建router-view 组件
    Vue.component('router-view',{
    render(h){ // h作用是创建虚拟DOM
    // 找到当前路由的地址 根据当前路由地址,去routeMap对象中,找到对应的组件,然后再调用h函数,把找到的组件转换成虚拟DOM直接返回
    const component = self.routeMap[self.data.current] //获取组件
    return h(component) // 调用h函数,返回虚拟DOM ,h函数还可以直接把组件转换成虚拟DOM
    }
    })
    }

    // 解决前进后退 地址栏变化组件不变化的问题
    initEvent(){
    window.addEventListener('popstate', ()=> {
    this.data.current = window.location.pathname // 路径部分就是/斜杠和斜杠后面的部分 eg: /home
    })
    }
    }
--------- 本文结束感谢您的阅读 ---------
分享