Vue全家桶简单实现 01

167 阅读2分钟

第一次发文,来自 vue 学习记录,后续持续更新学习内容,鞭策自己。加油鸭!

vue-router

它和Vue.js的核心深度集成,让构建单页面应用变的易如反掌。它并不能用在其他库中,比如react。

核心步骤:

  • 使用vue-router插件,router.js

    import Router from 'vue-router' Vue.use(Router)

  • 创建Router实例,router.js

    export default new Router({...})

  • 在根组件上添加该实例,main.js

    import router from './router' new Vue({ router, }).$mount("#app");

  • 添加路由视图,App.vue

导航:

<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>

跳转:

this.$router.push('/')
this.$router.push('/about')

问题:

  1. Vue.use(Router)的时候内部会做什么事情?

  2. 为什么要把router作为选项,设置到选项中?

  3. router-link 为什么可以直接使用,而不需要注入?

vue-router源码实现

单⻚面应用程序中,url发生变化时候,不能刷新,显示对应视图内容

需求分析

  • spa页面不能刷新

  • hash 比如: #/about

  • 监听hashchange事件

  • 会和component产生映射关系,当hash改变只需要把对应组件渲染到router-view

  • History api /about

  • 点击跳转时,url虽然改变了,但是浏览器并不刷新,就可以拦截下,并处理跳转

  • 根据url显示对应的内容

  • router-view

  • 数据响应式:current变量持有url地址,一旦变化,动态重新执行render

任务拆分

  • 实现一个插件

  • 实现VueRouter

  • 处理路由选项

  • 监控url变化,hashchange

  • 响应这个变化

  • 实现install方法

  • $router注册

  • 注册两个全局组件(router-viewrouter-link

代码实现

此案例实现的是 hash 方式

node版本为:10.23.0

先使用vue-cli创建一个vue2有vue-router的项目。目录结构:

package.json:

{
  "name": "hello-world",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vue-router": "^3.2.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  }
}

创建一个自己的 router 目录,我这里命名为 lrouter,并新建一个 index.js 文件

修改 main.js 文件里的 router 文件路径为 ./lrouter

// main.js
import Vue from 'vue'
import App from './App.vue'
// import router from './router'
import router from './lrouter'


Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

修改 lrouter 文件里的 index.js 内容:

// lrouter/index.js	此文件只是修改了router/index.js文件的Vue-router的引用路径 
import Vue from 'vue'
import VueRouter from './lvue-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({
  routes
})

export default router

创建 lvue-router.js 文件

// lrouter/lvue-router.js

// 1.实现一个插件
// 2.实现VueRouter:处理选项、监控url变化、动态渲染
class VueRouter {

}

// 因为在lrouter/index.js文件中 VueRouter 需要Vue.use来使用,因此需要实现一个install方法
VueRouter.install = function () {

}

export default VueRouter

Vue.use 使用插件的时候会将 vue 传递到插件中。VueRouter类中需要使用Vue,因此需要在外层先创建一个Vue变量

let Vue;
class VueRouter {
  // Vue要在这里使用
}

VueRouter.install = function (_vue) {
  Vue = _vue
}

export default VueRouter

这里先说一下问题2:为什么要把router作为选项,设置到选项中?

因为我们在组件中经常会使用this.$router.push(),如果想这样使用就需要将$router挂载到Vue实例上。挂载到Vue实例上一般使用Vue.prototype.$router = router来挂载。

现在有一个问题,就是install方法调用的时间点非常早。从 lrouter/index.js 文件看出,Vue.use(VueRouter) 要早与 new VueRouter,按照同步代码执行顺序,在install方法中是获取不到VueRouter实例的,现在利用全局混入尝试延迟调用:

VueRouter.install = function (_vue) {
  Vue = _vue
  // 利用全局混入来延时调用后续代码
  Vue.mixin({
    beforeCreate() {
      // 以后每个组件都会调用该方法, 避免重复调用
      if (this.$options.router) {
        // 此时的上下文this是当前组件实例
        Vue.prototype.$router = this.$options.router
      }
    }
  })
}

