实现简单的vue-router

1.使用vue-cli创建一个配置Router,且是history模式的项目,可以看到使用vue-router的相关代码:

// route-demo/src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // 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: "about" */ '../views/About.vue')
  }
]

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

export default router
// route-demo/src/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')

2.Vue.use(VueRouter)

在Vue源码中可以看到,它的的use方法会调用传入对象的install方法,并把Vue的构造函数传递给install方法,plugin.install(app, ...options)调用的方式说明install是个静态方法

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
	const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,
      version,
      get config() {
        return context.config
      },
      set config(v) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          )
        }
      },
	use(plugin: Plugin, ...options: any[]) {
        if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          installedPlugins.add(plugin)
          plugin(app, ...options)
        }
        return app
      },
}

在src下新建个vuerouter文件夹,然后新建index.js文件, 先实现install方法:

// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
  static install(Vue) {
    // 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
    if (VueRouter.install.installed) return
    VueRouter.install.installed = true
    // 全局变量保存Vue构造函数,后面要用
    _Vue = Vue
  }
}

在实际代码中,我们会经常使用$router访问router对象,可以在install方法中把创建Vue实例时传入的router对象注入到Vue实例,获取Vue实例的可以使用混入beforeCreate方法,在beforeCreate方法中可以拿到传入的router对象,混入后组件和实例都会执行该赋值,我们只需要赋值一次,所以判断一下是否存在options上是否存在router对象,存在说明是Vue实例

// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
  static install(Vue) {
    // 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
    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.接着看到以下代码:

// route-demo/src/router/index.js
const router = new VueRouter({
  mode: 'history',
  routes
})

查看VueRouter的实现,可以看到

export declare class VueRouter {
  constructor(options?: RouterOptions)
  app: Vue
  /**
   * Original options object passed to create the Router
   */
  options: RouterOptions
  /**
   * Configured mode when creating the Router instance.
   */
  mode: RouterMode
  /**
   * Current {@link Route}
   */
  currentRoute: Route
  // ... 一些操作路由的方法以及路由守卫,先不关注
}

实现VueRouter的构造方法:

// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
  static install(Vue) {
    // 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
    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
        }
      }
    })
  }

  constructor (options) {
    // 记录参数对象
    this.options = options
    // 记录路径和组件的对应关系
    this.routeMap = {}
    // 定义为响应式变量
    this.data = _Vue.observable({
      // 当前访问路径, 默认是'/'
      current: '/'
    })
  }
}

4.定义一个createRouteMap方法,解析传入的路由规则routes,并赋值给routeMap:

// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
  static install(Vue) {
    // 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
    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
        }
      }
    })
  }

  constructor (options) {
    // 记录参数对象
    this.options = options
    // 记录路径和组件的对应关系
    this.routeMap = {}
    // 定义为响应式对象
    this.data = _Vue.observable({
      // 当前访问路径, 默认是'/'
      current: '/'
    })
  }

  createRouteMap () {
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }
}

5.初始化组件方法initComponents方法,这里只实现router-view和router-link, 完了以后在beforeCreate方法中调用两个初始化方法:

// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
  static install(Vue) {
    // 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
    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()
        }
      }
    })
  }

  constructor (options) {
    // 记录参数对象
    this.options = options
    // 记录路径和组件的对应关系
    this.routeMap = {}
    // 定义为响应式对象
    this.data = _Vue.observable({
      // 当前访问路径, 默认是'/'
      current: '/'
    })
  }

  init () {
    this.createRouteMap()
    this.initComponents(_Vue)
  }

  createRouteMap () {
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

  initComponents (Vue) {
    Vue.component('router-link', {
      props: {
        to: String
      },
      render(h) {
        // h函数生成虚拟DOM,第一个参数是元素选择器,这里使用标签,
        // 第二个参数是属性设置,第三个参数是子元素,这里直接取标签中间的值,使用默认插槽的方式获取
        return h('a', {
          attrs: {
            href: this.to
          }
        },[this.$slots.default]
        )
      }
    })
    // 这里需要定义一个self变量保存this,指向VueRouter实例
    let self = this
    Vue.component('router-view', {
      render(h) {
        // 根据当前路径找到对应的组件,并使用h函数直接调用后返回
        return h(self.routeMap[self.data.current])
      }
    })
  }
}

