vue-router使用与源码浅析

vue-router使用与源码浅析_第1张图片

本文主要讨论最新版的vue-router(支持Vue3),主要从其出现原因、使用方式、自己实现一个玩具vue-router以及vue-router源码分析这几个角度去讨论,希望你喜欢。

前端路由演变

在JQuery时代,多数Web项目的路由都是由后端实现的,用户访问网页时的常见流程为:

  • 1.用户访问路由

  • 2.后端获得路由并匹配相应的模板

  • 3.后端动态渲染模板并返回

  • 4.浏览器加载返回模板中的JS、CSS

前端人员的开发多数都受限于后端框架提供的模板语言,用户每次页面跳转,都由后端动态渲染出相应的模板并返回。

这种项目开发方式有很多缺点:

  • 前后端无法分离

  • 页面调整需要刷新整个页面

  • 等待时间较长、交互体验下降

当Ajax出来后,前后端可以分离了,前端工程师尝试利用JS来构建路由系统,用户访问某路由时,利用Ajax去动态获取数据,再利用JS去动态生产换网页页面,这样页面不需要全局刷新,用户浏览网页的体验也更好了一些。

这种,通过JS去控制路由,让用户一直停留在index.html上,通过JS动态根据不同路由加载不同页面元素的应用便是单页应用程序(SPA,single page application)的早期形态。

vue-router的核心原理也类似,通过JS去匹配URL,然后加载不同的页面内容。

一般,前端JS匹配路由URL有两种方式:

  • hash模式,通过在URL中加入#来做内容区分

  • history模式,这种方式的URL看起与正常的一样

在2014年之前,大家主要通过hash模式来匹配URL,其URL形式为:

http://xxx.com/#/home

当页面跳转时,URL中#后的内容变化会触发hashchange事件(这种模式称为hash模式的原因),通过对hashchange事件的监听,便可以实现对网页内容的动态渲染:

window.addEventListener('hashchange', fn)

通过window.addEventListener方法,我们实现了,当hashchange事件改变时,对fn方法的调用,在fn方法中,可以对URL中不同的hash值进行不同的逻辑操作,比如加载不同页面内容。

在2014后,HTML5标准发布,浏览器多了pushState与replaceState这两个API,通过这两个API,我们可以改变URL且浏览器不会向后端服务发起请求,同时还触发popstate事件,利用这两个新增的API和popstate事件,便可以设计出history模式。

已经通过window.addEventListener方法监听popstate事件,然后调用相应的方法则可。

简单使用vue-router

构建一个vue3项目,然后安装vue-router@next。

yarn create vite DashboardFrameWork --template vue
yarn add vue-router@next

在src/components中创建Home.vue和About.vue这两个文件:






创建src/router目录,用于存放路由相关的逻辑,在src/router目录下,创建index.js,代码如下:

// src/router/index.js

import { createRouter, createWebHashHistory} from 'vue-router'
import Home from '../components/Home.vue'
import About from '../components/About.vue'

// 路由信息
const routes = [
    {
      path: "/",
      name: 'home',
      component: Home,
    },
    {
      path: "/about",
      name: 'about',
      component: About,
    },
  ];

const router = createRouter({
    history: createWebHashHistory(),
    routes
})

export default router

在index.js中,定义了两条路由信息,然后通过createRouter函数定义路由对象,其中使用了history模式。

在src/main.js中,我们需要导入router对象:

// src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'

createApp(App).use(router).mount('#app')

在App.vue使用vue-router:

在App.vue的代码中,我们使用了router-link和router-view,它们是vue-router提供的子组件,之所以可以直接使用,是因为vue-router将这两个组件注册到成了全局组件,所以能在项目的任意位置直接使用。

router-link的作用类似于a标签,用于切换页面内容,相比于a标签,router-link不会让浏览器刷新整个页面。

router-view的作用主要是展示相应组件的内容,比如本例中的Home.vue和About.vue。

将项目run起来,效果如下:

vue-router使用与源码浅析_第2张图片 image.png

点击Home或About,页面内容与网页URL都会发生改变。

动手实现Mini vue-router

了解了vue-router的基本用法后,我们来手撸一个mini vue-router,来实现【简单使用vue-router】小节中类似的效果。

在src/router下创建grouter文件夹,在src/router/grouter下创建index.js文件。

回顾深入Vuex与Pinia一文中,我们开发MiniVuex的过程,我们通过provide与inject将store对象提供出去,实现跨组件的使用,而MiniVuex管理的数据则全部存储在reactive定义的_state变量中。

