详解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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容