详解vue-router

120902.jpg

我有一碗酒,可以慰风尘

前端路由定义

在SPA中,路由指的是URL与UI之间的映射,这种映射是单向的,即URL变化引起UI更新(无需刷新界面)

实现前端路由

实现前端路由,需要解决两个核心问题

  • 如何改变URL却不引起页面刷新
  • 如何检测URL变化了

vue-router里面有hash和history两种方式,下面介绍一下这两种方式

hash实现

hash指的是URL中hash(#)及后面的那一part,改变URL中的hash部分不会引起页面刷新,并且可以通过hashchange事件监听URL的变化。改变URL的方式有如下几种:

  • 通过浏览器的前进后退改变URL
  • 通过<a>标签改变URL
  • 通过window.location改变URL

history实现

HTML5中的history提供了pushState和replaceState两个方法,这两个方法改变URL的path部分不会引起页面刷新
history提供类似hashchange事件的popstate事件,但又有不同之处

  • 通过浏览器前进后退改变URL是会触发popstate事件
  • 通过pushState/replaceState或者<a>标签改变URL不会触发popstate事件
  • 我们可以拦截pushState/replaceState的调用和<a>标签的点击事件来检测URL的变化
  • 通过js调用history的back,go,forward方法来触发该事件

实操js实现前端路由

基于hash

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title></title>
    </head>
    <body>
        <ul>
            <li><a href="#/home">home</a></li>
            <li><a href="#/about">about</a></li>
        </ul>   
        <!-- 路由渲染口子 -->
        <div id="routerView"></div>
    </body>
    <script>
        let routerView = document.getElementById('routerView')
        window.addEventListener('hashchange', () => {
           const hash = location.hash
           routerView.innerHTML = hash
        })
        
        window.addEventListener('DOMContentLoaded', () => {
            if (!location.hash) {
                location.hash = "/"
            } else {
                const hash = location.hash
                routerView.innerHTML = hash
            }
        })
        
    </script>
</html>

效果图


image.png

上面的代码干了哪些活?

  • 通过<a>标签的href属性来改变URL中的hash值
  • 监听了hashchange事件,当事件触发的时候,改变routerView中的内容
  • 监听了DOMContentLoaded事件,初次的时候需要渲染成对应的内容

基于HTML5 history实现

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title></title>
    </head>
    <body>
        <ul>
            <li><a href="#/home">home</a></li>
            <li><a href="#/about">about</a></li>
        </ul>   
        <!-- 路由渲染口子 -->
        <div id="routerView"></div> 
        <script>
            const router = document.getElementById('routerView')
            window.addEventListener('DOMContentLoaded', () => {
                const linkList = document.querySelectorAll('a[href]')
                linkList.forEach(el => el.addEventListener('click', function(e) {
                    e.preventDefault()
                    history.pushState(null, '', el.getAttribute('href'))
                    router.innerHTML = location.pathname
                }))
            })
            
            window.addEventListener('popstate', () => {
                router.innerHTML = location.pathname
            })
            
        </script>
    </body>
</html>
  • 我们通过a标签的href属性来改变URL的path值(当然,你触发浏览器的前进后退按钮也可以,或者在控制台输入history.go,back,forward赋值来触发popState事件)。这里需要注意的就是,当改变path值时,默认会触发页面的跳转,所以需要拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
  • 我们监听popState事件。一旦事件触发,就改变routerView的内容

vue 中vue-router

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]
const router = new VueRouter({
  mode:"history",
  routes
})
export default router


// App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/home">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

截图如下

image.png

改造vue-router文件

import Vue from 'vue'
// import VueRouter from 'vue-router'
import VueRouter from './myVueRouter'
import Home from '../views/Home.vue'
import About from "../views/About.vue"


Vue.use(VueRouter)
  const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]
const router = new VueRouter({
  mode:"history",
  routes
})
export default router

分析vue-router文件干了啥

1, 通过import VueRouter from 'vue-router' 引入了VueRouter
2,const router = new VueRouter({})
3,Vue.use(VueRouter)使得每个组件都可以拥有router实例

  • 通过new VueRouter({})获得实例,也就是说VueRouter其实是一个类
class VueRouter {
   
}
  • 使用Vue.use(),而Vue.use其实就是执行对象的install这个方法