这里也是类似的思路,通过provide与inject将router对象提供出去,完整代码如下:

// src/router/grouter/index.js

import {ref, inject} from 'vue'
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'

const ROUTER_KEY = '__router__'

function createRouter(options) {
    return new Router(options)
}

function useRouter() {
    return inject(ROUTER_KEY)
}

function createWebHashHistory() {
    function bindEvents(fn) {
        // 监听hashchange事件
        window.addEventListener('hashchange', fn)
    }

    return {
        bindEvents,
        url: window.location.hash.slice(1) || '/'
    }
}

class Router {
    constructor(options) {
        this.history = options.history
        this.routes = options.routes
        this.current = ref(this.history.url)
        // URL改变时,bindEvent中传入的方法会被调用
        this.history.bindEvents(() => {
            this.current.value = window.location.hash.slice(1)
        })
    }

    install(app) {
        app.provide(ROUTER_KEY, this)
        // 将router-line与router-view注册为全局组件
        app.component("router-link", RouterLink)
        app.component("router-view", RouterView)
    }
}

export {createRouter, createWebHashHistory, useRouter}

上述代码中,在useRouter函数中使用inject,在install函数中使用了provide,当我们需要在组件的script标签中使用router时,通过useRouter获得router对象,便可以使用其中的方法。

看到Router类,在构造方法(constructor)中,我们将options的属性赋值到类对应的数据中心,这里关键的一句是this.history.bindEvents的调用:

// URL改变时,bindEvent中传入的方法会被调用
this.history.bindEvents(() => {
  this.current.value = window.location.hash.slice(1)
})

bindEvents函数接收匿名方法,该匿名方法的作用便是获取当前URL中#号后的第一个元素,假设当前URL为http://localhost:3000/#/about,其效果如下:

window.location.hash
'#/about'
window.location.hash.slice(1)
'/about'

在install方法,除了使用provide方法将router对象本身提供出去外,还通过app.component方法将router-link与router-view组件注册成全局组件,这样,router-link与router-view可以在任意组件中使用。

现在我们来试一下router-link与router-view这两个组件,在src/router/grouter下创建RouterLink.vue和RouterView.vue两文件。

先开发RouterLink.vue,代码如下:




import { defineProps } from "vue";
let props = defineProps({
  to: {
    type: String,
    required: true,
  },
});

从代码可知,RouterLink的本质就是a标签,通过v-bind绑定a标签的href,通过props来接收父组件的传参,通过插槽(slot)将a标签要显示的内容交给父组件决定。

在App.vue中,使用方式为:

Home
About

其中to传入路径,会被router-link的defineProps接收到props对象中,而Home、About等内容,会在router-link的slot中显示。

接着开发RouterView.vue,代码如下:




import { computed } from 'vue';
import { useRouter } from '../grouter/index'


let router = useRouter()

const comp = computed(() => {
    // 寻找匹配的路由
    const route = router.routes.find(
        (route) => route.path === router.current.value
    )
    // 返回路由对于的组件
    return route?route.component:null
})

RouterView.vue的逻辑也很简单,利用了Vue提供的动态组件(上述代码中的component标签)来实现组件的动态显示,动态组件通过is属性来指定要动态显示的组件。

在RouterView.vue的JS中,通过useRouter获得router对象,然后获取其中的routes属性并通过find方法去匹配routes属性中的元素,元素的path与router.current.value相等,则返回相应的组件。

看回src/router/grouter/index.js中的代码(createWebHashHistory函数处),当用户改变URL时,hashchange事件会被监听到,然后将当前URL中hash部分赋值给router.current.value。

至此,Mini vue-router就开发好了,使用方法与vue-router一样,在src/router/index.js中,替换一下导入内容:

import { createRouter, createWebHashHistory} from './grouter/index'
// import { createRouter, createWebHashHistory} from 'vue-router'

运行项目,可以发现,项目成功运行。

在实现这个项目的过程中,我们使用了Vue3的slot与动态组件,这些都是灵活使用Vue3开发项目所必须要掌握的技巧,后续的文章会针对两者进行更深入的剖析,这里就点到为止。

vue-router源码浅析

install方法分析

vue-router在使用时,需要在main.js中通过app.use让Vue加载vue-touer插件,在src/router.ts中,可以找到install方法的逻辑:

