Vue3全家桶之路由系统

当我们切换login和home页面的时候,会发现浏览器路径后面多了个/#/login/#/home,并且在页面不刷新的情况下,就更新了局部的组件内容,而这就是由我们的路由系统控制的。

路由系统是Vue项目中的核心功能,包括在我们前面的章节中,也曾多次的涉及到路由管理的部分,本小节中我们就来好好的聊一下前端路由的由来以及Vue3中的路由实现。

输入路由的时候是怎么获取到页面的

我们都知道所谓的路由就是在浏览器地址栏输入的那一行地址,那么在我们输入这一行地址的时候,发生了什么,又是怎么获取到正确的页面并显示出来的呢?

当我们在浏览器中输入一个地址后,浏览器会根据路径构建一个请求,接下来就会对输入的域名进行DNS解析,得到正确的IP地址,然后和得到的IP地址建立TCP链接,发送HTTP请求,服务器接收到请求后,就会返回响应的HTML内容。

完成了请求和响应后,浏览器拿到了返回的HTML字符串,转换成DOM树结构,经过对DOM的样式计算,最终生成布局,在页面上进行合成渲染。

这就是在一个页面上输入路由,到最终页面显示的大概过程了,但是听上去好像和前端没什么关系,路由对应的页面信息都是请求后端返回的,我们只需要对返回的页面进行渲染就可以了。

前端路由的发展

其实在最开始的时候,前端确实是没有自己的路由系统的,前端代码都是嵌入在后端代码中,由后端代码来实现路由的跳转刷新。

用户访问页面地址后,由后端来解析加载对应的前端页面返回给浏览器,这样做的好处是前端不用考虑路由管理,工作量无形的减少了,同时有利于SEO,SSR其实就是这种方式。但相应的也存在一些弊端,在每次更新路由时,页面都会被强制刷新,也增加了网络请求的次数。

随着ajax的出现,前后端项目逐渐分离,前端控制页面数据的渲染、用户的交互,后端则关注数据逻辑的处理,那么要实现页面模块的切换,前端也需要一套自己的路由系统。

现在主流的前端框架都已经实现了前端路由,项目中的各个模块都在统一的入口页面中展示,用户切换地址跳转的时候,并不会刷新页面,也不会向后端发送请求,而是由js监听到路由的改变,进行路径的解析处理,匹配加载对应的组件渲染到入口页面对应的位置,这就是我们常说的SPA(单页面应用)。

hash路由与history路由

我们使用vue-router创建路由时,提到了两个方法createWebHistorycreateWebHashHistory,并表示这两个方法分别用来创建history模式与hash模式的路由,那么这两种路由方式有什么区别呢?

hash模式:从表现上来看,hash模式其实就是浏览器地址中/#/login的部分,通过window.location.hash可以获取到路径上的hash值。

当路径上的hash值发生改变的时候,不会导致页面刷新,也不会发送请求,我们可以通过hashchange事件来监听hash值的改变,进而渲染新的组件内容。

history模式:从表现上看,history模式在浏览器路径中不会存在#相关的内容,history模式之所以可以实现前端路由,主要由于在HTML5中,新增了pushStatereplaceState方法,支持对浏览器的路由进行修改操作,浏览器不会对服务端发送请求并且也会触发一个监听事件popstate。

从上面的描述来看,要实现前端路由,需要满足几个条件:

  • 更改页面路径,不会引起页面刷新,不会向服务端发送请求。
  • 存在监听路径改变的事件,可以在路径改变后动态渲染页面。

hash模式和history模式都具备上面的特点,用各自不同的方法实现前端路由,当然这两种模式也存在一些区别:

  • hash模式路径上会携带#,history模式没有。
  • hash模式可以直接路径回车刷新,history模式不可以,需要后端配合。
  • hash模式支持低版本浏览器,history模式需要支持HTML5。

怎么实现Vue3中的路由管理?

