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 实例。

相关推荐
范文杰2 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪3 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪3 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy3 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom4 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom4 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom4 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom4 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom4 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试
LaoZhangAI5 小时前
2025最全GPT-4o图像生成API指南:官方接口配置+15个实用提示词【保姆级教程】
前端