Vue-Router原理实现

本文详细介绍了Vue-Router的使用步骤、动态路由、路由规则配置、编程式导航、模式与模式的区别以及原理实现。重点讲解了模式的原理,包括监听事件、地址栏变化与组件渲染,并模拟实现了Vue-Router的简单版本。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Vue-Router的使用步骤

  1. vue项目中使用vue-router的步骤:

    1. 安装vue-router

    2. 创建router对象

      1. 使用 Vue.use注册vue-router组件
      2. 定义路由规则
      3. 根据路由规则创建 router 对象
      import Vue from "vue";
      import VueRouter from "vue-router";
      
      // Vue.use注册组件,如果传入的是一个方法,则调用该方法注册组件,如果传入的是一个对象,则调用对象的install方法来注册组件
      Vue.use(VueRouter);
      
      // 路由规则
      const routes = [
        {
          path: "/index",
          name: "index",
          component: () =>
            import(/* webpackChunkName: "index" */ "../views/Index.vue")
        },
        {
          path: "/blog",
          name: "blog",
          component: () => import(/* webpackChunkName: "blog" */ "../views/Blog.vue")
        },
        {
          path: "/photo",
          name: "photo",
          component: () =>
            import(/* webpackChunkName: "photo" */ "../views/Photo.vue")
        }
      ];
      
      // 创建 router 对象
      const router = new VueRouter({
        routes
      });
      
      export default router;
      
    3. 创建Vue实例时,将router对象传入

      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");
      
  2. 动态路由

    1. 两种实现方式
      1. 在路由规则中通过:paramName的形式声明动态路由参数,然后再组件中通过 $route.params.paramName来访问参数。

        // 路由规则配置
        {
          name: "blog",
          path: "/blog/:id",
          component: () => import(/* webpackChunkName: "blog" */ "../views/Blog.vue")
        }
        // 组件中使用
        <template>
          <div>Blog{{ $route.params.id }}</div>
        </template>
        
        <script>
        export default {
          name: 'blog'
        }
        </script>
        
      2. 在路由规则中通过:paramName的形式声明动态路由参数,设置props:true,route.params 将会被设置为组件属性。在组件中就可以组件属性的方式访问路由参数

        // 路由规则配置
        {
          name: "photo",
          path: "/photo/:num",
          // props 向路由组件传参,可以是一个Boolean,对象,或函数
          props: true, // 为true时,route.params 将会被设置为组件属性。
          component: () =>
            import(/* webpackChunkName: "photo" */ "../views/Photo.vue")
        }
        // 组件中使用
        <template>
          <div>Photo {{ num }}</div>
        </template>
        
        <script>
        export default {
          name: 'Photo',
          props: ['num']
        }
        </script>
        
    2. 路由规则配置中props的3种情况
      1. propstrueroute.params 将会被设置为组件属性

      2. props是一个对象时,会按原样设置为组件属性

        // 路由规则配置
        {
            name: "index",
            path: "/index",
            // props 向路由组件传参,可以是一个Boolean,对象,或函数
            props: { a: 12, b: 32 }, // props是一个对象时,会按原样设置为组件属性
            component: () =>
              import(/* webpackChunkName: "index" */ "../views/Index.vue")
          }
        // 组件中使用
        <template>
          <div>Index {{a}} {{b}}</div>
        </template>
        
        <script>
        export default {
          name: 'index',
          props: [
            'a',
            'b',
          ]
        }
        </script>
        
      3. props是一个函数时,会按原样设置为组件属性

        // 路由规则配置
        {
            name: "index",
            path: "/index",
            props: route => {
              console.log(route);
              return { a: 12, b: 32 };
            }, // props是一个函数时,会将函数的返回值作为props的值
            component: () =>
              import(/* webpackChunkName: "index" */ "../views/Index.vue")
          }
        // 组件中使用
        <template>
          <div>Index {{a}} {{b}}</div>
        </template>
        
        <script>
        export default {
          name: 'index',
          props: [
            'a',
            'b',
          ]
        }
        </script>
        
  3. 路由嵌套

    1. 在路由规则中配置children

    2. 在父组件中添加<router-view>标签

      // 嵌套路由
      const routes = [
        {
          name: "index",
          path: "/index/:id",
          component: () =>
            import(/* webpackChunkName: "index" */ "../views/Index.vue"),
          children: [
            {
              path: "blog",
              component: () =>
                import(/* webpackChunkName: "blog" */ "../views/Blog.vue"),
              children: [
                {
                  path: "photo",
                  component: () =>
                    import(/* webpackChunkName: "photo" */ "../views/Photo.vue")
                }
              ]
            },
            {
              path: "",
              component: () =>
                import(/* webpackChunkName: "photo" */ "../views/Photo.vue")
            }
          ]
        }
      ];
      // index.vue
      <template>
        <div>
          <div>Index {{ $route.params.id }}</div>
          <router-view></router-view>
        </div>
      </template>
      
      <script>
      export default {
        name: 'index',
      }
      </script>
      // blog.vue
      <template>
        <div>
          <div>Blog{{ $route.params.id }}</div>
          <router-view></router-view>
        </div>
      </template>
      
      <script>
      export default {
        name: 'blog'
      }
      </script>
      
  4. 编程式的导航

    1. 借助 router 的实例方法,通过编写代码来实现导航,在 Vue 实例内部,你可以通过 $router 访问路由实例。

    2. router.push(location, onComplete?, onAbort?):导航到不同的 URL,会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL

      // 字符串
      router.push('home')
      
      // 对象
      router.push({ path: 'home' })
      
      // 命名的路由,name必须是路由规则中存在的,这里的params可以在组件中通过$route.params访问
      router.push({ name: 'user', params: { userId: '123' }})
      
      // 带查询参数,变成 /register?plan=private
      router.push({ path: 'register', query: { plan: 'private' }})
      

      如果提供了 pathparams 会被忽略

      const userId = '123'
      router.push({ name: 'user', params: { userId }}) // -> /user/123
      router.push({ path: `/user/${userId}` }) // -> /user/123
      // 这里的 params 不生效
      router.push({ path: '/user', params: { userId }}) // -> /user
      
    3. router.replace(location, onComplete?, onAbort?):跟 router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是替换掉当前的 history 记录。

    4. router.go(n):这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)

  5. hash模式和history模式的区别
    hash模式和history模式都是客户端路由的实现方式,当路由发生变化时,js会监听路由变化,展示不同的内容,不会向服务器发送请求。

    1. 表现形式的区别
      1. hash模式路径中带有##后面的内容是路由的地址,可以通过?来携带参数。
        https://abs.cs.com/#/detail?id=123
        
      2. history模式是正常的路径,不带有与路径无关的字符。需要服务器端的支持。
        https://abs.cs.com/detail/123
        
    2. 原理的区别
      1. hash模式是基于锚点以及onhashchange事件。锚点的值作为路由地址,当路由发生变化时,会触发onhashchange事件。
      2. history模式是基于H5中的History API。IE10以后才支持
        1. history.pushState(),不会向服务器发送请求,只会改变地址栏中的地址,并将其保存到历史记录里。
        2. history.replaceState()
    3. history模式的使用
      1. history模式需要服务器的支持。单页应用中,服务端不存在http://demo.com/login这样的地址,会返回找不到页面。所以在服务器端应该配置除了静态资源外都返回单页应用的index.html
      2. history模式的开启,需要在创建router对象时,将mode:'history'传入。
        // 创建 router 对象
        const router = new VueRouter({
          mode: "history",
          routes
        });
        
      3. node服务器中添加history模式支持,使用express开发一个简单的node服务器,代码如下:dist是项目打包生成的代码目录
        const path = require("path");
        // 导入处理 history 模式的模块
        const history = require("connect-history-api-fallback");
        // 导入 express
        const express = require("express");
        
        const app = express();
        // 注册处理history模式的中间件
        app.use(history());
        //处理静态资源的中间件,网站根目录 dist
        app.use(express.static(path.join(__dirname, "dist")));
        
        // 开启服务器
        app.listen(3000, () => {
          console.log("服务器启动,端口:3000");
        });
        
      4. nginx服务器中开启对history模式的支持。需要在nginx的配置文件中的location中添加try_files $uri $uri/ /index.html;
        location / {
            root   html;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
        
        这行代码的意思是尝试从$uri 中查找文件,其中$uri 表示当前请求的路径。如果找不到则从$uri/(表示当前q请求的目录)中查找index.htmlindex.htm,如果还是找不到,则返回根目录的index.html

Vue-Router的原理实现

  1. VueRouter原理分析
    1. hash模式
      1. URL#后面的部分作为路径地址
      2. 监听hashchange事件,#后面部分变化时会触发该事件
      3. 根据当前路由地址找到对应组件重新渲染
    2. history模式
      1. 通过history.pushState方法改变地址栏,不会向服务器发送请求,只会改变地址栏,并将地址保存到历史中。
      2. 监听popstate事件来监听历史地址的变化,在该事件的回调函数中记录改变的历史。pushStatereplaceState方法不会触发该事件,只有点击浏览器的前进或后退按钮,调用backforwardgo时才会触发。
      3. 根据当前路由地址找到对应组件重新渲染
  2. VueRouter模拟实现,以history模式为例
    1. 首先根据VueRoute的使用分析,可以通过new关键字创建实例对象,所以VueRoute必须是一个类或函数。这里以类的方式实现。

    2. 确定VueRoute为一个类之后,由于使用Vue.use时传入了一个对象,所以该对象中必须包含install方法,因此类中应该包含一个install静态方法。
      该方法接收两个参数,第一个参数是Vue的构造函数,第二个参数是Vue.use安装插件时传入的可选的选项对象。
      install方法需要完成3件事:

      1. 判断是否已经安装过插件,避免重复安装

      2. 保存Vue构造函数,后面实例方法中需要用到,生成router-linkrouter-view组件时需要用到

      3. 将创建Vue实例时传入的router对象注入到所有的Vue实例中。所有的组件都是Vue 的实例。

        1. 首先要能获取到Vue实例创建时传入的router, 通过注册全局mixin为所有组件注册一个beforeCreate生命周期函数,在函数内部可以通过this.$options.router访问到传入的router
        2. 其次需要所有Vue的实例都能访问到,在Vueprototype中定义$router来保存router对象。
        3. 最后beforeCreate会调用多次,,只有传入了router对象才需要设置
        let _Vue = null;
        
        export default class VueRouter {
          /**
           *
           * @param {*} Vue Vue构造函数
           * @param {*} option Vue.use时传入的选项参数
           */
          static install(Vue) {
            // 1.判断是否已经安装过,如果已经安装过,则直接返回,避免重复安装
            if (VueRouter.install.installed) {
              return;
            }
            VueRouter.install.installed = true;
            // 保存Vue
            _Vue = Vue;
            // 将Vue实例创建时传入的router对象注入到所有Vue实例中。
            _Vue.mixin({
              beforeCreate() {
                if (this.$options.router) {
                  _Vue.prototype.$router = this.$options.router;
                }
              }
            });
          }
        }
        
    3. 实现构造函数

      1. 首先要保存调用构造函数时传入的配置对象,这里保存在实例属性options
      2. 初始化保存当前路由的对象data,该对象需要是一个响应式的对象,当路由变化时,需要自动加载组件。
      3. 保存options中传入的规则配置中的路径到展示组件的映射,保存为一个routeMap对象,以路径为键,路径对应展示的组件为值。调用构造函数时需要初始化为一个空对象。
      constructor(options) {
        // 保存配置对象
        this.options = options;
        // 初始化保存当前路由的对象data为一个响应式的对象
        this.data = _Vue.observable({ current: "/" });
        // 初始化保存路径和展示组件映射关系的对象。
        this.routeMap = {};
      }
      
    4. 创建initRouteMap方法,生成路由配置规则中的路径和组件的映射关系,并保存到routeMap对象中。

        initRouteMap() {
          this.options.routes.forEach(route => {
            this.routeMap[route.path] = route.component;
          });
        }
      
    5. 创建router-linkrouter-view组件的initComponent方法

      1. Vue-cli默认使用未包含编译器的vue版本,在创建组件时不能使用template模板,要想使用包含编译器的vue,需要在根目录下添加配置文件vue.config.js,并将runtimeCompiler设置为true

        module.exports = {
          runtimeCompiler: true
        };
        
      2. 如果不想使用带有编译器版本的vue(因为带有编译器大小会大10Kb左右),可以使用render函数来替换template

      3. 创建router-link组件,router-link组件接收一个to属性,并渲染一个a标签。点击router-link组件时,不刷新浏览器,改变地址栏地址并渲染对应视图。

        1. 通过props接收to属性的值
        2. render函数中使用h创建a标签,并通过this.$slots.default来获取子元素内容
        3. a标签添加点击事件,e.preventDefault来阻止默认行为,防止从服务器获取内容浏览器刷新,然后调用window.history.pushSate来改变地址栏地址,并记录到历史中。然后更新data.current的值。
        Vue.component("router-link", {
         props: {
            to: String
          },
          methods: {
            aClick(e) {
              e.preventDefault();
              window.history.pushState({}, "", this.to); // 修改地址栏中的地址,并将地址存入到历史中
              _this.data.current = this.to; // 修改记录中的本地地址,并触发视图更新
            }
          },
          render(h) {
            return h(
              "a",
              {
                attrs: {
                  href: this.to
                },
                on: {
                  click: this.aClick
                }
              },
              [this.$slots.default]
            );
          }
        });
        
      4. 创建router-view组件,router-view功能相对较简单,只是展示当前路径对应的视图。使用data中的current来获取当前的路由,data是一个响应式对象,所以data中的属性值变化时视图也会随即改变。

        Vue.component("router-view", {
          render(h) {
            const component = _this.routeMap[_this.data.current];
            return h(component);
          }
        });
        
    6. 创建initEvent函数,注册popstate事件监听
      popstate事件监听是为了当通过非router-link点击及组件提供的方法改变路由地址的情况下及时更新视图。只要在回调函数中改变更新data.current为最新的路由地址即可,响应式的data会自动触发视图的更新。

      initEvent() {
        window.addEventListener("popstate", () => {
          this.data.current = window.location.pathname; // 将当前地址保存到响应式对象中,以此触发视图更新
        });
      }
      
    7. initRouteMap,initComponent,initEvent方法包装到一个init方法中,关于init方法在何时执行,init执行时必须已经保存了Vue构造函数,initComponent执行时需要,并且需要已经完成构造函数的调用,initRouteMap需要调用构造函数时传入的路由规则。综上init应该在混入的beforeCreate函数中调用。

      static install(Vue) {
          // 1.判断是否已经安装过,如果已经安装过,则直接返回,避免重复安装
          if (VueRouter.install.installed) {
            return;
          }
          VueRouter.install.installed = true;
          // 保存Vue
          _Vue = Vue;
          // 将Vue实例创建时传入的router对象注入到所有Vue实例中。
          _Vue.mixin({
            beforeCreate() {
              if (this.$options.router) {
                _Vue.prototype.$router = this.$options.router;
                this.$options.router.init(); //通过保存的VueRouter实例来调用init方法,来完成初始化
              }
            }
          });
        }
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值