我们已经了解了前端路由的两种实现方式,接下来我们就来实现下Vue3中的hash模式路由。

在router文件夹下新建一个new-vue-router的文件夹,新增index.js子文件,我们将在这里实现路由管理的代码。

-- router
    |-- router.js
    |-- new-vue-router
        |-- index.js

先让我们来看下,Vue3中是如何使用路由的:

// router.js
import {
    
     createRouter, createWebHashHistory } from 'vue-router';

import Login from '../pages/login.vue';
import Home from '../pages/home.vue';

// 定义各个组件的路由地址
const routes = [{
    
    
    path: '/login',
    component: Login
}, {
    
    
    path: '/home',
    component: Home
}]

// 创建路由实例
export default createRouter({
    
    
    history: createWebHashHistory(),
    routes
});
// App.vue
import {
    
     createApp } from 'vue'

import App from './App.vue'
import router from './router/router';

const app = createApp(App);
app.use(router).mount('#app');

从router.js中我们可以看到,import导入了两个方法createRouter, createWebHashHistory,所以新路由管理中也需要这两个函数,并且createRouter方法返回了一个路由对象,那么我们新增代码如下。

function createRouter() {
    
    
    // 定义一个路由对象
    const router = {
    
    
        
    }
    // 返回路由对象
    return router;
}

function createWebHashHistory() {
    
    

}

export {
    
    
    createRouter,
    createWebHashHistory
}

好了, 一个非常简单的基础结构就完成了,两个方法都有了,并且createRouter方法可以返回一个空的路由对象,接下来就是要实现每个函数的具体功能。

从createRouter的使用中,我们可以看到传入了两个参数:

createRouter({
    
    
    history: createWebHashHistory(),
    routes
});

这两个参数分别用来说明使用的是什么路由模式,以及各个组件的路由地址,createRouter通过传入的参数来给router对象赋值。

我们先来实现createRouter方法:

function createRouter(params) {
    
    
    // 获取传入的两个参数
    const history = params.history;
    const routes = params.routes;
    
    const router = {
    
    
        history,
        routes
    }
    return router;
}

这样我们就是实现了给router实例的赋值。

因为要根据当前hash来动态选择组件渲染,所以router实例还需要保存当前的路径hash,当浏览器路径hash改变时,修改保存的hash值并通知其他地方渲染,学习过响应式章节后,我们知道可以将hash定义为响应式数据。

import {
    
    ref} from 'vue';

function createRouter(params) {
    
    
    const history = params.history;
    const routes = params.routes;

    const router = {
    
    
        history,
        routes,
        // 增加当前路径hash的响应式变量
        hash: ref(window.location.hash.slice(1)),
    }
    return router;
}

hash模式是通过hashchange事件来监听hash改变的,我们是根据传入的参数history来判断是否为hash模式,如果是则添加监听hashchange事件,当事件被触发时更新当前路径的hash:

function createWebHashHistory() {
    
    
    // 区分路由模式
+   return 'WebHashHistory';
}

function createRouter(params) {
    
    
    const history = params.history;
    const routes = params.routes;
    
    const router = {
    
    
        history,
        routes,
        // 增加当前路径hash的响应式变量
        hash: ref(window.location.hash.slice(1)),
    }
    // 增加监听事件
+   if (history === 'WebHashHistory') {
    
    
+        window.addEventListener('hashchange', () => {
    
    
+            // hash修改后更新变量
+            router.hash.value = window.location.hash.slice(1);
+        });
+    }

     return router;
}

new-vue-router中的主要方法我们就已经搞定了,但只有这些,我们只能监听到路由hash的改变,还不足以根据hash将组件渲染到页面上,看下App.vue,原来vue-router还新增了两个组件,router-viewrouter-link用来实现渲染和跳转功能,那我们继续新增两个文件router-view.vue、router-link.vue。

先看下router-link的使用方式:

