精华内容
下载资源
问答
  • vueRouter原理

    2021-01-18 17:51:25
    前端路由分为两种模式,分别是hash模式和history模式 hash模式原理实现是通过监听hashchange的改变进行无刷新...-- router-link --> <a href="#/">首页</a> <a href="#/about">关于</a>.

    前端路由分为两种模式,分别是hash模式和history模式
    hash模式原理实现是通过监听hashchange的改变进行无刷新渲染
    history原理通过调用history.pushState(无刷新插入历史状态,更新url)更新url,并且监听popstate回退键事件进行渲染

    1-hash模式

    	<-- router-link -->
        <a href="#/">首页</a>
        <a href="#/about">关于</a>
        
        <-- router-view -->
        <div id="view"></div>
        <script>
    		//1.监听hash值的变化,hash改变,div内容改变
    		window.addEventListener('hashchange',()=>{
    			view.innerHTML = location.hash.slice(1)
    		})	
    		//2、一开始页面当加载完dom时,DOMContentLoaded对dom进行渲染	
    		document.addEventListener('DOMContentLoaded',()=>{
    			view.innerHTML = location.hash.slice(1)
    })							
    
    // 注意:当元素不是a时,通过onclick(路径名)方法进行操作	{location.hash=路径名}						
    	</script>
    

    2-history模式

    	<!-- router-link -->
        <span onclick="changerouter('/')">首页</span>
        <span onclick="changerouter('/about')">关于</span>
        
        <!-- router-view -->
        <div id="view"></div>
    
    <script>
    	// 1、history.pushState无刷新插入历史状态,更改url
    	function changerouter(pathname){
    		history.pushState(null,null,pathname)
    }
    	//2、监听回退键popstate,渲染
    	window.addEventListener("popstate",()=>{
    		view.innerHTML = location.pathname
    })
    </script>
    

    history和hash的区别

    1-hash模式以#开头,浏览器可以进行前进和后退,并且不刷新,不和服务端交流
    2-无#,和服务端同事交流

    展开全文
  • 手写vue-router核心原理一、...基于 history 实现三、基于Vue实现VueRouter四、剖析VueRouter本质五、分析Vue.use六、完善install方法七、完善VueRouter类八、完善$route九、完善router-view组件十、完善router-link...

    手写vue-router核心原理

    • 一、核心原理

      • 1.什么是前端路由?

      • 2.如何实现前端路由?

    • 二、原生js实现前端路由

      • 1.基于 hash 实现

      • 2.基于 history 实现

    • 三、基于Vue实现VueRouter

    • 四、剖析VueRouter本质

    • 五、分析Vue.use

    • 六、完善install方法

    • 七、完善VueRouter类

    • 八、完善$route

    • 九、完善router-view组件

    • 十、完善router-link组件

    一、核心原理

    1.什么是前端路由?

    在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。

    2.如何实现前端路由?

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

    1. 如何改变 URL 却不引起页面刷新?

    2. 如何检测 URL 变化了?

    下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。

    hash 实现

    hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新

    通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:

    1. 通过浏览器前进后退改变 URL
    2. 通过标签改变 URL
    3. 通过window.location改变URL
    history 实现

    history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新

    history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:

    1. 通过浏览器前进后退改变 URL 时会触发 popstate 事件
    2. 通过pushState/replaceState或标签改变 URL 不会触发 popstate 事件。
    3. 好在我们可以拦截 pushState/replaceState的调用和标签的点击事件来检测 URL 变化
    4. 通过js 调用history的back,go,forward方法课触发该事件

    所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。

    二、原生js实现前端路由

    1.基于 hash 实现

    html

    html>
    <html lang="en">
    <body>
    <ul>
        <ul>
            
            <li><a href="#/home">homea>li>
            <li><a href="#/about">abouta>li>

            
            <div id="routeView">div>
        ul>
    ul>
    body>
    <script>let routerView = routeViewwindow.addEventListener('hashchange', ()=>{let hash = location.hash;
            routerView.innerHTML = hash
        })window.addEventListener('DOMContentLoaded', ()=>{if(!location.hash){//如果不存在hash值,那么重定向到#/
                location.hash="/"
            }else{//如果存在hash值,那就渲染对应UIlet hash = location.hash;
                routerView.innerHTML = hash
            }
        })
    script>
    html>


    解释下上面代码,其实很简单:

    1. 我们通过a标签的href属性来改变URL的hash值(当然,你触发浏览器的前进后退按钮也可以,或者在控制台输入window.location赋值来改变hash)
    2. 我们监听hashchange事件。一旦事件触发,就改变routerView的内容,若是在vue中,这改变的应当是router-view这个组件的内容
    3. 为何又监听了load事件?这时应为页面第一次加载完不会触发 hashchange,因而用load事件来监听hash值,再将视图渲染成对应的内容。

    2.基于 history 实现

    html>
    <html lang="en">
    <body>
    <ul>
        <ul>
            <li><a href='/home'>homea>li>
            <li><a href='/about'>abouta>li>

            <div id="routeView">div>
        ul>
    ul>
    body>
    <script>let routerView = routeViewwindow.addEventListener('DOMContentLoaded', onLoad)window.addEventListener('popstate', ()=>{
            routerView.innerHTML = location.pathname
        })function onLoad ({
            routerView.innerHTML = location.pathnamevar linkList = document.querySelectorAll('a[href]')
            linkList.forEach(el => el.addEventListener('click'function (e{
                e.preventDefault()
                history.pushState(null'', el.getAttribute('href'))
                routerView.innerHTML = location.pathname
            }))
        }
    script>
    html>

    解释下上面代码,其实也差不多:

    1. 我们通过a标签的href属性来改变URL的path值(当然,你触发浏览器的前进后退按钮也可以,或者在控制台输入history.go,back,forward赋值来触发popState事件)。这里需要注意的就是,当改变path值时,默认会触发页面的跳转,所以需要拦截 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
    2. 我们监听popState事件。一旦事件触发,就改变routerView的内容。
    3. load事件则是一样的

    有个问题:hash模式,也可以用history.go,back,forward来触发hashchange事件吗?

    A:也是可以的。因为不管什么模式,浏览器为保存记录都会有一个栈。

    三、基于Vue实现VueRouter

    我们先利用vue-cli建一个项目

    3ade4e188d830fa5b03e92edec725cb3.png

    删除一些不必要的组建后项目目录暂时如下:

    bc60d71f12b64f21c888fe9e9cdabe33.png

    已经把项目放到 github:https://github.com/Sunny-lucking/howToBuildMyVueRouter  可以卑微地要个star吗。有什么不理解或者什么建议,欢迎下方评论

    我们主要看下App.vue,About.vue,Home.vue,router/index.js

    代码如下:

    App.vue

    "app">
    "nav">"/home">Home |"/about">About

    router/index.js

    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

    Home.vue

    <template>
      <div class="home">
        <h1>这是Home组件h1>
      div>
    template>

    About.vue

    <template>
      <div class="about">
        <h1>这是about组件h1>
      div>
    template>

    现在我们启动一下项目。看看项目初始化有没有成功。

    8da295d94b72fcb952db7e59d3457a71.png

    ok,没毛病,初始化成功。

    现在我们决定创建自己的VueRouter,于是创建myVueRouter.js文件

    目前目录如下

    3310c6e785b1723faaf18de1592d4e36.png

    再将VueRouter引入 改成我们的myVueRouter.js

    //router/index.js
    import Vue from 'vue'
    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

    四、剖析VueRouter本质

    先抛出个问题,Vue项目中是怎么引入VueRouter。

    1. 安装VueRouter,再通过import VueRouter from 'vue-router'引入
    2. const router = new VueRouter({...}),再把router作为参数的一个属性值,new Vue({router})
    3. 通过Vue.use(VueRouter) 使得每个组件都可以拥有store实例

    从这个引入过程我们可以发现什么?

    1. 我们是通过new VueRouter({...})获得一个router实例,也就是说,我们引入的VueRouter其实是一个类。

    所以我们可以初步假设

    class VueRouter{
        
    }

    1. 我们还使用了Vue.use(),而Vue.use的一个原则就是执行对象的install这个方法

    所以,我们可以再一步 假设VueRouter有有install这个方法。

    class VueRouter{

    }
    VueRouter.install = function ({
        
    }

    到这里,你能大概地将VueRouter写出来吗?

    很简单,就是将上面的VueRouter导出,如下就是myVueRouter.js

    //myVueRouter.js
    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方法被同一个插件多次调用时,插件也只会被安装一次。

    关于如何上开发Vue插件,请看这篇文章,非常简单,不用两分钟就看完:如何开发 Vue 插件?

    (3)作用

    注册插件,此时只需要调用install方法并将Vue作为参数传入即可。但在细节上有两部分逻辑要处理:

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

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

    (4)实现

    Vue.use = function(plugin){
     const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
     if(installedPlugins.indexOf(plugin)>-1){
      return this;
     }
     
     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);
     }
     installedPlugins.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中,保证相同的插件不会反复被注册。(~~让我想起了曾经面试官问我为什么插件不会被重新加载!!!哭唧唧,现在总算明白了)

    第三点讲到,我们把Vue作为install的第一个参数,所以我们可以把Vue保存起来

    //myVueRouter.js
    let Vue = null;
    class VueRouter{

    }
    VueRouter.install = function (v{
        Vue = v;
    };

    export default VueRouter

    然后再通过传进来的Vue创建两个组件router-link和router-view

    //myVueRouter.js
    let Vue = null;
    class VueRouter{

    }
    VueRouter.install = function (v{
        Vue = v;
        console.log(v);

        //新增代码
        Vue.component('router-link',{
            render(h){
                return h('a',{},'首页')
            }
        })
        Vue.component('router-view',{
            render(h){
                return h('h1',{},'首页视图')
            }
        })
    };

    export default VueRouter

    我们执行下项目,如果没报错,说明我们的假设没毛病。

    61c0b97a55e8440fb25c98fd79e6be79.png

    天啊,没报错。没毛病!

    六、完善install方法

    install 一般是给每个vue实例添加东西的

    在这里就是给每个组件添加$route$router

    $route$router有什么区别?

    A:$router是VueRouter的实例对象,$route是当前路由对象,也就是说$route$router的一个属性 注意每个组件添加的$route是是同一个,$router也是同一个,所有组件共享的。

    这是什么意思呢???

    来看mian.js

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

    Vue.config.productionTip = false

    new Vue({
      router,
      renderfunction (hreturn h(App) }
    }).$mount('#app')

    我们可以发现这里只是将router ,也就是./router导出的store实例,作为Vue 参数的一部分。

    但是这里就是有一个问题咯,这里的Vue 是根组件啊。也就是说目前只有根组件有这个router值,而其他组件是还没有的,所以我们需要让其他组件也拥有这个router。

    因此,install方法我们可以这样完善

    //myVueRouter.js
    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',{},'首页')
            }
        })
        Vue.component('router-view',{
            render(h){
                return h('h1',{},'首页视图')
            }
        })
    };

    export default VueRouter

    解释下代码:

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

    这里有个问题,为什么判断当前组件是子组件,就可以直接从父组件拿到_root根组件呢?这让我想起了曾经一个面试官问我的问题:父组件和子组件的执行顺序

    A:父beforeCreate-> 父created -> 父beforeMounte  -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted

    可以得到,在执行子组件的beforeCreate的时候,父组件已经执行完beforeCreate了,那理所当然父组件已经有_root了。

    然后我们通过

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

    $router挂载到组件实例上。

    其实这种思想也是一种代理的思想,我们获取组件的$router,其实返回的是根组件的_root._router

    到这里还install还没写完,可能你也发现了,$route还没实现,现在还实现不了,没有完善VueRouter的话,没办法获得当前路径

    七、完善VueRouter类

    我们先看看我们new VueRouter类时传进了什么东东

    //router/index.js
    import Vue from 'vue'
    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

    可见,传入了一个为数组的路由表routes,还有一个代表 当前是什么模式的mode。因此我们可以先这样实现VueRouter

    class VueRouter{
        constructor(options) {
            this.mode = options.mode || "hash"
            this.routes = options.routes || [] //你传递的这个路由是一个数组表
        }
    }

    先接收了这两个参数。

    但是我们直接处理routes是十分不方便的,所以我们先要转换成key:value的格式

    //myVueRouter.js
    let Vue = null;
    class VueRouter{
        constructor(options) {
            this.mode = options.mode || "hash"
            this.routes = options.routes || [] //你传递的这个路由是一个数组表
            this.routesMap = this.createMap(this.routes)
            console.log(this.routesMap);
        }
        createMap(routes){
            return routes.reduce((pre,current)=>{
                pre[current.path] = current.component
                return pre;
            },{})
        }
    }

    通过createMap我们将

    const routes = [
      {
        path: '/home',
        name: 'Home',
        component: Home
      },
      {
        path: '/about',
        name: 'About',
        component: About
      }

    转换成

    79f64ebed9c7356769751c57f01aa6a9.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();
            
        }

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

    }

    但是我们现在发现这个current也就是 当前路径还是null,所以我们需要进行初始化。

    初始化的时候判断是是hash模式还是 history模式。,然后将当前路径的值保存到current里

    //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;
            },{})
        }

    }

    监听事件跟上面原生js实现的时候一致。

    八、完善$route

    前面那我们讲到,要先实现VueRouter的history.current的时候,才能获得当前的路径,而现在已经实现了,那么就可以着手实现$route了。

    很简单,跟实现$router一样

    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',{},'首页')
            }
        })
        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进行响应式化。

    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提供的API:defineReactive,使得this._router.history对象得到监听。

    因此当我们第一次渲染router-view这个组件的时候,会获取到this._router.history这个对象,从而就会被监听到获取this._router.history。就会把router-view组件的依赖wacther收集到this._router.history对应的收集器dep中,因此this._router.history每次改变的时候。this._router.history对应的收集器dep就会通知router-view的组件依赖的wacther执行update(),从而使得router-view重新渲染(其实这就是vue响应式的内部原理)

    好了,现在我们来测试一下,通过改变url上的值,能不能触发router-view的重新渲染

    9eb1dd119c2bb159204ceacbd3eb5911.png

    path改成home

    d661779df6ca7814dc9fc2ee052829ba.png

    可见成功实现了当前路径的监听。。

    十、完善router-link组件

    我们先看下router-link是怎么使用的。

    "/home">Home</router-link> about">About

    也就是说父组件间to这个路径传进去,子组件接收就好 因此我们可以这样实现

    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)
        }
    })

    我们把router-link渲染成a标签,当然这时最简单的做法。 通过点击a标签就可以实现url上路径的切换。从而实现视图的重新渲染

    ok,到这里完成此次的项目了。

    看下VueRouter的完整代码吧

    //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

    现在测试下成功没

    52447285c2caccc751d8cf663c444187.png|

    8958fb7a481794543647726a8307c090.png点击确实视图切换了,成功。

    完美收官!!!!

    有什么不理解或者什么建议,欢迎下方评论

    感谢您也恭喜您看到这里,我可以卑微的求个star吗!!!

    github:https://github.com/Sunny-lucking/howToBuildMyVueRouter

    参考文献:文章前面一、二节原理部分 摘自:https://blog.csdn.net/qq867263657/article/details/90903491

    1d159a31e987c0ef6f188595972cb74d.png

    展开全文
  • 8.1.2 Vue Router原理实现

    2020-08-07 14:07:58
    8.1.2 Vue Router原理实现 Vue router 初始化vue的时候选择router,会自动生成router相关代码 Vue.use(VueRouter) use传参如果是函数,会调用函数注册组件,如果是对象的话会调用对象的install方法 import Vue from ...

    本文为拉勾网大前端高薪训练营第一期笔记


    8.1.2 Vue Router原理实现

    Vue router

    初始化vue的时候选择router,会自动生成router相关代码

    Vue.use(VueRouter)

    use传参如果是函数,会调用函数注册组件,如果是对象的话会调用对象的install方法

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Index from '../views/Index.vue'
    // 1. 注册路由插件
    Vue.use(VueRouter)
    
    // 路由规则
    const routes = [
      {
        path: '/',
        name: 'Index',
        component: Index
      },
      {
        path: '/blog',
        name: 'Blog',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* webpackChunkName: "blog" */ '../views/Blog.vue')
      },
      {
        path: '/photo',
        name: 'Photo',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* webpackChunkName: "photo" */ '../views/Photo.vue')
      }
    ]
    // 2. 创建 router 对象
    const router = new VueRouter({
      routes
    })
    
    export default router
    
    //template部分代码
    <template>
      <div id="app">
        <div>
          <img src="@/assets/logo.png" alt="">
        </div>
        <div id="nav">
          <!-- 5. 创建链接 -->
          <router-link to="/">Index</router-link> |
          <router-link to="/blog">Blog</router-link> |
          <router-link to="/photo">Photo</router-link>
        </div>
        <!-- 4. 创建路由组件的占位,匹配到的会在这里显示 --> 
        <router-view/>
      </div>
    </template>
    
    //main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    
    Vue.config.productionTip = false
    
    new Vue({
      // 3. 注册 router 对象,这里注册的话Vue实例会增加$route和$router属性
    	//$route包括路由参数 $router是VueRouter实例提供很多方法,常用push replace go
    	//插件里经常拿不到$route,但是能拿到$router,currentRoute可以获取当前路由规则
      router,
      render: h => h(App)
    }).$mount('#app')
    
    

    动态路由

    路由配置

    const routes = [
      {
        path: '/',
        name: 'Index',
        component: Index
      },
      {
        path: '/blog',
        name: 'Blog',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* webpackChunkName: "blog" */ '../views/Blog.vue')
      },
      {
        path: '/photo',
        name: 'Photo',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* webpackChunkName: "photo" */ '../views/Photo.vue')
      }
    ]
    

    两种方式获得id,推荐第二个,前面的配置开启props:true

    <template>
      <div>
        <!-- 方式1: 通过当前路由规则,获取数据 -->
        通过当前路由规则获取:{{ $route.params.id }}
    
        <br>
        <!-- 方式2:路由规则中开启 props 传参 -->
        通过开启 props 获取:{{ id }}
      </div>
    </template>
    

    嵌套路由

    //layout.vue
    <router-view></router-view>
    
    //router/index.js
    const routes = [
      {
        name: 'login',
        path: '/login',
        component: Login
      },
      // 嵌套路由
      {
        path: '/',
        component: Layout,
        children: [
          {
            name: 'index',
            path: '',
            component: Index
          },
          {
            name: 'detail',
            path: 'detail/:id',
            props: true,
            component: () => import('@/views/Detail.vue')
          }
        ]
      }
    ]
    
    

    编程式导航

    <template>
      <div class="home">
        <div id="nav">
          <router-link to="/">Index</router-link>
        </div>
        <button @click="replace"> replace </button>
    
        <button @click="goDetail"> Detail </button>
      </div>
    </template>
    
    <script>
    export default {
      name: 'Index',
      methods: {
        replace () {
          this.$router.replace('/login')
        },
        goDetail () {
          this.$router.push({ name: 'Detail', params: { id: 1 } })
    //可以通过name来跳转
        }
      }
    }
    </script>
    

    hash模式和history模式区别

    • Hash模式是基于锚点,以及onhashchange事件

    • History模式是基于HTML5中的History API

      • history.pushState() IE10以后才支持
      • history.replaceState()

      history模式如果后端不支持,会出现刷新页面找不到页面的express提醒

      后端express支持的写法

      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')))
      
      // 开启服务器,端口是 3000
      app.listen(3000, () => {
        console.log('服务器开启,端口:3000')
      })
      

      静态文件后端nginx支持的方法

      location / {
      	root html;
      	index index.html index.htm;
      	try_files $uri $uri/ /index.html;
      }
      

    Vue带编译器版本

    会大10K左右,支持template,会把template编译成render函数

    //vue.config.js
    module.exports = {
    	runtimeCompiler: true
    }
    

    如果不用带编译版本的

    render(h){
        return h("a",{
            attrs:{
                href:this.to
            },
            on:{
                click:this.clickhander
            }
        },[this.$slots.default])
    },
    methods:{
        clickhander(e){
            history.pushState({},"",this.to)
            this.$router.data.current=this.to
            e.preventDefault()
        }
    }
    
    //用带编译器版本的写法 template:"<a :href='to'><slot></slot><>"
    
    展开全文
  • VueRouter 原理实现

    2020-12-01 20:07:36
    Vue Router 基础回顾 vue.use()的作用:注册插件VueRouter ... 创建路由规则 routes ...创建路由对象 router,...const router =new VueRouter({ routes }) <!-- 导出这个路由对象 --> export default router 在

    Vue Router 基础回顾

    • vue.use()的作用:注册插件VueRouter
    1. 参数是函数的话,vue.use()调用这个函数来注册组件
    2. 参数是对象的话,vue.use()调用这个对象的 install 方法注册组件
    • 创建路由规则 routes

    • 创建路由对象 router,将路由规则作为参数传递进来

    const router =new VueRouter({
      routes
    })
    <!-- 导出这个路由对象 -->
    export default router
    
    • 在 mian.js 中,这册 router 对象
    new Vue({
      // 注册router对象
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
    
    • 在创建的 vue 实例中传入 router 的作用
    1. 当 vue 实例中没有 router 的时候,vue 实例没有routeroute和router 属性
    2. 当 vue 实例中没有 router 的时候,vue 实例中被注入了routeroute和router 属性
      2.1 $route:路由规则
      2.2 $router:路由对象,提供了路由的相关方法,例如 push
    3. 当我们在某些情况下不方便获取到 vue 市里的route,route时候,可以通过router.currentRoute 属性访问到
      在这里插入图片描述

    动态路由

    • 动态路由在路由规则中,通过一个占位来匹配变化的位置
    • 组件加载的方式:懒加载
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
    
    • 在组件中获取动态参数的方式
    {
       path: '/about/:id', //动态路由
       name: 'About',
       component: () => import(/* webpackChunkName:"about" */ '../views/About.vue'),
       props:true // 开启路由传参
     }
    
    <template>
      <div class="about">
    
        <!-- 方式1: 通过当前路由规则,获取数据 -->
        通过当前路由规则,获取 : {{ this.$route.params.id }}
    
        <!-- 方式2: 路由规则中开启 props 传参 -->
        通过开启 props 获取 : {{ id }}
    
      </div>
    </template>
    <script>
    export default {
      props:["id"]
    };
    </script>
    
    • 优先使用第二种传参方式,组件和路由解绑

    嵌套路由

    • 关于嵌套路由默认首页的问题,可以将默认首页的 path 设置为空串("")
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: Home,
        children: [
          {
            path: '', // 当加载Home页的时候默认显示About内容
            name: 'About',
            component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
          }, {
            path: 'info',
            name: 'info',
            component: () => import(/* webpackChunkName: "info" */ '../views/info.vue')
          }
        ]
      }
    ]
    

    编程式导航

    • push => 记录历史
    • repalce => 替换当前历史记录
    • go 前进/后退

    Hash 和 History 模式的区别

    • 两种模式都是客户端路由的实现方式,当路径发生变化的时候,不会向服务器发送请求,是由 js 监视路由的变化,然后根据不同的地址来渲染不同的内容

    • Hash 模式

      • https://music.163.com/#/playlist?id=3102961863
    • History 模式

      • https://music.163.com/playlist/3102961863

    原理的区别

    • Hash 模式是基于锚点,以及 onhaschange 事件

    • History 模式是基于 HTML5 中的 History API

      • history.pushState() : 路径发生变化,不向服务器发送请求
      • history.replaceState() : 路径发生变化,不向服务器发送请求,只会改变浏览器地址栏中的地址,并将该地址添加到历史记录中,实现客户端路由
    • history.pushState()是 IE10 以后才支持,IE10 以前的浏览器只能使用 Hash 模式

    • 当路径发生变化Hash 模式和 History 都不会向服务器发送请求,但是浏览器刷新的时候,都会向服务器发送方请求(都做了静态资源管理,=>请求 url 和根目录下的静态资源一一对应)

      • Hash 模式:请求更目录下的 index.html,刷新之前的状态在 idnex.html 中通过锚点定位
      • History 模式:请求地址栏对应的静态资源(比如https://music.163.com/playlist/),因为我们我们打包的是单页面应用(index.html),找不到playlist.html页面,浏览器报错

    History 模式

    • History 需要服务器的支持
    • 单页面应用,服务器只存在 index.html,不存在http://www.testurl.com/login 这样的地址会会返回 404=>找不到该页面

    History 模式 -Node.js

    • 在 vue-cli 中,默认是 hash 模式,修改成 history 模式
    const router = new VueRouter({
     mode: "history",
     routes
    })
    

    在这里插入图片描述

    • 在开发环境中 dev-server 做了 history 模式的处理=>当请求的页面在根目录中找不到的时候,就返回 index.html
    • 但是我们打包上线的代码所在的服务器没有处理 history 模式,需要我们手动完成该功能

    Node 服务器的实现

    • 将正常的 vue-cli 创建的项目打包,生成 dist 目录
    $ npm run build
    
    • 创建 node 服务器=>入口文件 app.js

    • 将 dist 目录下的文件拷贝到 web 目录(网站根目录)下
      在这里插入图片描述

    • 安装第三方模块 express

    $ yarn add express -D
    
    • 因为 node 服务器默认没有对 history 模式进行处理(当请求的页面不存在时,返回根目录中的 index.html 文件),我们需要借助第三方模块来完成该工作=>connect-history-api-fallback

    • 安装 connect-history-api-fallback 模块

    $ yarn add connect-history-api-fallback -D
    
    • 入口文件 app.js(我们先看没处理 history 模式的情形)
    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")
    })
    

    在这里插入图片描述

    • 切换到 about 页面,刷新
      在这里插入图片描述

    • 改建:服务器处理 history 模式

    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")
    })
    

    当我们在 about 页面刷新的时候,会向服务器请求 about.html 的静态资源,但是服务器根目录下没有改资源,由于 connect-history-api-fallback 模块的作用,当根目录下没有相应的静态资源的时候,返回 index.html,然后在客户端(浏览器),根据路由规则,展示 about 页面的相关内容
    在这里插入图片描述

    History 模式-nginx

    nginx 服务器配置

    • 从官网下载 nginx 的压缩包
    • 把压缩包解压到 c 盘根目录(路径无中文),c:\nginx-1.18.0 文件夹
    • 打开命令行,切换到目录 c:\nginx-1.18.0

    nginx 相关命令

    // 启动-在后台启动服务器,不会阻塞当前命令行
    $ start nginx 
    // 重启
    $ nginx -s reload
    // 停止
    $ nginx -s stop
    

    在这里插入图片描述

    • 将打包好的dist目录下文件拷贝到html目录下
    • 启动nginx
    $ start nginx.exe
    
    • 在about页面刷新,依然报错

    • 改进,修改nginx配置文件
      在这里插入图片描述

    • 重新启动服务器,刷新不报错

    $ nginx.exe -s reload
    

    Vue-router实现原理

    Vue前置知识

    • 插件
    • 混入
    • Vue.observable()
    • 插槽
    • render函数
    • 运行时和完整版的vue

    Hash 模式

    • URL 中的#后面的内容作为路径地址,当#后面的内容发生变化的时候,不会像服务器发送请求,浏览器记录列表新增一条记录
    • 监听hashchange事件
    • 根据当前路由地址找到对应组件重新渲染

    Hsitory 模式

    • 通过history.pushState()方法改变地址栏,该方法仅仅是改变地址栏地址,不会重新向服务器发送请求,而是在浏览器记录中新增一条记录
    • 监听popstate事件,监听浏览器历史操作的变化,在popstate事件中可以记录浏览器改变后的地址,该事件不会因为history.pushState()或者history.replaceState()而出发,只有通过调用history.forward()或者history.back()才能被触发
    • 根据当前路由地址找到对应组件重新渲染

    回顾vue-Router核心代码

    核心代码

    在这里插入图片描述

    vue-Router类图

    属性

    • options
      记录vueRouter类传入的对象
    • data
    1. 响应式对象,当路由地址发生变化,对应的组件要自动更新,该对象有一个 current 属性,用来记录当前路由地址
    2. 将普通对象转换成响应式对象=>Vue.observable()
    • routeMap
      记录路由地址和组件之间的映射关系

    方法

    • Constructor(options):VueRouter
    1. 返回值是一个 VueRouter 实例
    2. 初始化传入的属性和init()方法
    • static install(Vue):void 私有方法
      注册插件
    • init():void
      调用下面三个方法,把不同的代码分割到不同的方法中来实现
    • initEvent():void
      注册popState()事件,监听浏览器历史的变化window.popState()
    • createRouteMap():void
      初始化routeMap属性,把构造函数中传入的路由规则转换成键值对的形式存储到routeMap对象中,键:路由地址 值:对应的组件
    • initComponents(Vue):void
      创建router-link和router-view两个组件

    VueRouter-install方法的实现

    install()参数,参数一:Vue构造函数,参数二:可选,选项配置

    install()的三个目标

    1. 判断当前插件(VueRouter)是否已经被安装
    2. 把Vue构造函数记录到全局变量=>因为当前的install是静态方法,在这个静态方法中我们接收了一个Vue构造函数,而将来我们在VueRouter中的一些实例方法中还要使用这个Vue的构造函数,比如我们创建router-link和router-view组件的时候,需要调用Vue.compoent()方法,所以需要将这个Vue构造函数记录到全局变量上
    3. 把创建的Vue实例时候传入的router对象注入到Vue实例上
      3.1 所有的组件都是Vue的实例
      3.2 我们在组件中使用的this.$router就是在这个时候注入到Vue实例上的

    install()的三个目标的实现

    • install(Vue)中的Vue参数,实际上是Vue.use(VueRouter)传进来的Vue构造函数
      1:判断当前插件是否已经被安装
    export default class VueRouter {
      static install(Vue) {
        /* 1:判断当前插件是否已经被安装:因为install是静态方法,
        也是一个对象,可以在该对象上挂载一个属性 */
        if (VueRouter.install.installed) {
          return
        }
        VueRouter.install.installed = true
      }
    }
    

    2.把Vue构造函数记录到全局变量

    let _Vue= null  //定义一个全局变量_Vue
    export default class VueRouter {
      static install(Vue) {
        if (VueRouter.install.installed) {
          return
        }
        VueRouter.install.installed = true
        //2.把Vue构造函数记录到全局变量
        _vue = Vue
      }
    }
    
    1. 创建的Vue实例时候传入的router对象注入到Vue实例上
      在mian.js入口文件中
    new Vue({
      router, //创建Vue实例的时候传入的router选项
      store,
      render: h => h(App)
    }).$mount('#app')
    

    即我们需要获取Vue构造函数的选项

    
    let _Vue= null
    export default class VueRouter {
      static install(Vue) {
        if (VueRouter.install.installed) {
          return
        }
        VueRouter.install.installed = true
        _Vue = Vue
        //3. 把创建的Vue实例时候传入的router对象注入到Vue实例上
        _Vue.prototype.$router= "Vue构造函数的选项"
      }
    }
    
    • 我们知道每个Vue实例(包含组件)其实都有一个$options选项,就是new Vue()创建Vue实例的选项
    
    let _Vue = null
    export default class VueRouter {
      static install(Vue) {
        if (VueRouter.install.installed) {
          return
        }
        VueRouter.install.installed = true
        _Vue = Vue
        //3. 把创建的Vue实例时候传入的router对象注入到Vue实例上
        _Vue.prototype.$router = this.$options.router // 理想很丰满,显示很骨感,这里的this指向VueRouter,并不是Vue实例
      }
    }
    
    • 引入mixin-混入的概念
    1. 在VueRouter插件中给所有的Vue实例(包含组件)混入一个选项,这个选项中有一个beforeCreate钩子函数,在该函数中我们可以获取到Vue实例,给Vue实例的原型上注入$router属性
    2. 将来所有的组件在加载时都会执行beforeCreate这个钩子函数
    
    let _Vue = null
    export default class VueRouter {
      static install(Vue) {
        if (VueRouter.install.installed) {
          return
        }
        VueRouter.install.installed = true
        _Vue = Vue
        //3. 把创建的Vue实例时候传入的router对象注入到Vue实例上
        // 混入
        _Vue.mixin({
          beforeCreate() {
            // 函数内部的this指向Vue实例
            _Vue.prototype.$router = this.$options.router
          }
        })
      }
    }
    
    1. 所有的组件也是Vue实例,所以加载每个组件的时候,都会执行beforeCreate函数,而我们往Vue原型上挂载router,newVue,(Vue),Vuerouter属性只需要执行一次,在newVue的时候,当加载到相应的组件(Vue实例)时候,Vue实例的原型上已经有router属性,不需要在组件的beforeCreate钩子中重新定义这个$router属性
    2. 改进:Vue实例上执行相关逻辑,组件内部不执行该逻辑=>Vue实例(通过new Vue)的$options有router属性,而组件内部没有
    3. 在创建Vue实例(new Vue)的时候,我们可以通过Vue构造函数的参数取到router,而在组件的this.optionsrouter,data,methods,Vue.prototype.options中是没有router这个选项的,只有组件自己的data,methods等,但在此处_Vue.prototype.router = this.options.router,this.options.router,所以可以在组件中用this.router 取到VueRouter实例
    
    let _Vue = null
    export default class VueRouter {
      static install(Vue) {
        if (VueRouter.install.installed) {
          return
        }
        VueRouter.install.installed = true
        _Vue = Vue
        //3. 把创建的Vue实例时候传入的router对象注入到Vue实例上
        // 混入
        _Vue.mixin({
          beforeCreate() {
            if (this.$options.router) {
              // new Vue()实例执行一次
              _Vue.prototype.$router = this.$options.router
            }
            // 组件内部不执行
          }
        })
      }
    }
    
    • install方法可以看出,Vue.ues(VueRouter)的过程实际上就是将router实例挂载到Vue原型对象上,没给Vue实例否可以访问的到

    VueRouter-Constructor构造函数的实现

    • 初始化options/data/routeMap
    constructor(options) {
      this.options = options //每个Vue实例上都有options选项
      this.routeMap = {} // 路由和组件的映射关系
      this.data = _Vue.observable({
        current: "/" //存储当前的路由地址
      })
    }
    

    VueRouter-createRouteMap方法的实现

    • 初始化routeMap属性,把构造函数中传入的路由规则转换成键值对的形式存储到routeMap对象中,键:路由地址 值:对应的组件
    creatRouteMap() {
      // 遍历所有的路由规则,把路由规则解析成键值对的形式,存储到routeMap中
      this.options.routes.forEach(route => {
        this.routeMap[route.path] = route.component
      })
    }
    

    VueRouter-initComponents方法的实现

    • 创建router-link和router-view两个组件

    Vue的构建版本

    • 运行时版:不支持template模板,需要打包的时候提前编译
    • 完整版:包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板(template)转换成render函数
    • vue-cli默认使用运行时版本,在启用vue-cli服务器的时候,实际上执行打包后的代码dist目录,而里面的代码通过了vue-loader将template编译成render函数,所以可以正常运行,但我们自定义的VueRuter构造器中写入了template语法,因为是js文件,没有相应的loader来讲js文件中的template语法转换成render语法,所以该部分代码会原封不动的打包到dist目录下,而vue-cli默认是运行时版本,对template语法不支持,会报错

    router-link组件

    initComponents(Vue) {
      Vue.component("router-link", {
        props: {
          to: String
        },
        template: '<a :href="to"><slot></slot></a>'
      })
    }
    

    在这里插入图片描述

    • 为什么init方法不再constructor构造函数中调用,是因为我们在闯将router实例的时候(new VueRouter),constructor构造函数执行,如果此时项目中没有Vue实例,这部分操作实际上是没有意义的,所以我们需要在Vue实例化(new Vue)的过程中丰富Router实例的选项(功能)
    • Vue,use(VueRouter)只是往Vue原型对象上挂载了一个router,Vue(newVue)router的空壳,而实例化Vue对象(new Vue)的过程中丰富router的功能(选项)
      完整代码
    
    let _Vue = null
    export default class VueRouter {
      static install(Vue) {
        if (VueRouter.install.installed) {
          return
        }
        VueRouter.install.installed = true
        _Vue = Vue
        _Vue.mixin({
          beforeCreate() {
            if (this.$options.router) {
              _Vue.prototype.$router = this.$options.router
              // 调用router实例的init方法
              this.$options.router.init()
            }
          }
        })
      }
      constructor(options) {
        this.options = options
        this.routeMap = {}
        this.data = _Vue.observable({
          current: "/" //存储当前的路由地址
        })
      }
      init() {
        this.creatRouteMap()
        this.initComponents(_Vue)
      }
      creatRouteMap() {
        // 遍历所有的路由规则,把路由规则解析成键值对的形式,存储到routeMap中
        this.options.routes.forEach(route => {
          this.routeMap[route.path] = route.component
        })
      }
      initComponents(Vue) {
        Vue.component("router-link", {
          props: {
            to: String
          },
          template: '<a :href="to"><slot></slot></a>'
        })
      }
    }
    

    在这里插入图片描述

    • 方案一:使用完整版本vue=>可写template
    • 因为创建router-link组件的时候使用了template,所以Vue的构建版本我们要使用完整版
    module.exports = {
      runtimeCompiler: true
    }
    

    在这里插入图片描述

    • 方案:使用编译版本Vue=>直接写render函数
    1. 为什么我们在写单文件组件的时候可以写template,但在上线的时候还是用的Vue运行时版本=>因为在打包过程中已经将template转换成了render函数
    2. render函数的执行环境是Vue实例,即render函数内部的this指向Vue实例
    initComponents (Vue) {
      Vue.component('router-link', {
        props: {
          to: String
        },
        // template: '<a :href="to"><slot></slot></a>'
        render (h) {
          return h('a', { attrs: { href: this.to } }, [this.$slots.default])
        }
      })
    }
    

    router-view组件

    const self = this
    Vue.component('router-view', {
      render (h) {
        // this指向VueRouter实例
        // 获取当前路由地址,然后找到该路由下的组件
        const component = self.routeMap[self.data.current]
        // h函数会帮助我们将组件-component转换成虚拟dom并返回
        return h(component)
      }
    })
    

    在这里插入图片描述

    • 在单页面应用中,改变路由,我们是不希望刷新浏览器发生请求的,希望在客户端处理路径变化的操作
    1. 超链接(a标签)阻止默认事件
    2. 地址栏路径发生变化,但不会向服务器发送请求,且会产生一个历史记录=>history.pushState()
    3. history.pushState()只会改变地址栏的路径,但该路径下显示的内容还需要我们处理=>把当前地址记录到this.data.current中来,记录当前路由地址,this.data是响应式的数据,当这个数据发行变化的时候自动更新视图,即显示该路径下的内容
    • histort.pushState()参数
    1. data:Object 将来触发window.popState()方法的时候,传给该函数的参数
    2. title:String 网页的标题
    3. url:String 当前超链接要跳转的地址
      在这里插入图片描述
      完整代码
    initComponents (Vue) {
        Vue.component('router-link', {
          props: {
            to: String
          },
          //  注册a标签的事件
          render (h) {
            return h('a', {
              attrs: { href: this.to },
              on: {
                click: this.clickHandler // 注册事件,而不是调用,不要加()
              }
            }, [this.$slots.default])
          },
          methods: {
            clickHandler (e) {
              // 1:改变地址栏地址=>history.pushState()
              history.pushState({}, '', this.to)
              // 2:将当前的路径记录到this.data.current中,此时的this还是VueRouter实例,而该函数的this指向Vue实例,所以this.$router指向的就是VueRouter实例,由于是响应式的,自动触发视图更新-router-link是一个组件,也是一个Vue实例
              this.$router.data.current = this.to
              // 阻止默认行为
              e.preventDefault()
            }
          }
        })
        const self = this
        Vue.component('router-view', {
          render (h) {
            // this指向VueRouter实例
            // 获取当前路由地址,然后找到该路由下的组件
            const component = self.routeMap[self.data.current]
            // h函数会帮助我们将组件-component转换成虚拟dom并返回
            return h(component)
          }
        })
    }
    

    VueRouter-initEvent方法的实现

    • 上面的我们可以正常切换不同的路由而显示不同的内容
      在这里插入图片描述
    • 单击前进/后退,地址路径没有发生变化,且视图也没更新
      在这里插入图片描述
    • window.popState():当历史发生变化的时候触发(点击前进/后退),调用history.pushState()或者history.replaceState()方法是不会触发window.popState()的
    initEvent() {
      // 注册popState事件
      window.addEventListener("popstate", () => {
        // 响应式数据,自动触发视图更新
        this.data.current = window.location.pathname
      })
    }
    

    在这里插入图片描述- 完整代码

    
    let _Vue = null
    export default class VueRouter {
      static install (Vue) {
        if (VueRouter.install.installed) {
          return
        }
        VueRouter.install.installed = true
        _Vue = Vue
        _Vue.mixin({
          beforeCreate () {
            if (this.$options.router) {
              _Vue.prototype.$router = this.$options.router
              // 调用router实例的init方法
              this.$options.router.init()
            }
          }
        })
      }
    
      constructor (options) {
        this.options = options
        this.routeMap = {}
        this.data = _Vue.observable({
          current: '/' // 存储当前的路由地址
        })
      }
    
      init () {
        this.creatRouteMap()
        this.initComponents(_Vue)
        this.initEvent()
      }
    
      creatRouteMap () {
        // 遍历所有的路由规则,把路由规则解析成键值对的形式,存储到routeMap中
        this.options.routes.forEach(route => {
          this.routeMap[route.path] = route.component
        })
      }
    
      initComponents (Vue) {
        Vue.component('router-link', {
          props: {
            to: String
          },
          //  注册a标签的事件
          render (h) {
            return h('a', {
              attrs: { href: this.to },
              on: {
                click: this.clickHandler // 注册事件,而不是调用,不要加()
              }
            }, [this.$slots.default])
          },
          methods: {
            clickHandler (e) {
              // 1:改变地址栏地址=>history.pushState()
              history.pushState({}, '', this.to)
              // 2:将当前的路径记录到this.data.current中,由于是响应式的,自动触发视图更新-router-link是一个组件,也是一个Vue实例
              this.$router.data.current = this.to
              // 阻止默认行为
              e.preventDefault()
            }
          }
        })
        const self = this
        Vue.component('router-view', {
          render (h) {
            // this指向VueRouter实例
            // 获取当前路由地址,然后找到该路由下的组件
            const component = self.routeMap[self.data.current]
            // h函数会帮助我们将组件-component转换成虚拟dom并返回
            return h(component)
          }
        })
      }
    
      initEvent () {
        // 注册popState事件
        window.addEventListener('popstate', () => {
          // 响应式数据,自动触发视图更新
          this.data.current = window.location.pathname
        })
      }
    }
    
    

    history模式和hash模式的兼容

    模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类型,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。

    let _Vue = null
    export default class VueRouter {
      static install (vue) {
        // 1 判断当前插件是否被安装
        if (VueRouter.install.installed) return
        VueRouter.install.installed = true
        // 2 把Vue的构造函数记录在全局
        _Vue = vue
        // 3 把创建Vue的实例传入的router对象注入到Vue实例
        // _Vue.prototype.$router = this.$options.router
        _Vue.mixin({
          beforeCreate () {
            if (this.$options.router) {
              _Vue.prototype.$router = this.$options.router
            }
          }
        })
      }
    
      constructor (options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes
        this.routeMap = {}
        // observable
        this.data = _Vue.observable({
          current: '/'
        })
        this.init()
      }
      init () {
        this.initRouteMap()
        this.initComponent(_Vue)
        this.initEvent()
      }
      initRouteMap () {
        //遍历所有路由信息,把组件和路由的映射记录到 routeMap 对象中
        this.routes.forEach( route => {
          this.routeMap[route.path] = route.component
        })
      }
      initComponent(Vue){
        //创建 router-link 和 router-view 组件
        const self = this
        Vue.component('router-link',{
          props:{
            to:String
          },
          render(h){
            return h("a",{
              attrs:{
                href: self.mode === "history" ? "" : "#" + this.to
              },
              on:{
                click:self.mode === "history" ? this.clickhander : this.hashClickhander
              }
            },[this.$slots.default])
          },
          methods:{
            clickhander(e){
              history.pushState({},"",this.to)
              this.$router.data.current=this.to
              e.preventDefault()
            },
            hashClickhander(e){
              this.$router.data.current=this.to
            }
          }
          // template:"<a :href='to'><slot></slot><>"
        })
    
        Vue.component('router-view',{
          render(h){
            let cm = self.routeMap[self.data.current]
            return h(cm)
          }
        })
      }
      initEvent(){
        //注册 popstate 事件,当路由地址发生变化,重新记录当前的路径
        const eventType = this.mode === 'history' ? 'popstate' : 'hashchange'
        const handle = this.mode === 'history' ? this.getLocation : this.getHash
        window.addEventListener(eventType,handle.bind(this))
      }
      getLocation(){
        this.data.current = window.location.pathname
      }
      getHash(){
        let href = window.location.href
        const index = href.indexOf('#')
        if (index < 0) return '/'
        this.data.current = href.slice(index + 1)
      }
    }
    
    
    展开全文
  • 手写vue-router核心原理一、...基于 history 实现三、基于Vue实现VueRouter四、剖析VueRouter本质五、分析Vue.use六、完善install方法七、完善VueRouter类八、完善$route九、完善router-view组件十、完善router-link...
  • 它本质上是通过在生成的标签上绑定了click事件,然后执行对应的VueRouter实例的push()实现的,对于router-link组件来说,可以传入以下props: to 表示目标路由的链接,当被点击后,内部会立刻把to的值传到router....
  • 浅析vue-router原理

    2020-10-17 20:46:27
    主要围绕Vue的SPA单页面设计展开。SPA(single page application):单一页面应用程序,有且只有一个完整的页面,对vue router原理感兴趣的朋友跟随小编一起看看吧
  • 手写vue-router核心原理一、...基于 history 实现三、基于Vue实现VueRouter四、剖析VueRouter本质五、分析Vue.use六、完善install方法七、完善VueRouter类八、完善$route九、完善router-view组件十、完善router-link...
  • vue的路由,既熟悉又陌生,堪称是最熟悉的陌生人,我们在平时的开发中一直在用,但是对其原理却知道的非常少,那么他到底是如何实现切换...所以想要了解vue-router原理一切都要从这个new Router()创建实例开始。很明
  • VueRouter 实现原理

    2021-01-31 11:31:47
    export default class VueRouter{ static install(Vue){ // 1.判断当前插件是否已安装 if(VueRouter.install.installed){ return } VueRouter.install.installed = true // 2.把Vue构造函数记录到全局变量 _...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,459
精华内容 583
关键字:

vuerouter原理

vue 订阅