class VueRouter {

}

VueRouter.install = function () {

}

export default VueRouter

分析Vue.use

Vue.use(plugin)
1,参数

{ object | Function } plugin

2,用法
安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。
3, 作用
注册插件,此时只需要调用install方法并将Vue作为参数传入即可。但在细节上有两部分逻辑要处理:

1、插件的类型,可以是install方法,也可以是一个包含install方法的对象。

2、插件只能被安装一次,保证插件列表中不能有重复的插件。

4,实现

Vue.use = function(plugin) {
    const installPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installPlugins.indexOf(plugin) > -1) {
        return
    }

    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
        plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
        plugin.apply(null, plugin, args)
    }

    installPlugins.push(plugin)
    return this    
}

1、在Vue.js上新增了use方法,并接收一个参数plugin。
2、首先判断插件是不是已经别注册过,如果被注册过,则直接终止方法执行,此时只需要使用indexOf方法即可。
3、toArray方法我们在就是将类数组转成真正的数组。使用toArray方法得到arguments。除了第一个参数之外,剩余的所有参数将得到的列表赋值给args,然后将Vue添加到args列表的最前面。这样做的目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。
4、由于plugin参数支持对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,即可知用户使用哪种方式祖册的插件,然后执行用户编写的插件并将args作为参数传入。
5、最后,将插件添加到installedPlugins中,保证相同的插件不会反复被注册。

了解以上开始写myVueRouter.js

let Vue = null

class VueRouter {

}

VueRouter.install = function (v) {
    Vue = v
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首页')
        }
    })
}

export default VueRouter

简易版果然跑起来了,截图如下


image.png

完善install方法

install 是给每个vue实例添加东西的,在router中给每个组件添加了$route和$router

这俩的区别是:

$router是VueRouter的实例对象,$route是当前路由对象,也就是说$route是$router的一个属性

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

我们可以发现这里只是将router ,也就是./router导出的router实例,作为Vue 参数的一部分。
但是这里就是有一个问题咯,这里的Vue 是根组件啊。也就是说目前只有根组件有这个router值,而其他组件是还没有的,所以我们需要让其他组件也拥有这个router。
因此,install方法我们可以这样完善

let Vue = null

class VueRouter {

}

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根组件
                this._root = this // 把当前实例挂载到_root上
                this._router = this.$options.router
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首页')
        }
    })
}

export default VueRouter

一通操作之下的解释

  • 参数Vue,我们在分析Vue.use的时候,再执行install的时候,将Vue作为参数传进去。,
  • mixin的作用是将mixin的内容混合到Vue的初始参数options中。相信使用vue的同学应该使用过mixin了。
  • 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options已经初始化好了。
  • 如果判断当前组件是根组件的话,就将我们传入的router和_root挂在到根组件实例上。
  • 如果判断当前组件是子组件的话,就将我们_root根组件挂载到子组件。注意是引用的复制,因此每个组件都拥有了同一个_root根组件挂载在它身上。

? 为啥判断是子组件就直接取父组件的_root根组件呢
来,看一下父子组件执行顺序先
父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted

在执行子组件的beforeCreate的时候,父组件已经执行完beforeCreate了,拿到_root那就没问题了


在vueRouter文件中

const router = new VueRouter({
  mode:"history",
  routes
})

我们传了两个参数,一个模式mode,一个是路由数组routes

let Vue = null

class VueRouter {
    constructor (options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        this.routesMap = this.createMap(this.routes)
        
    }
    createMap (routes) {
        return routes.reduce((pre, current) => {
            pre[current.path] = current.component
            return pre
        }, {})
    }
}

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根组件
                this._root = this // 把当前实例挂载到_root上
                this._router = this.$options.router
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首页')
        }
    })
}

export default VueRouter

路由中需要存放当前的路径,来表示当前的路径状态,为了方便管理,用一个对象来表示。初始化的时候判断是那种模式,并将当前的路径保存到current中

let Vue = null

class HistoryRoute {
    constructor () {
        this.current = null
    }
}

class VueRouter {
    constructor (options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        this.routesMap = this.createMap(this.routes)
        this.history = new HistoryRoute()
        this.init()
    }

