美文网首页vue前端
Vue Router 4 的使用,一篇文章给你讲透彻

Vue Router 4 的使用,一篇文章给你讲透彻

作者: chonglingliu | 来源:发表于2022-02-06 19:13 被阅读0次

    Vue 3.X 使用Vue Router 4.x 进行路由配置,本文我们就来研究下如何使用Vue Router 4.x,本文中所有的使用方式都是使用 Composition API的方式。

    本文通过一步步介绍Vue Router 4.x的使用,来搭建一个简单的博客系统,让你对新版的Vue Router 4.x有一个完整的认识,然后能够非常轻松滴将Vue Router 4.x应用在自己的项目中。

    项目初始化

    项目搭建

    项目使用vite进行创建。

    npm init vite@latest vue-router-blog
    npm install
    npm run dev
    

    目前安装的是Vue 3.2.25

    配置vite.config.js

    我们配置@别名,这样就比较方便书写引入文件的路径

    // 引入文件
    const path = require("path");
    
    export default defineConfig({
      // 添加 @
      resolve: {
        alias: {
          "@": path.resolve(__dirname, "./src"),
        },
      },
      plugins: [vue()],
    });
    
    
    配置jsconfig.json

    jsconfig.json可以让VSCode更加智能

    {
      "include": [
        "./src/**/*",
      ],
      "compilerOptions": {
        "baseUrl": ".",
        "paths": {
          "@/*": ["./src/*"]
        }
      }
    }
    

    Vue Router 4初体验

    安装Vue Router 4
    npm i vue-router@4
    

    目前安装的是Vue Router 4.0.12

    创建两个页面Home.vueAbout.vue
    <!-- Home.vue -->
    <template>
      <div>
        主页
      </div>
    </template>
    
    <!-- About.vue -->
    <template>
      <div>
        关于页
      </div>
    </template>
    

    这两个页面很简单,每个页面仅仅就是显示一行文字

    创建router

    我们在src目录下新建router目录,在router目录下创建index.js文件, 在里面进行路由的信息配置。

    import { createRouter, createWebHistory } from "vue-router";
    
    // 引入
    import Home from "@/views/Home.vue";
    import About from "@/views/About.vue";
    
    // 路由信息
    let routes = [
      {
        path: "/",
        name: 'home',
        component: Home,
      },
      {
        path: "/about",
        name: 'about',
        component: About,
      },
    ];
    
    // 路由器
    const router = createRouter({
      history: createWebHistory(), // HTML5模式
      routes,
    });
    
    export default router;
    
    
    安装router

    将路由安装router安装到app上。

    import { createApp } from 'vue'
    import App from './App.vue'
    
    // 引入插件
    import router from "@/store/index";
    // 安装router插件
    createApp(App).use(router).mount('#app')
    
    使用 router-linkrouter-view

    修改App.vue

    <template>
      <img alt="Vue logo" src="./assets/logo.png" /><br />
      <div>
        <router-link to="/">Home</router-link> |
        <router-link to="/about">About</router-link><br />
      </div>
      <router-view></router-view>
    </template>
    

    至此,我们的就实现了页面间的切换功能了。

    basic

    几个重要的概念

    router-link组件和a标签的区别?

    router-link组件底层也是渲染的a标签,但是router-link的页面切换只是更新了页面的部分内容,不会进行整个页面的刷新,而a标签跳转(例如:<a href="/about">调到Home标签</a><br>)是对整个页面进行刷新。

    底层原理是router-link劫持了元素的点击事件,添加了处理页面更新的逻辑。

    Hash模式和HTML5模式的区别?

    Hash模式的URL中有一个#号,eg:http://localhost:3000/#/about, #号后面的就是Hash地址,这个模式以前是SPA的常用模式,但是链接有一个#号比较丑。

    HTML5模式和正常的链接地址一样的,eg:http://localhost:3000/about, 这个地址很优雅,但是有一个问题,需要服务器支持。 原因是浏览器中输入http://localhost:3000/about支持,服务器以为要访问根路劲下的about目录的HTML文件,而不是访问根路劲下的HTML文件。

    webpackvite启动的服务器是支持HTML5模式的,所以开发环境使用HTML5模式没有问题。

    router-link组件和router-view组件为什么能直接使用?

    安装router插件的时候注册了这两个全局组件,所以能直接使用。

    install(app: App) {
        app.component('RouterLink', RouterLink)
        app.component('RouterView', RouterView)
    }
    

    路由懒加载

    上面写法有一个严重的问题,router中所有的组件都会被一次加载。我们的例子中就是 HomeAbout组件,即使有时候不会用到About组件, 也要加载,这对首页的显示会有很大的影响。

    改造如下:

    <!--// 删除 import Home from "@/views/Home.vue";-->
    <!--// 删除 import About from "@/views/About.vue";-->
    
    let routes = [
      {
        path: "/",
        name: 'home',
        <!--// 改成如下的写法-->
        component: () => import("@/views/Home.vue"),
      },
      {
        path: "/about",
        name: 'about',
        <!--// 改成如下的写法-->
        component: () => import("@/views/About.vue"),
      },
    ];
    

    这样在开发环境中只有使用到组件才会加载进来,在生产环境中异步组件会分开文件进行打包。

    修改代码(创建博客的框架)

    为了方便介绍其他内容,我们修改一下代码内容:

    新建模拟博客列表数据
    [
      {
        "id": 1,
        "catId": 1,
        "catName": "iOS",
        "subCatId": 1,
        "subcatName": "推荐",
        "name": "RxSwift实现MVVM架构",
        "image": "https://images.xiaozhuanlan.com/photo/2018/2f5dff865155d756dfe04f2909cd1a36.png",
        "description": "在本文中,我将介绍iOS编程中的MVVM设计模式,当然还有RxSwift的介绍。本文分为两部分。在第1部分中简要介绍了RxSwift的设计模式和基础知识,在第2部分中 ,我们有一个使用RxSwift的MVVM的示例项目。"
      },
      
      //省略...
    ]
    
    

    命名为data.json将其放置在src文件夹下

    创建路由信息
    // 路由信息
    let routes = [
      {
        path: "/",
        name: 'home',
        component: () => import("@/views/All.vue"),
      },
      {
        path: "/iOS",
        name: 'iOS',
        component: () => import("@/views/iOS.vue"),
      },
      {
        path: "/android",
        name: 'android',
        component: () => import("@/views/Android.vue"),
      },
      {
        path: "/flutter",
        name: 'flutter',
        component: () => import("@/views/Flutter.vue"),
      },
      {
        path: "/web",
        name: 'web',
        component: () => import("@/views/Web.vue"),
      },
    ];
    

    设置5个路由:全部iOSAndroidFlutterWeb

    顶部导航组件
    <!-- TheNavigation.vue -->
    <template>
      <div id="nav">
        <router-link to="/" class="nav-link">全部</router-link>
        <router-link to="/ios" class="nav-link">iOS</router-link>
        <router-link to="/android" class="nav-link">Android</router-link>
        <router-link to="/flutter" class="nav-link">Flutter</router-link>
        <router-link to="/web" class="nav-link">Web</router-link>
      </div>
    </template>
    

    TheNavigation导航组件中有5个router-link,分别切换到全部iOSAndroidFlutterWeb

    5个页面组件
    <template>
      <div class="container">
        <!-- 博客列表 -->
        <div v-for="blog in arrs" class="item" :key="blog.id">
          <!-- 图片 -->
          <img class="thumb" :src="blog.image" />
          <!-- 信息 -->
          <div class="info">
            <div class="title">{{ blog.name }}</div>
            <div class="message"> {{ blog.description }} </div>
          </div>
        </div>
      </div>
    </template>
    
    <script setup>
    
    // 数据
    import sourceData from "@/data.json";
    let arrs = sourceData;
    
    </script>
    
    APP.vue
    <script setup>
    import TheNavigation from "@/components/TheNavigation.vue";
    </script>
    
    <template>
      <TheNavigation />
      <router-view></router-view>
    </template>
    

    至此,博客框架就完成了,实现了5个博客分类,效果如下图:

    博客分类

    设置linkActiveClass

    路由器可以设置router-link激活的类:

    const router = createRouter({
      history: createWebHistory(),
      routes,
      <!--// 添加激活的类-->
      linkActiveClass: "blog-active-link"
    });
    

    然后设置样式:

    #nav .blog-active-link  {
      color: red;
      border-bottom: 2px solid red;
    }
    
    linkActiveClass

    命名路由

    我们在顶部导航组件使用的跳转都是路径跳转例如:to="/", 我们可以给路由设置一个名称name,这样可以通过路由的名称name进行跳转。

    <template>
      <div id="nav">
        <router-link to="/" class="nav-link">全部</router-link>
        <!-- 修改 to 属性为 name -->
        <router-link :to="{name: 'ios'}" class="nav-link">iOS</router-link>
        <router-link :to="{name: 'android'}" class="nav-link">Android</router-link>
        <router-link :to="{name: 'flutter'}" class="nav-link">Flutter</router-link>
        <router-link :to="{name: 'web'}" class="nav-link">Web</router-link>
      </div>
    </template>
    

    路由的query

    前面提到的5个博客分类是固定的,我们点击博客列表的每条数据进入博客详情,此时由于不同的博客内容是不同的,所以不能固定写死。实现方法一是通过路由传参实现。

    添加博客详情的路由
    let routes = [
      //...
      {
        path: '/blogdetail',
        name: "blogdetail",
        component: () => import("@/views/BlogDetail.vue")
      }
    ];
    
    query传参
    <template>
      <div class="container">
        <!-- 传参 -->
        <router-link v-for="blog in arrs" class="item" :key="blog.id" :to="{ name: 'blogdetail', query: { id: blog.id } }">
          // 省略
        </router-link>
      </div>
    </template>
    
    

    设置query: { id: blog.id } }给路由传参

    接收query传参
    <template>
      <div class="container">
        <h2>{{ blog.name }}</h2>
        <p>{{ blog.description }}</p>
      </div>
    </template>
    
    <script>
    import sourceData from "@/data.json";
    import { useRoute } from "vue-router";
    export default {
      setup(props) {
        // 获取路由
        let route = useRoute();
        // 获取query参数
        let blogId = route.query.id;
    
        return {
          blog: sourceData.find((blog) => blog.id == blogId),
        };
      },
    };
    </script>
    

    通过route.query.id就能获取到传递的博客id, 然后就能显示对应的博客信息了。

    query

    动态路由

    博客详情的页面逻辑,也可以用动态路由去实现。

    修改博客详情的路由
    <!-- router.js -->
    let routes = [
      //...
      {
        <!-- 动态路由路径 -->
        path: '/blogdetail/:id',
        name: "blogdetail",
        component: () => import("@/views/BlogDetail.vue")
      }
    ];
    

    :id 表示 路由的路径是动态的,路径最后表示博客id.

    传参
    <template>
      <div class="container">
        <!-- 传参 -->
        <router-link v-for="blog in arrs" class="item" :key="blog.id" :to="{ name: 'blogdetail', params: { id: blog.id } }">
          // 省略
        </router-link>
      </div>
    </template>
    
    

    设置params: { id: blog.id } }给动态路由传参

    接收参数
    let blogId = route.params.id;
    

    通过route.params.id就能获取到传递的博客id, 然后就能显示对应的博客信息了。

    重命名路由

    知道了动态路由的逻辑后,我们当然可以把iOS, Android, Flutter, Web四个页面合并为一个页面。

    合并router
    <!-- router.js -->
    let routes = [
      {
        path: "/",
        name: 'home',
        component: () => import("@/views/All.vue"),
      },
      <!-- 将/ios,/android,/flutter,/web四个合并为/category/:catId -->
      {
        path: "/category/:catId",
        name: 'category',
        component: () => import("@/views/All.vue"),
      },
      {
        path: '/blogdetail/:id',
        name: "blogdetail",
        component: () => import("@/views/BlogDetail.vue")
      }
    ];
    
    修改导航
    <template>
      <div id="nav">
        <router-link to="/" class="nav-link">全部</router-link>
        <!-- 动态路由 -->
        <router-link :to="{name: 'category', params: { catId: 1 }}" class="nav-link">iOS</router-link>
        <router-link :to="{name: 'category', params: { catId: 2 }}" class="nav-link">Android</router-link>
        <router-link :to="{name: 'category', params: { catId: 3 }}" class="nav-link">Flutter</router-link>
        <router-link :to="{name: 'category', params: { catId: 4 }}" class="nav-link">Web</router-link>
      </div>
    </template>
    
    
    列表
    <script setup>
    import { useRoute } from 'vue-router';
    
    // 获取路由
    let route = useRoute();
    // 获取params参数
    let catId = route.params.catId;
    
    // 数据
    import sourceData from "@/data.json";
    let arrs = sourceData.filter((blog) => blog.catId == catId);
    
    </script>
    
    

    这样我就可以把iOS.vue,Android.vue,Flutter.vue,Web.vue四个组件文件删除了。

    你应该有个疑问,home路由的内容其实和category路由的内容也是一样的,是否可以合并呢?

    重命名"/"

    可以将"/"重命名为'/category/0',这样所有的5个路由都将访问"/category/:catId"这个路由了。

    <!-- router.js -->
    let routes = [
      {
        path: "/",
        <!-- 重命名 -->
        redirect: '/category/0'
      },
      {
        path: "/category/:catId",
        name: 'category',
        component: () => import("@/views/All.vue"),
      },
      {
        path: '/blogdetail/:id',
        name: "blogdetail",
        component: () => import("@/views/BlogDetail.vue")
      }
    ];
    
    import sourceData from "@/data.json";
    let arrs = catId != '0' ? sourceData.filter((blog) => blog.catId == catId) : sourceData;
    

    判断下,如果catId != '0'为分类筛选,否则就是显示全部

    监听路由变化

    此时的代码出现了问题,点击顶部的导航切换不同的分类,底下的列表将不会变化。这是因为组件复用了。此时需要监听组件的路由的变化,切换数据。

    路由

    可以通过watch函数监听route.params, 当路由变化后,就可以重新获取数据。

    <!-- All.vue -->
    <script setup>
    import { ref } from '@vue/reactivity';
    import { useRoute } from 'vue-router';
    import sourceData from "@/data.json";
    import { watch } from '@vue/runtime-core';
    
    let arrs = ref([]);
    
    let route = useRoute();
    let params = route.params;
    
    let initData = (catId) => {
      arrs.value = catId != '0' ? sourceData.filter((blog) => blog.catId == catId) : sourceData;
    }
    
    // 初始化的时候获取数据
    initData(params.catId);
    
    // 监听paramas,更新数据
    watch(() => route.params.catId, (value) => {
      initData(value);
    })
    
    </script>
    

    禁止路由复用

    解决上节问题,还有一个更简单的方法,就是禁止路由的复用。

    <template>
      <TheNavigation />
      <!-- 禁止路由复用 -->
      <router-view :key="$route.path"></router-view>
    </template>
    
    

    通过这个方法,动态组件将不会复用,直接卸载旧组件,挂载新组件。所以性能上有丢丢的损耗。

    给组件传递props

    我们前面在组件中需要使用useRoute获取到路由,然后获取对应的route.params, 我们可以通过另外一种方式获取route.params

    路由添加props属性
    <!-- router.js -->
    {
        path: "/category/:catId",
        name: 'category',
        component: () => import("@/views/All.vue"),
        <!-- 路由添加`props`属性 -->
        props: true,
    }
    
    组件中获取props属性
    <script setup>
    import { ref } from '@vue/reactivity';
    import { useRoute } from 'vue-router';
    import sourceData from "@/data.json";
    
    // 定义props
    const props = defineProps({
      catId: {
        type: String,
        required: true,
      }
    })
    
    let arrs = props.catId != '0' ? sourceData.filter((blog) => blog.catId == props.catId) : sourceData;
    
    </script>
    

    组件中可以直接获取到catId参数,个人认为这种写法更优美。

    路由props属性支持函数
    <!-- router.js -->
    {
        path: "/category/:catId",
        name: 'category',
        component: () => import("@/views/All.vue"),
        props: route => ({ catId: parseInt(route.params.catId) }) ,
    }
    

    函数中,可以对参数进行处理,我们的例子中是将catId从字符串变成了数字

    // 定义props
    const props = defineProps({
      catId: {
        type: Number,
        required: true,
      }
    })
    
    let arrs = props.catId !== 0 ? sourceData.filter((blog) => blog.catId === props.catId) : sourceData;
    

    props catId的定义和使用也要进行相应的修改

    编程式导航

    除了使用<router-link> 来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。

    例如:可以在详情页加一个按钮,点击返回上一个页面

    <button @click="$router.back()">返回</button>
    

    转场动画

    Vue Router4 的转场动画的实现 和 以前的版本有些不一致。需要将transition 包含在router-view, 如下所示:

      <router-view v-slot="{ Component }">
        <transition name="fade" mode="out-in">
          <component :is="Component" :key="$route.path" />
        </transition>
      </router-view>
    

    加上对应的css样式

    /* fade 模式 name="fade" mode="out-in" */
    .fade-enter-active,
    .fade-leave-active {
      transition: opacity 0.3s;
    }
    
    .fade-enter-from,
    .fade-leave-to {
      opacity: 0;
    }
    

    这样切换就有淡入淡出的效果了。效果自定义,很方便。

    路由未匹配上

    有时候用户可能输入一个根本不存在的路劲(例如:http://localhost:3000/categorys),此时最好是给显示个默认的404页面,这样用户体验更好。

    404页面
    定义路由
    <!-- router.js -->
    {
        path: '/:pathMatch(.*)*',
        name: "NotFound",
        component: () => import("@/views/404.vue"),
    }
    

    注意,这个路由一定要放在最后,否则就有问题了。

    404页面
    <template>
      <div class="container">
        <h2>未找到页面</h2>
      <router-link to="/">回到首页</router-link>
      </div>
    </template>
    

    这个页面内容随意

    路由守卫

    路由独享的守卫

    想象下用户浏览器地址栏输入http://localhost:3000/category/6, 其实也会出现一些问题,因为不存在这个分类。这时候需要进行处理, 当分类不存在的时候跳转到404页面。

    <!-- router.js -->
      {
        path: "/category/:catId",
        name: 'category',
        component: () => import("@/views/All.vue"),
        props: route => ({ catId: parseInt(route.params.catId) }),
        <!-- 添加路由守卫 -->
        beforeEnter: (to, from) => {
          // 如果不是正确的分类,跳转到NotFound的页面
          console.log(to.params.catId);
          if (!["0", "1", "2", "3", "4"].includes(to.params.catId)) { 
            return {
              name: "NotFound",
              // 这个是在地址栏保留输入的信息,否则地址栏会非常的丑
              params: { pathMatch: to.path.split("/").slice(1) },
              query: to.query,
              hash: to.hash,
            };
          }
        }
      },
    

    判断如果不是正确的分类,跳转到NotFound的页面

    路由全局守卫

    在某些路由中需要一些特定的操作,譬如访问前必须是登录用户。这时候可以通过使用meta属性和全局守卫来实现。

    譬如有一个课程专栏我们设置为需要用户登录才能访问。我们可以如下设置

    <!-- router.js -->
      {
        path: '/course',
        name: "course",
        component: () => import("@/views/Course.vue"),
        <!-- 需要登录 -->
        meta: {needLogin: true}
      },
      {
        path: '/login',
        name: "login",
        component: () => import("@/views/Login.vue"),
      },
      
    

    添加一个全局守卫, 需要登录但是没有登录的情况下就跳转到登录页面

    <!-- router.js -->
    // 全局守卫
    router.beforeEach((to, from) => {
      if (to.meta.needLogin && !userLogin) {
        // need to login
        return { name: "login" };
      }
    });
    
    组件内的路由守卫

    前面的切换分类的章节的问题其实还有第三种解决方案,就是用组件内的路由守卫。

    <script setup>
    import { ref } from '@vue/reactivity';
    import { useRoute, onBeforeRouteUpdate } from 'vue-router';
    import sourceData from "@/data.json";
    
    // 定义props
    const props = defineProps({
      catId: {
        type: Number,
        required: true,
      }
    })
    
    let arrs = ref([]);
    
    let fetchData = (id) => {
      return id !== 0 ? sourceData.filter((blog) => blog.catId == id) : sourceData;
    }
    
    <!-- 组件内的路由守卫 -->
    onBeforeRouteUpdate((to, from, next) => {
      arrs.value = fetchData(to.params.catId)
    });
    
    arrs.value = fetchData(props.catId);
    
    </script>
    

    对于一个带有动态参数的路径 /category/:catId,在 /category/1/category/2 之间跳转的时候, 会触发onBeforeRouteUpdate的路由钩子函数,在钩子函数中可以进行数据的更新。

    扩展 RouterLink

    router-link可以实现路由的跳转,此外为了更加丰富功能,可以对其进行扩展。譬如我们可以扩展实现能够跳转到外部链接。

    <!--AppLink.vue-->
    <template>
      <!-- 如果是外部链接,跳转(<slot />表示router-link组件中的slot内容)  -->
      <a v-if="isExternal" :href="to"><slot /></a>
      <!-- 如果是APP内的链接,路由跳转 (<slot />表示router-link组件中的slot内容) -->
      <router-link v-else v-bind="$props"><slot /></router-link>
    </template>
    
    <script>
    import { computed, defineComponent } from "@vue/runtime-core";
    import { RouterLink } from "vue-router";
    
    export default {
      props: {
        // 继承RouterLink的props
        ...RouterLink.props,
      },
      setup(props) {
        
        // 如果`to`属性值是字符串类型,并且以`http`开头,我们认为它是外部链接
        let isExternal = computed(() => typeof props.to === 'string' && props.to.startsWith('http'));
    
        return {
          isExternal
        }
      }
    };
    </script>
    

    使用:

    <AppLink to="https://www.domain.cn" />
    

    总结

    Vue Router 4.x 的使用基本上介绍完了,最重要的特性是能和Composition API的搭配使用,此外使用上也还是有一些不小的变化。

    相关文章

      网友评论

        本文标题:Vue Router 4 的使用,一篇文章给你讲透彻

        本文链接:https://www.haomeiwen.com/subject/dwzekrtx.html