更换route-demo/src/router/index.js中引用VueRouter的路径,改为自己定义的vuerouter文件

6.可以看到切换页面时,只有地址栏发生了变化,内容没有响应,现在在router-link定义时添加点击事件

let _Vue = null
export default class VueRouter {
  static install(Vue) {
    // 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
    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()
        }
      }
    })
  }

  constructor (options) {
    // 记录参数对象
    this.options = options
    // 记录路径和组件的对应关系
    this.routeMap = {}
    // 定义为响应式对象
    this.data = _Vue.observable({
      // 当前访问路径, 默认是'/'
      current: '/'
    })
  }

  init () {
    this.createRouteMap()
    this.initComponents(_Vue)
  }

  createRouteMap () {
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

  initComponents (Vue) {
    Vue.component('router-link', {
      props: {
        to: String
      },
      render(h) {
        // h函数生成虚拟DOM,第一个参数是元素选择器,这里使用标签,
        // 第二个参数是属性设置,第三个参数是子元素,这里直接取标签中间的值,使用默认插槽的方式获取
        return h('a', {
          attrs: {
            href: this.to
          },
          // 事件也属于属性
          on: {
            click: this.clickhandle
          }
        },[this.$slots.default]
        )
      },
      methods: {
        clickhandle(e) {
          // 使用pushState仅改变路径栏的地址
          history.pushState({}, "", this.to)
          // 将当前路径变更
          this.$router.data.current = this.to
          // 阻止默认响应
          e.preventDefault()
        }
      },
    })
    // 这里需要定义一个self变量保存this,指向VueRouter实例
    let self = this
    Vue.component('router-view', {
      render(h) {
        // 根据当前路径找到对应的组件,并使用h函数直接调用后返回
        return h(self.routeMap[self.data.current])
      }
    })
  }
}

访问页面,可以看到点击链接可以正常切换

但是浏览器前进/后退键还是不生效,这时需要监听popstate,改变current的值

// route-demo/src/vuerouter/index.js
let _Vue = null
export default class VueRouter {
  static install(Vue) {
    // 判断插件是否已经安装,因为方法或函数本身也是一个对象,所以我们可以在VueRouter.install定义一个属性installed,表示是否已经安装
    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()
        }
      }
    })
  }

  constructor (options) {
    // 记录参数对象
    this.options = options
    // 记录路径和组件的对应关系
    this.routeMap = {}
    // 定义为响应式对象
    this.data = _Vue.observable({
      // 当前访问路径, 默认是'/'
      current: '/'
    })
  }

  init () {
    this.createRouteMap()
    this.initComponents(_Vue)
    this.initEvent()
  }

  createRouteMap () {
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

  initComponents (Vue) {
    Vue.component('router-link', {
      props: {
        to: String
      },
      render(h) {
        // h函数生成虚拟DOM,第一个参数是元素选择器,这里使用标签,
        // 第二个参数是属性设置,第三个参数是子元素,这里直接取标签中间的值,使用默认插槽的方式获取
        return h('a', {
          attrs: {
            href: this.to
          },
          // 事件也属于属性
          on: {
            click: this.clickhandle
          }
        },[this.$slots.default]
        )
      },
      methods: {
        clickhandle(e) {
          // 使用pushState仅改变路径栏的地址
          history.pushState({}, "", this.to)
          // 将当前路径变更
          this.$router.data.current = this.to
          // 阻止默认响应
          e.preventDefault()
        }
      },
    })
    // 这里需要定义一个self变量保存this,指向VueRouter实例
    let self = this
    Vue.component('router-view', {
      render(h) {
        // 根据当前路径找到对应的组件,并使用h函数直接调用后返回
        return h(self.routeMap[self.data.current])
      }
    })
  }

  initEvent() {
    window.addEventListener('popstate', () => {
      this.data.current = window.location.pathname
    })
  }
}

这时,点击浏览器的前进/后退按键,内容也跟着变化。

源码见:[https://gitee.com/caicai521/vue-study/tree/master/route-demo](

posted @ 2022-10-14 09:23  菜菜123521  阅读(60)  评论(0编辑  收藏  举报