    init () {
        if (this.mode === 'hash') {
            // 先判断用户打开是是否有hash值,没有跳转到#/
            location.hash ? '' : location.hash = '/'
            window.addEventListener('load', () => {
                this.history.current = location.hash.slice(1)
            })
            window.addEventListener('hashchange', () => {
                this.history.current = location.hash.slice(1)
            })
        } else {
            location.pathname ? '' : location.pathname = '/'
            window.addEventListener('load', () => {
                this.history.current = location.pathname
            })
            window.addEventListener('popstate', () => {
                this.history.current = location.pathname
            })
        }
    }

    createMap (routes) {
        return routes.reduce((pre, current) => {
            pre[current.path] = current.component
            return pre
        }, {})
    }
}

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根组件
                this._root = this // 把当前实例挂载到_root上
                this._router = this.$options.router
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首页')
        }
    })
}

export default VueRouter

完善$route

其实/$route就是获取当前的路径

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根组件
                this._root = this // 把当前实例挂载到_root上
                this._router = this.$options.router
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })

            Object.defineProperty(this, '$route', {
                get () {
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首页')
        }
    })
}

完善router-view

Vue.component('router-view', {
        render(h) {
            let current = this._self._root._router.history.current
            let routeMap = this._self._root._router.routesMap
            return h(routeMap[current])
        }
    })

render函数里的this指向的是一个Proxy代理对象,代理Vue组件,而我们前面讲到每个组件都有一个_root属性指向根组件,根组件上有_router这个路由实例。
所以我们可以从router实例上获得路由表,也可以获得当前路径。
然后再把获得的组件放到h()里进行渲染。
现在已经实现了router-view组件的渲染,但是有一个问题,就是你改变路径,视图是没有重新渲染的,所以需要将_router.history进行响应式化。

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根组件
                this._root = this // 把当前实例挂载到_root上
                this._router = this.$options.router
                // 新增
                Vue.util.defineReactive(this, 'xxx', this._router.history)
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })

            Object.defineProperty(this, '$route', {
                get () {
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            let current = this._self._root._router.history.current
            let routeMap = this._self._root._router.routesMap
            return h(routeMap[current])
        }
    })
}

效果如下


image.png

image.png

完善router-link

Vue.component('router-link',{
        props: {
            to: String
        },
        render(h) {
            let mode = this._self._root._router.mode
            let to = mode === 'hash' ? '#'+this.to : this.to
            return h('a', {attrs: {href: to}}, this.$slots.default)
        }
    })

截图如下


image.png

image.png

myVueRouter.js完整代码

let Vue = null

class HistoryRoute {
    constructor () {
        this.current = null
    }
}

class VueRouter {
    constructor (options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        this.routesMap = this.createMap(this.routes)
        this.history = new HistoryRoute()
        this.init()
    }

    init () {
        if (this.mode === 'hash') {
            // 先判断用户打开是是否有hash值,没有跳转到#/
            location.hash ? '' : location.hash = '/'
            window.addEventListener('load', () => {
                this.history.current = location.hash.slice(1)
            })
            window.addEventListener('hashchange', () => {
                this.history.current = location.hash.slice(1)
            })
        } else {
            location.pathname ? '' : location.pathname = '/'
            window.addEventListener('load', () => {
                this.history.current = location.pathname
            })
            window.addEventListener('popstate', () => {
                this.history.current = location.pathname
            })
        }
    }

    createMap (routes) {
        return routes.reduce((pre, current) => {
            pre[current.path] = current.component
            return pre
        }, {})
    }
}

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根组件
                this._root = this // 把当前实例挂载到_root上
                this._router = this.$options.router
                // 新增
                Vue.util.defineReactive(this, 'xxx', this._router.history)
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })

            Object.defineProperty(this, '$route', {
                get () {
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link',{
        props: {
            to: String
        },
        render(h) {
            let mode = this._self._root._router.mode
            let to = mode === 'hash' ? '#'+this.to : this.to
            return h('a', {attrs: {href: to}}, this.$slots.default)
        }
    })
    Vue.component('router-view', {
        render(h) {
            let current = this._self._root._router.history.current
            let routeMap = this._self._root._router.routesMap
            return h(routeMap[current])
        }
    })
}

export default VueRouter
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容