<router-link to="/login">login</router-link>

其实router-link就是一个a标签组件,传入了一个to参数,router-link的子元素使用插槽插入,代码实现如下:

<template>
    <a :href="'#' + props.to">
        <slot></slot>
    </a>
</template>

<script setup>
    import {
    
     defineProps } from 'vue';

    let props = defineProps({
    
    
        to: String
    })
</script>

router-view是组件动态渲染的地方,需要根据当前hash值,去匹配查询到对应的组件,那么我们需要获取到当前的hash和路由数组(就是router对象下的hash和routes),我们知道这两个信息存在createRouter创建的路由实例中,但要怎么传递到router-view中呢?我们暂时放一下这个问题,假设已经传过来了,先实现匹配渲染的功能。

<template>
    <component :is="currentComponent"></component>
</template>

<script setup>
    import {
    
    computed, reactive, ref} from 'vue';
    // 假设已经有当前路由和路由数组
    let hash = ref('/');
    let routes = reactive([]);

    // hash地址为响应式,根据地址计算出组件
    let currentComponent = computed(() => {
    
    
        const targetComponent = routes.find((route) => {
    
    
            return route.path === hash.value
        })

        return targetComponent.component
    })
</script>

router-link和router-view就已经实现了,那么需要在App.use的时候将它们注册为全局组件,App.use()方法执行的时候,如果参数是个方法,会直接执行,如果是对象,会执行对象下的install方法。

我们知道import router from './router/router';导入的是createRouter返回的路由对象,所以我们需要在router中添加一个install方法,注册全局组件。

import RouterLink from './router-link.vue';
import RouterView from './router-view.vue';
...
function createRouter(params) {
    
    
    const history = params.history;
    const routes = params.routes;

    const router = {
    
    
        history,
        routes,
        // 增加当前路径hash的响应式变量
        hash: ref(window.location.hash.slice(1)),

+        install (app) {
    
    
+            // 注册全局组件
+            app.component('router-link', RouterLink);
+            app.component('router-view', RouterView);
+        }
    }

    if (history === 'WebHashHistory') {
    
    
        window.addEventListener('hashchange', () => {
    
    
            // hash修改后更新变量
            router.hash.value = window.location.hash.slice(1);
        });
    }
    
    return router;
}

不要忘记了,在router-view中,还有两个数据没有传递过去,既然router-view已经被注册成app下的组件了,那这不就是组件之间的传值,因为我们不确定router-view与app中间会嵌套几层,所以我们可以使用Provide和Inject进行跨层级传递参数,将router对象传递给router-view组件。

// index.js
...
function createRouter(params) {
    
    
    const history = params.history;
    const routes = params.routes;

    const router = {
    
    
        history,
        routes,
        // 增加当前路径hash的响应式变量
        hash: ref(window.location.hash.slice(1)),

        install (app) {
    
    
            app.component('router-link', RouterLink);
            app.component('router-view', RouterView);
            
+           app.provide('ROUTER', router);
        }
    }

    if (history === 'WebHashHistory') {
    
    
        window.addEventListener('hashchange', () => {
    
    
            // hash修改后更新变量
            router.hash.value = window.location.hash.slice(1);
        });
    }
    
    return router;
}
// router-view.vue

<template>
    <component :is="currentComponent"></component>
</template>

<script setup>
    import {
    
    computed, reactive, ref, inject} from 'vue';
    import NoFind from './404.vue';
    // 获取app组件的传值
    const router = inject('ROUTER');
    let hash = router.hash;
    let routes = router.routes;

    // hash地址为响应式,根据地址计算出组件
    let currentComponent = computed(() => {
    
    
        const targetComponent = routes.find((route) => {
    
    
            return route.path === hash.value
        })
        return  targetComponent?.component || NoFind
    })
</script>

这样就将hash和routes的值传递到router-view中了,并且我们增加了一个404页面,当没有匹配的地址时,显示404页面内容。