install(app: App) {
      const router = this
      // 注册RouterLink
      app.component('RouterLink', RouterLink)
      // 注册RouterView
      app.component('RouterView', RouterView)
      // 注册$router作为全局变量
      app.config.globalProperties.$router = router
      Object.defineProperty(app.config.globalProperties, '$route', {
        enumerable: true,
        get: () => unref(currentRoute),
      })

      // ... 省略部分逻辑
  
      // 将router对象提供出去,让其他组件可以使用router对象
      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute))
      app.provide(routerViewLocationKey, currentRoute)
      
      // https://staging-cn.vuejs.org/api/application.html#app-unmount
      // 卸载一个已挂载应用实例
      const unmountApp = app.unmount
      installedApps.add(app)
      app.unmount = function () {
        // ...
      }
     
    },
  }

  return router
}

我将install方法中的部分逻辑删除了,只看最核心的东西。

在install方法中,一开始便通过app.component注册了RouterLink和RouterView,这点与我们开发的MiniVueRouter一致。

随后,通过app.config.globalProperties将router对象注册为全局变量,以作为全局变量的开头主要是避免全局变量重名冲突。

为了让不同组件可以实验router,在install方法中,还通过app.provide将router对象提供出去,这点也与我们开发MiniVueRouter一样。

RouterLink实现逻辑

大体了解install方法的逻辑后,来看看RouterLink是如何实现的。

与MiniVueRouter中实现的方式不同,MiniVueRouter通过template实现(模板语法),而RouterLink利用了JSX来实现,相比于模板语法,JSX利用JavaScript动态化的优势可以实现更加灵活的效果,但RouterLink是对a标签封装的本质并没有改变,看到src/RouterLink.ts中setup方法中的一段逻辑,便可以知,虽然写法变了,原理依旧。

return () => {
  const children = slots.default && slots.default(link)
  return props.custom
    ? children
  : h(
    'a',
    {
      'aria-current': link.isExactActive
      ? props.ariaCurrentValue
      : null,
      href: link.href,
      // this would override user added attrs but Vue will still add
      // the listener so we end up triggering both
      onClick: link.navigate,
      class: elClass.value,
    },
    children
  )
}

上述代码中,通过h函数生成a标签的虚拟DOM,这个便是RouterLink节点。

RouterView实现逻辑

RouterView最核心的逻辑就是匹配出用户当前访问URL对于的组件并将该组件渲染出来,理解了这个逻辑,看源码就好理解了,不然会陷入RouterView源码细节中。

RouterView依旧使用JSX来开发,通过h函数将组件动态渲染出来,其核心逻辑在src/Router.ts中:

return () => {
      const route = routeToDisplay.value
      // 匹配出的路由
      const matchedRoute = matchedRouteRef.value
      // 路由对应的组件(即RouterView要显示的组件)
      const ViewComponent = matchedRoute && matchedRoute.components[props.name]
      // we need the value at the time we render because when we unmount, we
      // navigated to a different location so the value is different
      const currentName = props.name

      // ... 省略部分逻辑
      
      // h函数将ViewComponent渲染成虚拟DOM
      const component = h(
        ViewComponent,
        assign({}, routeProps, attrs, {
          onVnodeUnmounted,
          ref: viewRef,
        })
      )

      // ...
      
      // 返回渲染出的组件
      return (
        // pass the vnode to the slot as a prop.
        // h and  both accept vnodes
        normalizeSlot(slots.default, { Component: component, route }) ||
        component
      )
    }

上述代码中,通过matchedRouteRef匹配出相应的路由,然后获得该路由对于的组件(matchedRoute.components),随后通过h函数将组件动态渲染成虚拟DOM并返回宣传好的组件。

createRouter方法分析

分析RouterView时,需要匹配路由,而这些路由就是通过RouterView传入的,回忆一下它的用法,方便理解该方法的核心逻辑,用法如下:

// 路由信息
const routes = [
    {
      path: "/",
      name: 'home',
      component: Home,
    },
    {
      path: "/about",
      name: 'about',
      component: About,
    },
  ];

const router = createRouter({
    history: createWebHashHistory(),
    routes
})

看到src/router.ts中的createRouter方法,先看该方法传入参数的类型为RouterOptions:

export interface RouterOptions extends PathParserOptions {
  history: RouterHistory
  routes: RouteRecordRaw[]
  ...

嗯,与我们的用法对上了,history对于Hash路由匹配模式还是history路由匹配模式,routers则是我们传入的路由对象。

createRouter方法最终会创建router对象,该对象会有添加路由、删除路由、获得路由等方法和各种属性,相关代码字面意思很好理解,就不赘述了。

尾部

因为能力问题,vue-router源码没有分析的特别细致,但也理出了主逻辑,下次有需求时,再细细剖析吧。

你可能感兴趣的:(vue,python,js,javascript,java)