vue2源码学习(1) - 手写 vue-router

1. 初始化项目

先使用 vue 脚手架(这里使用的是 vue-cli 2)初始化项目,执行命令 vue init webpack demo01-vue-router,生成的目录结构如下:

node 复制代码
├── buile
│   ├── build.js
│   ├── check-versions.js
│   ├── logo.png
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   └── webpack.prod.conf.js
├── config
│   ├── dev.env.js
│   ├── index.js
│   ├── prod.env.js
│   └── test.env.js
├── node_modules
├── src
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── router
│   │   └── index.js
│   ├── App.vue
│   └── main.js
├── static
│   └── .gitkeep
├── test
│   ├── e2e
│   │   ├── custom-assertions
│   │   │   └── elementCount.js
│   │   ├── specs
│   │   │   └── test.js
│   │   ├── nightwatch.conf.js
│   │   └── runner.js
│   └── unit
│       ├── specs
│       │   └── HelloWorld.spec.js
│       ├── .eslintrc
│       ├── jest.conf.js
│       └── setup.js
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── index.html
├── package.json
└── README.md

1.1 src/router/index.js

js 复制代码
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export default new Router({
  // 路由映射表
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    }
  ]
})

其中 Vue.use(Router) 作用是引入插件,并调用了 vue-router 插件的 install

1.2 src/main.js

js 复制代码
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router, // 传入了路由器实例给根实例作为组件选项
  components: { App },
  template: '<App/>'
})

思考两个问题:

  • 1、为什么可以组件中可以直接使用 router-linkrouter-view 实现路由跳转?(router-view 路由的出口,承载内容的容器)
  • 2、为什么组件中能使用 this.$router 访问路由器的实例,并能通过 this.$router.push 进行命令式的路由跳转?
html 复制代码
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
js 复制代码
this.$router.push('/')
this.$router.push('/about') 

能直接使用说明进行了全局注册,那在哪里注册的呢?我们来看一下 vue-router 的 install 方法:

在 vue-router 的 install 方法中做了什么呢?

  1. 在 Vue 构造函数的原型上添加 <math xmlns="http://www.w3.org/1998/Math/MathML"> r o u t e r ,这样可以在所有的子组件中使用 t h i s . router,这样可以在所有的子组件中使用 this. </math>router,这样可以在所有的子组件中使用this.router
  2. 注册了两个全局组件:router-link 和 router-view

手写 vue-router 需要实现上面两件事,

2. 需求分析

  • 1、SPA 页面路由发生变化,但是页面不能刷新
    • hash: #/about
    • History API: /about
  • 2、根据路由的变化页面显示对应的内容
    • router-view
    • 数据响应式:定义一个变量 curr 持有 url 地址,当变量发生变化时动态的重新执行 render

实现步骤如下

  • 创建并导出 VueRouter 类
    • 处理路由选项
    • 监听 url 变化,hashChange
    • 响应变化
  • 实现 install 方法
    • 在 Vue 原型上添加 $router
    • 全局注册组件:router-link 和 router-view

3. 开始

在初始化项目中,默认生成了路由配置文件 src/router/index.js,现在我们重新创建我们的路由配置文件及插件 vue-router 的文件:

  • src/myRouter/my-vue-router.js
    • 实现 vue 插件 VueRouter
      • 插件必须有一个静态的 install 方法
        • 注册 $router
        • 注册全局组件
  • src/myRouter/index.js
    • 正常的路由配置文件,内容同 src/router/index.js
    • 其中 import Router from 'vue-router' 改为 import Router from 'my-vue-router'

在 main.js 中修改相应的引用:

js 复制代码
import router from './router'
// 改为 
import router from './myRouter'

3.1 编写 my-vue-router.js

js 复制代码
let Vue

class VueRouter {
  constructor(options){
    this.$options = options;

    // 缓存path和route映射关系
    this.routeMap = {};
    this.$options.routes.forEach(item => {
      this.routeMap[item.path] = item;
    })

    Vue.util.defineReactive(this, 'current', window.location.hash.slice(1) || '/')
    window.addEventListenner('hashchange', () => {
      this.current = window.location.hash.slice(1)
    })
    window.addEventListenner('load', () => {
      this.current = window.location.hash.slice(1)
    })
  }
}

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

  // 注册 $router
  Vue.mixin({
    beforeCreate(){
      if(this.$options.router) {
        Vue.prototype.$router = this.$options.router 
      }
    }
  })

  // 注册全局组件 router-link
  Vue.component('router-link', {
    props: {
      to: {
        type: String,
        required: true,
      },
    },
    render(h) {
      return h('a', {
        attrs: {
          href: '#' + this.to
        },
      }, [this.$slots.default]);
    },
  })

  // 注册全局组件 router-view
  Vue.component('router-view', {
    render(h) {
      const { routeMap, current } = this.$router;
      const component = routeMap[current] ? routeMap[current].component : null;
      return h(component);
    }
  })
}