到目前为止,我们只用了不到50行代码就实现了简单的hash模式路由,成功的完成了路由的切换和组件的动态渲染,history模式的实现方式大家可以自己去尝试下。

那么vue-router也是这样来实现的嘛?我们可以对比下vue-router的部分源码,看下有什么区别:

// router.ts
install(app: App) {
    
    
      const router = this
      // 注册RouterLink和RouterView的全局组件
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)
        
      // 将router实例绑定到app的全局属性$router上
      app.config.globalProperties.$router = router
      // 拦截对$router属性的访问
      Object.defineProperty(app.config.globalProperties, '$route', {
    
    
        enumerable: true,
        // 如果当前路由是响应式变量,则返回value值,否则返回路由字符串
        get: () => unref(currentRoute),
      })
      ...
      // 通过provide跨组件传值
      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute))
      app.provide(routerViewLocationKey, currentRoute)
        
      // app组件卸载时的相关处理逻辑
      const unmountApp = app.unmount
      installedApps.add(app)
      app.unmount = function () {
    
    
        installedApps.delete(app)
        // the router is not attached to an app anymore
        if (installedApps.size < 1) {
    
    
          // invalidate the current navigation
          pendingLocation = START_LOCATION_NORMALIZED
          removeHistoryListener && removeHistoryListener()
          currentRoute.value = START_LOCATION_NORMALIZED
          started = false
          ready = false
        }
        unmountApp()
      }
      ...
    },

在源码的router.ts文件中,也是注册了RouterLink和RouterView两个组件,通过provide方式传递了一些参数给子组件们,并且额外增加了对全局属性$router的赋值和访问的拦截处理,以及app组件卸载时的一些逻辑。再来看下createWebHashHistory方法。

// hash.ts
export function createWebHashHistory(base?: string): RouterHistory {
    
    
  // Make sure this implementation is fine in terms of encoding, specially for IE11
  // for `file://`, directly use the pathname and ignore the base
  // location.pathname contains an initial `/` even at the root: `https://example.com`
  base = location.host ? base || location.pathname + location.search : ''
  // allow the user to provide a `#` in the middle: `/base/#/app`
  if (!base.includes('#')) base += '#'
  
  if (__DEV__ && !base.endsWith('#/') && !base.endsWith('#')) {
    
    
    warn(
      `A hash base must end with a "#":\n"${base}" should be "${base.replace(
        /#.*$/,
        '#'
      )}".`
    )
  }
  return createWebHistory(base)
}

createWebHashHistory方法的源码看上去就很简短了,只是处理了下基础路径,最终调用的竟然是createWebHistory,可见vue-router并没有像我们一样使用hashchange来监听,难道也是使用pushState和replaceState来处理hash模式的吗?大家可以自己去验证一下,想自己去尝试实现createWebHistory方法的同学可以去参考下vue-router源码的实现,这里就不多展开说明了。

从源码中我们可以看到,vue-router的实现原理与我们的基本类似,当然vue-router对更多的细节进行了处理,功能也更加强大丰富,感兴趣的同学可以下载源码研究一下vue-router

总结

本节中,我们介绍了前端路由的由来,从开始的借用后端路由来实现页面刷新跳转,到后来的前后端分离,前端拥有了自己的路由系统,在SPA中被大量应用。

然后我们介绍了前端路由的两种模式,hash模式和history模式,解释了两种方式实现前端路由的原理和区别,最后我们在Vue3中实现了hash模式的路由管理。

我们实现的这个版本与最终版本还是存在非常大的差异的,vue-router还有一些进阶用法比如动态路由匹配,导航守卫等也是会经常使用到的,大家可以对照官方文档了解路由的更多用法,路由系统是Vue的核心功能之一,希望通过本节能让大家对前端路由有个更深的理解。

猜你喜欢

转载自blog.csdn.net/qq_37215621/article/details/130971686