上述代码完成了挂载$router,下面实现注册两个全局组件,在 install 方法中,加入如下代码:

Vue.component('router-view', {
  // 注意:这里不能写template,因为目前的vue版本是运行时(runtime-onle)版本,不带编译器的
  render(h) {
    return h('div', 'router-view')
  }
})
Vue.component('router-link', {
  render(h) {
    return h('a', 'router-link')
  }
})

此时项目已经不报错,页面也可以正常渲染了

修改router-link渲染内容:

Vue.component('router-link', {
  props: {
    to: {
      type: String,
      require: true
    }
  },
  render(h) {
    // 用户使用方式为 <router-link to="/about">内容</router-link>
    // <a href="#/about">内容</a>
    // 通过 this.$slots.default 来获取内容
    return h('a',{ attrs: {href: '#' + this.to}}, this.$slots.default)
  }
})

此时 router-link 中的内容也正常了。

现在写VueRouter类:

class VueRouter {
  // Vue要在这里使用
  constructor(options) {
    // 1.处理选项
    this.$options = options

    // 初始化url地址
    this.current = '/'
    // 2.监控url变化
    // 因为后边的回调函数是指向了window,所以要bind一下
    window.addEventListener("hashchange", this.onHashChange.bind(this))
  }

  onHashChange() {
    this.current = window.location.hash.slice(1)
  }
}

思考一下下方 router-view 组件的工作方式?

Vue.component('router-view', {
  // 注意:这里不能写template,因为目前的vue版本是运行时(runtime-onle)版本,不带编译器的
  render(h) {
    return h('div', 'router-view')
  }
})

它是一个内容的容器,因此容器的标签不能写死为 div,它需要把用户配置的映射表中的 **component** 取出来,再渲染出来

Vue.component('router-view', {
  // 注意:这里不能写template,因为目前的vue版本是运行时(runtime-onle)版本,不带编译器的
  render(h) {
    let Component = null
    // 获取 current
    const route = this.$router.$options.routes.find(route => route.path === this.$router.current)
    if (route) {
      Component = route.component
    }
    return h(Component)
  }
})

解析:route 变量:在 mixin 中已经将 $router 保存到 Vue 的实例中(Vue.prototype.$router = this.$options.router),因此,this.$router.$options.routes获取的是 new VueRouter中的 options 中的 routes,即如下代码的 routes

// lrouter/index.js 
const router = new VueRouter({
  routes
})

此时可以看到页面可以正常输出了:

但是切换 Home 和 About 页面并不会重新渲染,因为 current 并不是响应式数据,需要修改 current 为响应式数据,我们根据 Vue 提供的一个 API(Vue.util.defineReactive) 来创建 current 为响应式数据:

class VueRouter {
  // Vue要在这里使用
  constructor(options) {
    // 1.处理选项
    this.$options = options

    // 需要响应式current
    const initial = window.location.hash.slice(1) || '/'
    Vue.util.defineReactive(this, 'current', initial)
    // 2.监控url变化
    // 因为后边的回调函数是指向了window,所以要bind一下
    window.addEventListener("hashchange", this.onHashChange.bind(this))
  }

  onHashChange() {
    this.current = window.location.hash.slice(1)
  }
}

此时页面点击链接后可以显示对应组件内容了。但是如果出现嵌套路由,则无法正常渲染,因为没有处理嵌套路由的逻辑。。

回答:

问题1:Vue.use(Router) 的时候内部会做什么事情?

  • 利用全局混入将 $router 挂载到 Vue 实例上
  • 注册 router-viewrouter-link 全局组件

问题3:router-link 为什么可以直接使用,而不需要注入?

  • 因为在 Vue.use(VueRouter) 时注册了 router-link 全局组件