export default VueRouter;

3.2 对 my-vue-router.js 内容进行简单解说

1、一个插件的的基本结构如下:

js 复制代码
class VueRouter {}
VueRouter.install = function(Vue){
 // do something
};

export default VueRouter;

2、文件中定义的全局变量 Vue:

js 复制代码
let Vue

用于存放 Vue 构造函数,方便使用,这样使用 vue 不需要通过 import 形式引入,打包的时候就不会把 vue 打包进去。

3、类的构造函数:

js 复制代码
  constructor(options){
    this.$options = options;

    // 缓存path和route映射关系
    this.routeMap = {};
    this.$options.routes.forEach(item => {
      this.routeMap[item.path] = item;
    })

    Vue.util.defineReactive(this, 'current', window.location.hash.slice(1) || '/')
    window.addEventListenner('hashchange', () => {
      this.current = window.location.hash.slice(1)
    })
    window.addEventListenner('load', () => {
      this.current = window.location.hash.slice(1)
    })
  }

先返回来看看 src/myRouter/index.js 中实例化 VueRouter 的部分:

js 复制代码
export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    }
  ]
})

构造函数 constructor 中的参数 options 就是通过 new Router 实例化 VueRouter 时传入的参数,并将传入的选项保存到 VueRouter 实例中。

通过 Vue.util.defineReactive 创建一个响应式数据 current,当 current 的值发生变化的时候,与它相关的函数会重新执行。

window.addEventListenner('hashchange', () => { }) 监听url中 hash 的变化。

4、VueRouter.install

我们再回来看看 src/myRouter/index.js

js 复制代码
Vue.use(Router)

Vue.use 的作用是引入插件,并调用了插件的 install方法,这里 VueRouter.install 方法的 形参1 是 Vue 的构造函数。

  • 挂载 $router
js 复制代码
  Vue.mixin({
    beforeCreate(){
      if(this.$options.router) {
        // 将路由器实例赋值给了Vue原型
        Vue.prototype.$router = this.$options.router 
      }
    }
  })

因为 $router 要添加到 Vue 构造函数的原型上需要确保 Vue 已实例化,也就是说将 $router 添加到 Vue 原型上的操作需要延迟到未来的某个时刻(Vue实例创建之时),这里使用 Vue 的全局混入,通过钩子函数 beforeCreate 来实现这个延迟。

【注意】因为 vue-router 与 vue 是强耦合的关系,所以我们可以使用 Vue.mixin

4、注册全局组件 router-link

我们通常使用 router-link 是这样的:<router-link to="/about">XXX</router-link>,不难发现,跟我们平时开发过程中封装的组件使用是一样的。这里使用 Vue.component 创建组件:

js 复制代码
  Vue.component('router-link', {
    props: {
      to: {
        type: String,
        required: true,
      },
    },
    render(h) {
      return h('a', {
        attrs: {
          href: '#' + this.to
        },
      }, [this.$slots.default]);
    },
  })

this.$slots.default 获取当前组件的默认插槽,vue slot插槽的相关使用可以查阅:vue slot插槽

【注意】:为什么这里使用的是:

js 复制代码
Vue.component('router-link', {
  render: {}
})

而不是:

js 复制代码
Vue.component('router-link', {
  template: ''
})

原因可参考:Vue 中的 Runtime + Compiler 和 Runtime-only 的简单理解

5、注册全局组件 router-view

js 复制代码
  Vue.component('router-view', {
    render(h) {      
      const { routeMap, current } = this.$router;
      const component = routeMap[current] ? routeMap[current].component : null;
      return h(component);
    }
  })

这里的 this 指的是 Vue 实例,this.$router 指的是 VueRouter 实例。

相关推荐
cy玩具19 分钟前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v2 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫2 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.2 小时前
Chrome调试工具(查看CSS属性)
前端·chrome