• Vue3学习笔记(五)——路由,Router


    一、前端路由的概念与原理

    1.1. 什么是路由

    路由(英文:router)就是对应关系。

    1.2. SPA 与前端路由

    SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。

    此时,不同组件之间的切换需要通过前端路由来实现。

    结论:在 SPA 项目中,不同功能之间的切换,要依赖于前端路由来完成!

    1.3. 什么是前端路由

    通俗易懂的概念:Hash 地址与组件之间的对应关系。

    1.4. 前端路由的工作方式

    ① 用户点击了页面上的路由链接
    ② 导致了 URL 地址栏中的 Hash 值发生了变化
    ③ 前端路由监听了到 Hash 地址的变化
    ④ 前端路由把当前 Hash 地址对应的组件渲染都浏览器中

    结论:前端路由,指的是 Hash 地址与组件之间的对应关系

    1.5. 实现简易的前端路由

    步骤1:通过 <component> 标签,结合 comName 动态渲染组件。示例代码如下:

    <template>
      <div class="main">
        <nav>
          <a href="#/Home">首页</a> | <a href="#/Movie">电影</a> |
          <a href="#/About">关于</a>
        </nav>
        <section>
          <KeepAlive>
            <component :is="coms[comName]"></component>
          </KeepAlive>
        </section>
      </div>
    </template>
    
    <script lang="ts" setup>
    import Home from "./views/Home.vue";
    import Movie from "./views/Movie.vue";
    import About from "./views/About.vue";
    import { ref, KeepAlive, onMounted } from "vue";
    
    let coms = {
      Home,
      Movie,
      About,
    };
    
    let comName = ref("Home");
    //当页面挂载成功时的钩子
    onMounted(() => {
      //当hash值变化时的事件
      window.addEventListener(
        "hashchange",
        (event) => {
          //#/Home,获取路径名称
          let comKey = location.hash.substring(2);
          //更换当前组件名称
          comName.value = comKey;
        },
        false
      );
    });
    </script>
    
    <style>
    .main a {
      color: #00f;
      text-decoration: none;
      font-size: 16px;
    }
    .main a:hover {
      color: orangered;
    }
    .main nav {
      border-bottom: 2px solid #999;
      height: 46px;
      line-height: 46px;
    }
    </style>

    步骤2:在 App.vue 组件中,为 <a> 链接添加对应的 hash 值:

        <nav>
          <a href="#/Home">首页</a> | 
          <a href="#/Movie">电影</a> |
          <a href="#/About">关于</a>
        </nav>

    步骤3:在 created 生命周期函数中,监听浏览器地址栏中hash 地址的变化,动态切换要展示的组件的名称:

    //当页面挂载成功时的钩子
    onMounted(() => {
      //当hash值变化时的事件
      window.addEventListener(
        "hashchange",
        (event) => {
          //#/Home,获取路径名称
          let comKey = location.hash.substring(2);
          //更换当前组件名称
          comName.value = comKey;
        },
        false
      );
    });

    二、vue-router 的基本用法

    2.1. 什么是 vue-router

    vue-router 是 vue.js 官方给出的路由解决方案。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目中组件的切换。

    vue-router 的官方文档地址:https://router.vuejs.org/zh/

    Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。功能包括:

    • 嵌套路由映射
    • 动态路由选择
    • 模块化、基于组件的路由配置
    • 路由参数、查询、通配符
    • 展示由 Vue.js 的过渡系统提供的过渡效果
    • 细致的导航控制
    • 自动激活 CSS 类的链接
    • HTML5 history 模式或 hash 模式
    • 可定制的滚动行为
    • URL 的正确编码

    2.2. vue-router 安装和配置的步骤

    ① 安装 vue-router 包
    ② 创建路由模块与路由规则
    ③ 导入并挂载路由模块
    ④ 声明路由链接和占位符

    2.2.1 在项目中安装 vue-router

    在 vue3 的项目中,安装 vue-router 的命令如下:

    2.2.2 创建路由模块与路由规则

    在 src 源代码目录下,新建router/index.ts路由模块,并初始化如下的代码:

    import {createRouter,createWebHashHistory,RouteRecordRaw} from 'vue-router'
    
    import Home from '../views/Home.vue';
    import Movie from '../views/Movie.vue';
    import About from '../views/About.vue';
    
    //路由记录集合
    let routes:RouteRecordRaw[]=[
        {
            path:"/",
            component:Home
        },
        {
            path:"/home",
            component:Home
        },
        {
            path:"/movie",
            component:Movie
        },
        {
            path:"/about",
            component:About
        },
    ];
    
    //创建路由器
    let router=createRouter({
        history:createWebHashHistory(),  //指定路由模式为hash模式(兼容性好,但带#)
        routes
    });
    
    //导出
    export default router;

    2.2.3 导入并挂载路由模块

    在 src/main.ts 入口文件中,导入并挂载路由模块。示例代码如下:

    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router/index'  //导入路由规则
    
    let app=createApp(App);
    
    //挂载路由中间件
    app.use(router);
    
    app.mount('#app')

    2.2.4 声明路由链接和占位符

    router-link

    请注意,我们没有使用常规的 a 标签,而是使用一个自定义组件 router-link 来创建链接。这使得 Vue Router 可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码。我们将在后面看到如何从这些功能中获益。

    router-view

    router-view 将显示与 url 对应的组件。你可以把它放在任何地方,以适应你的布局。

    在 src/App.vue 组件中,使用 vue-router 提供的 <router-link> 和 <router-view> 声明路由链接和占位符:

    <template>
      <div class="main">
        <nav>
          <router-link to="/home">首页</router-link> |
          <router-link to="/movie">电影</router-link> |
          <router-link to="/about">关于</router-link>
        </nav>
        <section>
          <router-view></router-view>
        </section>
      </div>
    </template>
    
    <script lang="ts" setup></script>
    
    <style>
    .main a {
      color: #00f;
      text-decoration: none;
      font-size: 16px;
    }
    .main a:hover {
      color: orangered;
    }
    .main nav {
      border-bottom: 2px solid #999;
      height: 46px;
      line-height: 46px;
    }
    section {
      min-height: 500px;
      margin: 0;
    }
    </style>

    src/views/Home.vue

    <template>
      <div class="cls1">
        <h2>这是首页 - Home</h2>
      </div>
    </template>
    <script setup lang="ts"></script>
    <style scoped>
    .cls1 {
      background: #def;
      min-height: 500px;
    }
    .cls1 h2 {
      margin: 0;
      padding: 0;
    }
    </style>

    src/views/Movie.vue

    <template>
      <div class="cls1">
        <h2>这是电影频道 - Movie</h2>
      </div>
    </template>
    <script setup lang="ts"></script>
    <style scoped>
    .cls1 {
      background: #dfe;
      min-height: 500px;
    }
    .cls1 h2 {
      margin: 0;
      padding: 0;
    }
    </style>

    src/views/About.vue

    <template>
      <div class="cls1">
        <h2>这是关于我们 - About</h2>
      </div>
    </template>
    <script setup lang="ts"></script>
    <style scoped>
    .cls1 {
      background: #fde;
      min-height: 500px;
    }
    .cls1 h2 {
      margin: 0;
      padding: 0;
    }
    </style>

    运行效果:

    三、vue-router 的常见用法

    3.1. 路由重定向

    路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面。

    通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:

    import {createRouter,createWebHashHistory,RouteRecordRaw} from 'vue-router'
    
    import Home from '../views/Home.vue';
    import About from '../views/About.vue';
    import Movie from '../views/Movie.vue';
    
    //路由记录
    let routes:RouteRecordRaw[]=[
        {
            path:"/",
            component:Home
        },
        {
            path:"/movie",
            component:Movie
        },
        {
            path:"/about",
            component:About
        },
        {
            path:"/film",
            redirect:"/movie"
        }
    ];
    
    //创建路由对象
    let router=createRouter({
        history: createWebHashHistory(),  //指定路由模式
        routes
    })
    
    export default router;

    重定向也是通过 routes 配置来完成,下面例子是从 /home 重定向到 /

    const routes = [{ path: '/home', redirect: '/' }]
    

    重定向的目标也可以是一个命名的路由:

    const routes = [{ path: '/home', redirect: { name: 'homepage' } }]
    

    甚至是一个方法,动态返回重定向目标:

    const routes = [
      {
        // /search/screens -> /search?q=screens
        path: '/search/:searchText',
        redirect: to => {
          // 方法接收目标路由作为参数
          // return 重定向的字符串路径/路径对象
          return { path: '/search', query: { q: to.params.searchText } }
        },
      },
      {
        path: '/search',
        // ...
      },
    ]
    

    请注意,导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上。在上面的例子中,在 /home 路由中添加 beforeEnter 守卫不会有任何效果。

    在写 redirect 的时候,可以省略 component 配置,因为它从来没有被直接访问过,所以没有组件要渲染。唯一的例外是嵌套路由:如果一个路由记录有 children 和 redirect 属性,它也应该有 component 属性。

    3.2. 嵌套路由

    通过路由实现组件的嵌套展示,叫做嵌套路由。嵌套路由也称之为子路由,就是在被切换的组件中又切换其他子组件

    例如:在one界面中又有两个按钮,通过这两个按钮进一步切换one中的内容一般都是这种,子路由定义到一级路由里面

    点击父级路由链接显示模板内容                          ① 模板内容中又有子级路由链接

                                            ② 点击子级路由链接显示子级模板内容

    3.1 声明子路由链接和子路由占位符

    在 About.vue 组件中,声明 tab1 和 tab2 的子路由链接以及子路由占位符。示例代码如下:

    <template lang="">
      <div class="about">
        <h2>这是About组件 - 关于</h2>
        <router-link to="/about/tab1">企业文化</router-link> |
        <router-link to="/about/tab2">企业介绍</router-link>
        <hr />
        <div class="viewBox">
          <router-view />
        </div>
      </div>
    </template>
    <script>
    export default {};
    </script>
    <style scoped>
    .about {
      border: 1px solid #ccc;
      background: #def;
      padding: 10px;
      margin: 10px;
    }
    .viewBox {
      min-height: 200px;
      background: #dfe;
    }
    </style>

    3.2 通过 children 属性声明子路由规则

    在 src/router/index.js 路由模块中,导入需要的组件,并使用 children 属性声明子路由规则:

    import {createRouter,createWebHashHistory,RouteRecordRaw} from 'vue-router'
    
    import Home from '../views/Home.vue';
    import About from '../views/About.vue';
    import Movie from '../views/Movie.vue';
    
    import Tab1 from '../views/Tab1.vue';
    import Tab2 from '../views/Tab2.vue';
    
    //路由记录
    let routes:RouteRecordRaw[]=[
        {
            path:"/",
            component:Home
        },
        {
            path:"/movie",
            component:Movie
        },
        {
            path:"/about",
            component:About,
            redirect:"/about/tab1",
            children:[{path:"tab1",component:Tab1},{path:"tab2",component:Tab2}]
        },
        {
            path:"/film",
            redirect:"/movie"
        }
    ];
    
    //创建路由对象
    let router=createRouter({
        history: createWebHashHistory(),  //指定路由模式
        routes
    })
    
    export default router;

    3.3. 带参数的动态路由匹配

    3.3.1、获取路径参数param、query与hash

    思考:有如下 3 个路由链接:

    定义如下 3 个路由规则,是否可行?

    缺点:路由规则的复用性差。

    很多时候,我们需要将给定匹配模式的路由映射到同一个组件。例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数 :

    const User = {
      template: '<div>User</div>',
    }
    
    // 这些都会传递给 `createRouter`
    const routes = [
      // 动态字段以冒号开始
      { path: '/users/:id', component: User },
    ]
    

    现在像 /users/johnny 和 /users/jolyne 这样的 URL 都会映射到同一个路由。

    路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。因此,我们可以通过更新 User 的模板来呈现当前的用户 ID:

    const User = {
      template: '<div>User {{ $route.params.id }}</div>',
    }
    

    你可以在同一个路由中设置有多个 路径参数,它们会映射到 $route.params 上的相应字段。例如:

    匹配模式匹配路径$route.params
    /users/:username /users/eduardo { username: 'eduardo' }
    /users/:username/posts/:postId /users/eduardo/posts/123 { username: 'eduardo', postId: '123' }

    除了 $route.params 之外,$route 对象还公开了其他有用的信息,如 $route.query(如果 URL 中存在参数)、$route.hash 等。你可以在 API 参考中查看完整的细节。

    这个例子的 demo 可以在这里找到。

    <template lang="">
      <div>
        <h2>这是Movie组件 - 电影</h2>
        <h3>当前id={{ $route.params.id }}</h3>
        <h3>当前query={{ $route.query }}</h3>
        <h3>当前hash={{ $route.hash }}</h3>
      </div>
    </template>

    3.3.2、使用 props 接收路由参数

    为了简化路由参数的获取形式,vue-router 允许在路由规则中开启props 传参。示例代码如下:

        {
            name:"movie",
            path:"/movie/:id?",
            component:Movie,
            props:true
        },

    Movie.vue

    <template lang="">
      <div>
        <h2>这是Movie组件 - 电影</h2>
        <h3>当前id={{ $route.params.id }}</h3>
        <h3>当前query={{ $route.query }}</h3>
        <h3>当前hash={{ $route.hash }}</h3>
        <h3>当前id={{ id }} (props直接接收)</h3>
      </div>
    </template>
    <script>
    export default {
      setup() {
        console.log("组件被创建");
      },
      created() {
        this.$watch(
          () => this.$route.params.id,
          (toParam, previousParam) => {
            console.log(toParam, previousParam);
          }
        );
      },
      props: ["id"],
    };
    </script>
    <style lang=""></style>

     

    <template>
      <div class="cls1">
        <h2>这是电影频道 - Movie</h2>
        <h4>当前电影编号:{{ $route.params.id }} 对象:{{ $route.params }}</h4>
        <h4>当前电影类型:{{ $route.params.type }}</h4>
        <h4>
          取出查询(query,问号后的参数):{{ $route.query.money }},{{
            $route.query.name
          }}
          - 对象:{{ $route.query }}
        </h4>
        <h4>取出hash值:{{ $route.hash }}</h4>
        <h4>使用props取出参数的值:id={{ id }},type={{ type }}</h4>
      </div>
    </template>
    <script setup lang="ts">
    let props = defineProps(["id", "type"]);
    console.log(props);
    </script>
    <style scoped>
    .cls1 {
      background: #dfe;
      min-height: 500px;
    }
    .cls1 h2 {
      margin: 0;
      padding: 0;
    }
    </style>

    3.3.3、响应路由参数的变化

    使用带有参数的路由时需要注意的是,当用户从 /users/johnny 导航到 /users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用。

    要对同一个组件中参数的变化做出响应的话,你可以简单地 watch $route 对象上的任意属性,在这个场景中,就是 $route.params :

    const User = {
      template: '...',
      created() {
        this.$watch(
          () => this.$route.params,
          (toParams, previousParams) => {
            // 对路由变化做出响应...
          }
        )
      },
    }
    

    或者,使用 beforeRouteUpdate 导航守卫,它也可以取消导航:

    const User = {
      template: '...',
      async beforeRouteUpdate(to, from) {
        // 对路由变化做出响应...
        this.userData = await fetchUser(to.params.id)
      },
    }
    <template lang="">
      <div>
        <h2>这是Movie组件 - 电影</h2>
        <h3>当前id={{ $route.params.id }}</h3>
        <h3>当前query={{ $route.query }}</h3>
        <h3>当前hash={{ $route.hash }}</h3>
      </div>
    </template>
    <script>
    export default {
      setup() {
        console.log("组件被创建");
      },
      created() {
        this.$watch(
          () => this.$route.params.id,
          (toParam, previousParam) => {
            console.log(toParam, previousParam);
          }
        );
      },
    };
    </script>
    <style lang=""></style>

    3.3.4、捕获所有路由或 404 Not found 路由

    常规参数只匹配 url 片段之间的字符,用 / 分隔。如果我们想匹配任意路径,我们可以使用自定义的 路径参数 正则表达式,在 路径参数 后面的括号中加入 正则表达式 :

    const routes = [
      // 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
      { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
      // 将匹配以 `/user-` 开头的所有内容,并将其放在 `$route.params.afterUser` 下
      { path: '/user-:afterUser(.*)', component: UserGeneric },
    ]
    

    在这个特定的场景中,我们在括号之间使用了自定义正则表达式,并将pathMatch 参数标记为可选可重复。这样做是为了让我们在需要的时候,可以通过将 path 拆分成一个数组,直接导航到路由:

    this.$router.push({
      name: 'NotFound',
      // 保留当前路径并删除第一个字符,以避免目标 URL 以 `//` 开头。
      params: { pathMatch: this.$route.path.substring(1).split('/') },
      // 保留现有的查询和 hash 值,如果有的话
      query: this.$route.query,
      hash: this.$route.hash,
    })
    router/index.ts
    import {createRouter,createWebHashHistory,RouteRecordRaw} from 'vue-router'
    
    import Home from '../views/Home.vue';
    import About from '../views/About.vue';
    import Movie from '../views/Movie.vue';
    
    import Tab1 from '../views/Tab1.vue';
    import Tab2 from '../views/Tab2.vue';
    
    const NotFound={
        template:'<div>没有找到你要访问的页面 404,目标位置:{{$route.params.path}}</div>'
    }
    
    //路由记录
    let routes:RouteRecordRaw[]=[
        {
            path:"/",
            component:Home
        },
        {
            path:"/movie/:id?",
            component:Movie
        },
        {
            path:"/about",
            component:About,
            redirect:"/about/tab1",
            children:[{path:"tab1",component:Tab1},{path:"tab2",component:Tab2}]
        },
        {
            path:"/film",
            redirect:"/movie"
        },
        {
            path:"/:path(.*)*",
            component:NotFound
        }
    ];
    
    //创建路由对象
    let router=createRouter({
        history: createWebHashHistory(),  //指定路由模式
        routes
    })
    
    export default router;

    直接定义组件默认是不允许的

     开启运行时编译

    配置vue.config.js文件,加上下面这一段:

    module.exports = defineConfig({
      transpileDependencies: true,
      runtimeCompiler: true,  // 加上这一段
      // lintOnSave: false,
    })

    记得保存并重新运行项目

    3.4、路由的匹配语法

    3.4.1、在参数中自定义正则

    当定义像 :userId 这样的参数时,我们内部使用以下的正则 ([^/]+) (至少有一个字符不是斜杠 / )来从 URL 中提取参数。这很好用,除非你需要根据参数的内容来区分两个路由。想象一下,两个路由 /:orderId 和 /:productName,两者会匹配完全相同的 URL,所以我们需要一种方法来区分它们。最简单的方法就是在路径中添加一个静态部分来区分它们:

    const routes = [
      // 匹配 /o/3549
      { path: '/o/:orderId' },
      // 匹配 /p/books
      { path: '/p/:productName' },
    ]
    

    但在某些情况下,我们并不想添加静态的 /o /p 部分。由于,orderId 总是一个数字,而 productName 可以是任何东西,所以我们可以在括号中为参数指定一个自定义的正则:

    const routes = [
      // /:orderId -> 仅匹配数字
      { path: '/:orderId(\\d+)' },
      // /:productName -> 匹配其他任何内容
      { path: '/:productName' },
    ]
    

    现在,转到 /25 将匹配 /:orderId,其他情况将会匹配 /:productNameroutes 数组的顺序并不重要!

    TIP

    确保转义反斜杠( \ ),就像我们对 \d (变成\\d)所做的那样,在 JavaScript 中实际传递字符串中的反斜杠字符。

        {
            path:"/:id(\\d{3})?",
            component:Movie
        },

    3.4.2、可选参数

    你也可以通过使用 ? 修饰符(0 个或 1 个)将一个参数标记为可选:

    const routes = [
      // 匹配 /users 和 /users/posva
      { path: '/users/:userId?' },
      // 匹配 /users 和 /users/42
      { path: '/users/:userId(\\d+)?' },
    ]
    

    请注意,* 在技术上也标志着一个参数是可选的,但 ? 参数不能重复。

    3.5、声明式导航 & 编程式导航

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

    3.5.1、导航到不同的位置

    注意:在 Vue 实例中,你可以通过 $router 访问路由实例。因此你可以调用 this.$router.push

    想要导航到不同的 URL,可以使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。

    当你点击 <router-link> 时,内部会调用这个方法,所以点击 <router-link :to="..."> 相当于调用 router.push(...) :

    声明式编程式
    <router-link :to="..."> router.push(...)

    该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:

    // 字符串路径
    router.push('/users/eduardo')
    
    // 带有路径的对象
    router.push({ path: '/users/eduardo' })
    
    // 命名的路由,并加上参数,让路由建立 url
    router.push({ name: 'user', params: { username: 'eduardo' } })
    
    // 带查询参数,结果是 /register?plan=private
    router.push({ path: '/register', query: { plan: 'private' } })
    
    // 带 hash,结果是 /about#team
    router.push({ path: '/about', hash: '#team' })
    

    注意:如果提供了 pathparams 会被忽略,上述例子中的 query 并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name 或手写完整的带有参数的 path :

    const username = 'eduardo'
    // 我们可以手动建立 url,但我们必须自己处理编码
    router.push(`/user/${username}`) // -> /user/eduardo
    // 同样
    router.push({ path: `/user/${username}` }) // -> /user/eduardo
    // 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
    router.push({ name: 'user', params: { username } }) // -> /user/eduardo
    // `params` 不能与 `path` 一起使用
    router.push({ path: '/user', params: { username } }) // -> /user
    

    当指定 params 时,可提供 string 或 number 参数(或者对于可重复的参数可提供一个数组)。任何其他类型(如 undefinedfalse 等)都将被自动字符串化。对于可选参数,你可以提供一个空字符串("")来跳过它。

    由于属性 to 与 router.push 接受的对象种类相同,所以两者的规则完全相同。

    router.push 和所有其他导航方法都会返回一个 Promise,让我们可以等到导航完成后才知道是成功还是失败。我们将在 Navigation Handling 中详细介绍。

    App.vue

    <template>
      <div class="main">
        <router-link to="/">首页</router-link> |
        <router-link to="/movie">电影</router-link> |
        <router-link to="/about">关于</router-link> |
        <router-link to="/film">影视</router-link>
        <router-view></router-view>
      </div>
    
      <button @click="navClick">编程式导航</button>
    </template>
    
    <script lang="ts" setup>
    import { useRouter, useRoute } from "vue-router";
    const router = useRouter();
    const route = useRoute();
    
    function navClick() {
      // 字符串路径
      //router.push("/movie/100");
    
      // 带有路径的对象
      //router.push({ path: "/movie/200" });
    
      // 命名的路由,并加上参数,让路由建立 url
      //router.push({ name: "user", params: { username: "eduardo" } });
    
      // 带查询参数,结果是 /movie?plan=private
      //router.push({ path: "/movie", query: { plan: "private" } });
    
      // 带 hash,结果是 /about#team
      router.push({ path: "/about", hash: "#team" });
    }
    </script>
    
    <style scoped>
    .main {
      width: 400px;
      border: 2px solid #999;
      border-radius: 10px;
      padding: 20px;
    }
    </style>

    router/index.ts

    import {createRouter,createWebHashHistory,RouteRecordRaw} from 'vue-router'
    
    import Home from '../views/Home.vue';
    import About from '../views/About.vue';
    import Movie from '../views/Movie.vue';
    
    import Tab1 from '../views/Tab1.vue';
    import Tab2 from '../views/Tab2.vue';
    
    const NotFound={
        template:'<div>没有找到你要访问的页面 404,目标位置:{{$route.params.path}}</div>'
    }
    
    //路由记录
    let routes:RouteRecordRaw[]=[
        {
            path:"/",
            component:Home
        },
        {
            path:"/:id(\\d{3})?",
            component:Movie
        },
        {
            name:"movie",
            path:"/movie/:id?",
            component:Movie
        },
        {
            path:"/about",
            component:About,
            redirect:"/about/tab1",
            children:[{path:"tab1",component:Tab1},{path:"tab2",component:Tab2}]
        },
        {
            path:"/film",
            redirect:"/movie"
        },
        {
            path:"/:path(.*)*",
            component:NotFound
        }
    ];
    
    //创建路由对象
    let router=createRouter({
        history: createWebHashHistory(),  //指定路由模式
        routes
    })
    
    export default router;

    目标路由组件 target.vue:

    <script setup lang="ts"> //setup写在script标签里,是setup(){}的语法糖
    import {useRoute} from 'vue-router'
    const route = useRoute()
    console.log("name:"+route.query.name)//接收参数
    </script>

    3.5.2、替换当前位置

    它的作用类似于 router.push,唯一不同的是,它在导航时不会向 history 添加新记录,正如它的名字所暗示的那样——它取代了当前的条目。

    声明式编程式
    <router-link :to="..." replace> router.replace(...)

    也可以直接在传递给 router.push 的 routeLocation 中增加一个属性 replace: true :

    router.push({ path: '/home', replace: true })
    // 相当于
    router.replace({ path: '/home' })

    push 和 replace 的区别:
    ⚫ push 会增加一条历史记录
    ⚫ replace 不会增加历史记录,而是替换掉当前的历史记录

    3.5.3、横跨历史

    该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步,类似于 window.history.go(n)

    例子

    // 向前移动一条记录,与 router.forward() 相同
    router.go(1)
    
    // 返回一条记录,与 router.back() 相同
    router.go(-1)
    
    // 前进 3 条记录
    router.go(3)
    
    // 如果没有那么多记录,静默失败
    router.go(-100)
    router.go(100)

     $router.go 的简化用法
    在实际开发中,一般只会前进和后退一层页面。因此 vue-router 提供了如下两个便捷方法:
    ① $router.back()
    ⚫ 在历史记录中,后退到上一个页面

    ② $router.forward()
    ⚫ 在历史记录中,前进到下一个页面

    3.6、命名路由

    除了 path 之外,你还可以为任何路由提供 name。这有以下优点:

    • 没有硬编码的 URL
    • params 的自动编码/解码。
    • 防止你在 url 中出现打字错误。
    • 绕过路径排序(如显示一个)
    const routes = [
      {
        path: '/user/:username',
        name: 'user',
        component: User,
      },
    ]
    

    要链接到一个命名的路由,可以向 router-link 组件的 to 属性传递一个对象:

    <router-link :to="{ name: 'user', params: { username: 'erina' }}">
      User
    </router-link>
    

    这跟代码调用 router.push() 是一回事:

    router.push({ name: 'user', params: { username: 'erina' } })
    

    在这两种情况下,路由将导航到路径 /user/erina

    3.7、命名视图

    3.7.1、实现命名视图

    有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default只允许在app.vue根路由中,否则无效。

    <router-view class="view left-sidebar" name="LeftSidebar"></router-view>
    <router-view class="view main-content"></router-view>
    <router-view class="view right-sidebar" name="RightSidebar"></router-view>
    

    一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置 (带上 s):

    const router = createRouter({
      history: createWebHashHistory(),
      routes: [
        {
          path: '/',
          components: {
            default: Home,
            // LeftSidebar: LeftSidebar 的缩写
            LeftSidebar,
            // 它们与 `<router-view>` 上的 `name` 属性匹配
            RightSidebar,
          },
        },
      ],
    })
    index.ts
        {
            path:"/about",
            components:{
                default:About,
                leftView:Tab1,
                rightView:Tab2
            },
            redirect:"/about/tab1",
            children:[{path:"tab1",component:Tab1},{path:"tab2",component:Tab2}]
        },

    App.vue

    <template>
      <div class="main">
        <router-link to="/">首页</router-link> |
        <router-link :to="{ name: 'movie', params: { id: 98 } }">电影</router-link>
        | <router-link to="/about">关于</router-link> |
        <router-link to="/film">影视</router-link>
        <div class="views">
          <router-view class="view left" name="leftView"></router-view>
          <router-view class="view center"></router-view>
          <router-view class="view right" name="rightView"></router-view>
        </div>
      </div>
    
      <button @click="go(1)">go(1)</button>
      <button @click="go(-1)">go(-1)</button>
    </template>
    
    <script lang="ts" setup>
    import { useRouter, useRoute } from "vue-router";
    const router = useRouter();
    const route = useRoute();
    
    function go(n: number) {
      //router.go(n);
      router.back();
    }
    </script>
    
    <style scoped>
    .main {
      min-width: 400px;
      border: 2px solid #999;
      border-radius: 10px;
      padding: 20px;
    }
    .views {
      display: flex;
    }
    .view {
      flex: 1;
      border: 1px solid #ccc;
      min-width: 300px;
      min-height: 200px;
      margin: 10px;
      padding: 10px;
    }
    </style>

    3.7.2、嵌套命名视图

    我们也有可能使用命名视图创建嵌套视图的复杂布局。这时你也需要命名用到的嵌套 router-view 组件。我们以一个设置面板为例:

    /settings/emails                                       /settings/profile
    +-----------------------------------+                  +------------------------------+
    | UserSettings                      |                  | UserSettings                 |
    | +-----+-------------------------+ |                  | +-----+--------------------+ |
    | | Nav | UserEmailsSubscriptions | |  +------------>  | | Nav | UserProfile        | |
    | |     +-------------------------+ |                  | |     +--------------------+ |
    | |     |                         | |                  | |     | UserProfilePreview | |
    | +-----+-------------------------+ |                  | +-----+--------------------+ |
    +-----------------------------------+                  +------------------------------+
    
    • Nav 只是一个常规组件。
    • UserSettings 是一个视图组件。
    • UserEmailsSubscriptionsUserProfileUserProfilePreview 是嵌套的视图组件。

    注意:我们先忘记 HTML/CSS 具体的布局的样子,只专注在用到的组件上。

    UserSettings 组件的 <template> 部分应该是类似下面的这段代码:

    <!-- UserSettings.vue -->
    <div>
      <h1>User Settings</h1>
      <NavBar />
      <router-view />
      <router-view name="helper" />
    </div>
    

    那么你就可以通过这个路由配置来实现上面的布局:

    {
      path: '/settings',
      // 你也可以在顶级路由就配置命名视图
      component: UserSettings,
      children: [{
        path: 'emails',
        component: UserEmailsSubscriptions
      }, {
        path: 'profile',
        components: {
          default: UserProfile,
          helper: UserProfilePreview
        }
      }]
    }

    3.8、别名

    重定向是指当用户访问 /home 时,URL 会被 / 替换,然后匹配成 /。那么什么是别名呢?

    将 / 别名为 /home,意味着当用户访问 /home 时,URL 仍然是 /home,但会被匹配为用户正在访问 /

    上面对应的路由配置为:

    const routes = [{ path: '/', component: Homepage, alias: '/home' }]
        {
            name:"movie",
            path:"/movie/:id?",
            component:Movie,
            props:true,
            alias:["/film"]
        },
        {
            path:"/film",
            redirect:"/movie",
        },

    通过别名,你可以自由地将 UI 结构映射到一个任意的 URL,而不受配置的嵌套结构的限制。使别名以 / 开头,以使嵌套路径中的路径成为绝对路径。你甚至可以将两者结合起来,用一个数组提供多个别名:

    const routes = [
      {
        path: '/users',
        component: UsersLayout,
        children: [
          // 为这 3 个 URL 呈现 UserList
          // - /users
          // - /users/list
          // - /people
          { path: '', component: UserList, alias: ['/people', 'list'] },
        ],
      },
    ]
    

    如果你的路由有参数,请确保在任何绝对别名中包含它们:

    const routes = [
      {
        path: '/users/:id',
        component: UsersByIdLayout,
        children: [
          // 为这 3 个 URL 呈现 UserDetails
          // - /users/24
          // - /users/24/profile
          // - /24
          { path: 'profile', component: UserDetails, alias: ['/:id', ''] },
        ],
      },
    ]

        {
            name:"movie",
            path:"/movie/:id?",
            component:Movie,
            props:true,
            alias:["/film","/:id(\\d*)"]
        },

    3.9、不同的路由模式

    在创建路由器实例时,history 配置允许我们在不同的历史模式中进行选择。

    3.9.1、Hash 模式

    hash 模式是用 createWebHashHistory() 创建的:

    import { createRouter, createWebHashHistory } from 'vue-router'
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes: [
        //...
      ],
    })
    

    它在内部传递的实际 URL 之前使用了一个哈希字符(#)。由于这部分 URL 从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响。如果你担心这个问题,可以使用 HTML5 模式。

    3.9.2、HTML5 模式

    用 createWebHistory() 创建 HTML5 模式,推荐使用这个模式:

    import { createRouter, createWebHistory } from 'vue-router'
    
    const router = createRouter({
      history: createWebHistory(),
      routes: [
        //...
      ],
    })
    

    当使用这种历史模式时,URL 会看起来很 "正常",例如 https://example.com/user/id。漂亮!

    不过,问题来了。由于我们的应用是一个单页的客户端应用,如果没有适当的服务器配置,用户在浏览器中直接访问 https://example.com/user/id,就会得到一个 404 错误。这就尴尬了。

    不用担心:要解决这个问题,你需要做的就是在你的服务器上添加一个简单的回退路由。如果 URL 不匹配任何静态资源,它应提供与你的应用程序中的 index.html 相同的页面。漂亮依旧!

    注意:使用HTML5模式,参数监听会失效。

    3.10、链接样式

    3.10.1、active-class

    • 类型:string

    • 默认值:"router-link-active" (或者全局 linkActiveClass)

    • 详细内容:

      链接激活时,应用于渲染的 <a> 的 class。

    3.10.2、linkActiveClass

    用于激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-active

    3.10.3、exact-active-class

    • 类型:string

    • 默认值:"router-link-exact-active" (或者全局 linkExactActiveClass)

    • 详细内容:

      链接精准激活时,应用于渲染的 <a> 的 class。

    3.10.4、linkExactActiveClass

    用于精准激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-exact-active

     

    四、高级应用

    4.1、 导航守卫

    导航守卫可以控制路由的访问权限。示意图如下:

    正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。

    4.1.1、全局前置守卫

    你可以使用 router.beforeEach 注册一个全局前置守卫:

    const router = createRouter({ ... })
    
    router.beforeEach((to, from) => {
      // ...
      // 返回 false 以取消导航
      return false
    })
    

    当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。

    每个守卫方法接收两个参数:

    可以返回的值如下:

    • false: 取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • 一个路由地址: 通过一个路由地址跳转到一个不同的地址,就像你调用 router.push() 一样,你可以设置诸如 replace: true 或 name: 'home' 之类的配置。当前的导航被中断,然后进行一个新的导航,就和 from 一样。
     router.beforeEach(async (to, from) => {
       if (
         // 检查用户是否已登录
         !isAuthenticated &&
         // ❗️ 避免无限重定向
         to.name !== 'Login'
       ) {
         // 将用户重定向到登录页面
         return { name: 'Login' }
       }
     })
    

    如果遇到了意料之外的情况,可能会抛出一个 Error。这会取消导航并且调用 router.onError() 注册过的回调。

    如果什么都没有,undefined 或返回 true,则导航是有效的,并调用下一个导航守卫

    以上所有都同 async 函数 和 Promise 工作方式一样:

    router.beforeEach(async (to, from) => {
      // canUserAccess() 返回 `true` 或 `false`
      const canAccess = await canUserAccess(to)
      if (!canAccess) return '/login'
    })

    非next写法
    //添加导航守卫
    //每一个导航的开始都必须经过该函数,钩子
    router.beforeEach((to,from)=>{
        console.log("---开始导航了beforeEach---");
        console.log("目标位置:",to);
        console.log("原来位置:",from);
        //1、如果返回值为true或无返回值,没有next参数,则正常访问默认路径
        //return true;
        //2、如果返回值为false则阻塞访问,拒绝访问
        //return false;
        //3、返回路径对象,则导航到指定路径
        if(to.name!=="login"&&!authentication()){  //如果当前路由不是login则跳转到login
            return {name:"login"}
        }
        return true; //放行
    });
    
    //用户是否登录
    function authentication(){
        let username=localStorage.getItem("USERNAME");
        return !!username;
    }

    next写法

    //添加导航守卫
    //每一个导航的开始都必须经过该函数,钩子
    router.beforeEach((to,from,next)=>{
        //1、如果不调用next则导航阻塞
        //2、如果直接调用next不带参数则表示放行
        //next();
        //3、可以在next中指定路径对象
        if(to.name!=="login"&&!authentication()){
            next({name:"login"});
        }
        next();
    });
    
    //用户是否登录
    function authentication(){
        let username=localStorage.getItem("USERNAME");
        return !!username;
    }

    4.1.2、全局后置钩子

    你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

    router.afterEach((to, from) => {
      sendToAnalytics(to.fullPath)
    })
    

    它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。

    它们也反映了 navigation failures 作为第三个参数:

    router.afterEach((to, from, failure) => {
      if (!failure) sendToAnalytics(to.fullPath)
    })

    4.1.3、路由独享的守卫

    你可以直接在路由配置上定义 beforeEnter 守卫:

    const routes = [
      {
        path: '/users/:id',
        component: UserDetails,
        beforeEnter: (to, from) => {
          // reject the navigation
          return false
        },
      },
    ]
    

    beforeEnter 守卫 只在进入路由时触发,不会在 paramsquery 或 hash 改变时触发。例如,从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects。它们只有在 从一个不同的 路由导航时,才会被触发。

    你也可以将一个函数数组传递给 beforeEnter,这在为不同的路由重用守卫时很有用:

    function removeQueryParams(to) {
      if (Object.keys(to.query).length)
        return { path: to.path, query: {}, hash: to.hash }
    }
    
    function removeHash(to) {
      if (to.hash) return { path: to.path, query: to.query, hash: '' }
    }
    
    const routes = [
      {
        path: '/users/:id',
        component: UserDetails,
        beforeEnter: [removeQueryParams, removeHash],
      },
      {
        path: '/about',
        component: UserDetails,
        beforeEnter: [removeQueryParams],
      },
    ]
    

    请注意,你也可以通过使用路径 meta 字段全局导航守卫来实现类似的行为。

    4.1.4、组件内的守卫

    最后,你可以在路由组件内直接定义路由导航守卫(传递给路由配置的)

    你可以为路由组件添加以下配置:

    • beforeRouteEnter
    • beforeRouteUpdate
    • beforeRouteLeave
    const UserDetails = {
      template: `...`,
      beforeRouteEnter(to, from) {
        // 在渲染该组件的对应路由被验证前调用
        // 不能获取组件实例 `this` !
        // 因为当守卫执行时,组件实例还没被创建!
      },
      beforeRouteUpdate(to, from) {
        // 在当前路由改变,但是该组件被复用时调用
        // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
        // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
        // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
      },
      beforeRouteLeave(to, from) {
        // 在导航离开渲染该组件的对应路由时调用
        // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
      },
    }
    

    beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

    不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:

    beforeRouteEnter (to, from, next) {
      next(vm => {
        // 通过 `vm` 访问组件实例
      })
    }
    

    注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持 传递回调,因为没有必要了:

    beforeRouteUpdate (to, from) {
      // just use `this`
      this.name = to.params.name
    }
    

    这个 离开守卫 通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回 false 来取消。

    beforeRouteLeave (to, from) {
      const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
      if (!answer) return false
    }
    

    使用组合 API

    如果你正在使用组合 API 和 setup 函数来编写组件,你可以通过 onBeforeRouteUpdate 和 onBeforeRouteLeave 分别添加 update 和 leave 守卫。 请参考组合 API 部分以获得更多细节。

    简单示例

    Login.vue

    <template>
      <h2>用户登录</h2>
      <fieldset>
        <legend>用户信息</legend>
        <p>
          <label>帐号:</label>
          <input v-model="user.username" />
        </p>
        <p>
          <label>密码:</label>
          <input v-model="user.password" type="password" />
        </p>
        <p>
          <button @click.prevent="login">登录</button>
        </p>
      </fieldset>
    </template>
    <script setup lang="ts">
    import { useRoute, useRouter } from "vue-router";
    
    let route = useRoute();
    let router = useRouter();
    
    console.log(route);
    console.log(router);
    
    let user = { username: "", password: "" };
    
    function login() {
      if (user.username === "admin" && user.password === "123456") {
        localStorage.setItem("USERNAME", user.username);
        //获取上一个请求的路径,返回路径,如果没有则直接进入main
        let returnUrl = route.query.returnUrl || "/main";
        router.replace({ path: returnUrl + "" });
      }
    }
    </script>
    <style scoped></style>

    Main.vue

    <template>
      <h2>用户后台</h2>
      <div>
        <p><a href="#">修改密码</a> | <a href="#">安全退出</a></p>
      </div>
    </template>
    <script setup lang="ts"></script>
    <style scoped></style>

    route/index.ts

    import {createRouter,createWebHashHistory,createWebHistory,RouteRecordRaw} from 'vue-router'
    
    import Home from '../views/Home.vue';
    import Movie from '../views/Movie.vue';
    import About from '../views/About.vue';
    
    import Culture from '../views/Culture.vue';
    import Introduce from '../views/Introduce.vue';
    
    import Login from '../views/Login.vue';
    import Main from '../views/Main.vue';
    
    //快速定义组件的方法
    const NotFound={
        template:'<p>没有找到您要访问的内容!</p> <p>{{$route.params}}</p> <p>{{anypath}}</p>',
        props:['anypath']
    }
    
    //路由记录集合
    let routes:RouteRecordRaw[]=[
        {
            path:"/",
            component:Home
        },
        {
            path:"/home",
            component:Home
        },
        {
            name:"movie",
            path:"/movie/:id?/:type?",  //如果在参数后增加问号则参数变成可选参数,否则为必选参数
            component:Movie,
            props:true,  //是否允许将参数映射到组件的props中
            alias:["/film","/m","/:mid(\\d+)"],
            beforeEnter:[RemoveParam,RemoveHash]
        },
        {
            path:"/about",
            name:"about",
            components:{
                leftView:Culture,
                default:About,
                rightView:Introduce
            },
            redirect:"/about/culture",
            children:[  //子路由,不用以/开始
                {path:"culture",component:Culture},
                {path:"introduce",component:Introduce}
            ]
        },
        {
            path:"/film",
            redirect:"/movie"  //重定向
        },
        {
            name:"login",
            path:"/login",
            component:Login
        },
        {
            name:"main",
            path:"/main",
            component:Main
        },
        {
            path:"/:anypath(.*)*",
            component:NotFound,
            props:true
        }
    ];
    
    //创建路由器
    let router=createRouter({
        history:createWebHashHistory(),  //指定路由模式为hash模式(兼容性好,但带#)
        //history:createWebHistory(),  //HTML5模式,路径中没有#号,与正常的地址一样
        routes,
        linkActiveClass:"active",
        linkExactActiveClass:"exact"
    });
    
    // //添加导航守卫
    // //每一个导航的开始都必须经过该函数,钩子
    // router.beforeEach((to,from)=>{
    //     console.log("---开始导航了beforeEach---");
    //     console.log("目标位置:",to);
    //     console.log("原来位置:",from);
    //     //1、如果返回值为true或无返回值,没有next参数,则正常访问默认路径
    //     //return true;
    //     //2、如果返回值为false则阻塞访问,拒绝访问
    //     //return false;
    //     //3、返回路径对象,则导航到指定路径
    //     if(to.name!=="login"&&!authentication()){  //如果当前路由不是login则跳转到login
    //         return {name:"login"}
    //     }
    //     return true; //放行
    // });
    
    //添加导航守卫
    //每一个导航的开始都必须经过该函数,钩子
    router.beforeEach((to,from,next)=>{
         console.log("---开始导航了beforeEach---");
        //1、如果不调用next则导航阻塞
        //2、如果直接调用next不带参数则表示放行
        //next();
        //3、可以在next中指定路径对象
        if(to.name!=="login"&&!authentication()){
            next({name:"login",query:{returnUrl:to.path}});
        }
        next();
    });
    
    //导航后
    router.afterEach((to,from)=>{
        console.log("---结束导航了afterEach---");
    })
    
    //用户是否登录
    function authentication(){
        let username=localStorage.getItem("USERNAME");
        return !!username;
    }
    
    //移除导航中的参数
    function RemoveParam(to){
        console.log("---路由独享的守卫开始---");
        if(Object.keys(to.params).length){//如果有参数
            //return {name:to.name,path:to.path,query:to.query,hash:to.hash,params:{}}
            to.params={};
            return to;
        }
        return true;
    }
    //移除导航中的hash
    function RemoveHash(to){
        if(to.hash){//如果有hash
            to.hash=undefined;
            return to;
        }
        return true;
    }
    
    //导出
    export default router;

     

    4.1.5、完整的导航解析流程

    1. 导航被触发。
    2. 在失活的组件里调用 beforeRouteLeave 守卫。
    3. 调用全局的 beforeEach 守卫。
    4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
    5. 在路由配置里调用 beforeEnter
    6. 解析异步路由组件。
    7. 在被激活的组件里调用 beforeRouteEnter
    8. 调用全局的 beforeResolve 守卫(2.5+)。
    9. 导航被确认。
    10. 调用全局的 afterEach 钩子。
    11. 触发 DOM 更新。
    12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

    4.2、路由元信息

    有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现,并且它可以在路由地址和导航守卫上都被访问到。定义路由的时候你可以这样配置 meta 字段:

    const routes = [
      {
        path: '/posts',
        component: PostsLayout,
        children: [
          {
            path: 'new',
            component: PostsNew,
            // 只有经过身份验证的用户才能创建帖子
            meta: { requiresAuth: true }
          },
          {
            path: ':id',
            component: PostsDetail
            // 任何人都可以阅读文章
            meta: { requiresAuth: false }
          }
        ]
      }
    ]
    

    那么如何访问这个 meta 字段呢?

    首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,它可能匹配多个路由记录。

    例如,根据上面的路由配置,/posts/new 这个 URL 将会匹配父路由记录 (path: '/posts') 以及子路由记录 (path: 'new')。

    一个路由匹配到的所有路由记录会暴露为 $route 对象(还有在导航守卫中的路由对象)的$route.matched 数组。我们需要遍历这个数组来检查路由记录中的 meta 字段,但是 Vue Router 还为你提供了一个 $route.meta 方法,它是一个非递归合并所有 meta 字段的(从父字段到子字段)的方法。这意味着你可以简单地写

    router.beforeEach((to, from) => {
      // 而不是去检查每条路由记录
      // to.matched.some(record => record.meta.requiresAuth)
      if (to.meta.requiresAuth && !auth.isLoggedIn()) {
        // 此路由需要授权,请检查是否已登录
        // 如果没有,则重定向到登录页面
        return {
          path: '/login',
          // 保存我们所在的位置,以便以后再来
          query: { redirect: to.fullPath },
        }
      }
    })

    4.3、路由懒加载

    4.3.1、什么是路由懒加载?

    整个网页默认是刚打开就去加载所有页面,路由懒加载就是只加载你当前点击的那个模块。

    按需去加载路由对应的资源,提高首屏加载速度(tip:首页不用设置懒加载,而且一个页面加载过后再次访问不会重复加载)。

    实现原理:将路由相关的组件,不再直接导入了,而是改写成异步组件的写法,只有当函数被调用的时候,才去加载对应的组件内容。

    4.3.2.传统路由配置

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Login from '@/views/login/index.vue'
    import Home from '@/views/home/home.vue'
    Vue.use(VueRouter)
    const router = new VueRouter({
    routes: [
    { path: '/login', component: Login },
    { path: '/home', component: Home }
    ]
    
    export default router

    4.3.3.路由懒加载写法

    import Vue from 'vue'
    import VueRouter from 'vue-router'
     
    //const  Login = ()=> {
    //    return  import('@/views/login/index.vue')
    //}
    //const  Home = ()=> {
    //    return  import('@/views/home/home.vue')
    //}
    //有return且函数体只有一行,所以省略后为
    const  Login = ()=> import('@/views/login/index.vue') 
     
    const  Home = ()=>  import('@/views/home/home.vue')
     
    Vue.use(VueRouter)
    const router = new VueRouter({
     routes: [
        { path: '/login', component: Login },
        { path: '/home', component: Home }
      ]
     
    export default router

    再次简化(省去定义变量):

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)
    const router = new VueRouter({
    routes: [
    { path: '/login', component: () => import('@/views/login/index.vue') },
    { path: '/home', component: () => import('@/views/home/home.vue') }
    ]
    
    export default router

    当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

    Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:

    // 将
    // import UserDetails from './views/UserDetails.vue'
    // 替换成
    const UserDetails = () => import('./views/UserDetails.vue')
    
    const router = createRouter({
      // ...
      routes: [{ path: '/users/:id', component: UserDetails }],
    })
    

    component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。这意味着你也可以使用更复杂的函数,只要它们返回一个 Promise :

    const UserDetails = () =>
      Promise.resolve({
        /* 组件定义 */
      })
    

    一般来说,对所有的路由都使用动态导入是个好主意。

    注意

    不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。

    如果你使用的是 webpack 之类的打包器,它将自动从代码分割中受益。

    如果你使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 正确地解析语法。

    没有动态导入前所有的模块打包在同一个js文件中:

     添加动态导入:

    import {createRouter,createWebHashHistory,createWebHistory,RouteRecordRaw} from 'vue-router'
    
    import Movie from '../views/Movie.vue';
    import About from '../views/About.vue';
    
    import Culture from '../views/Culture.vue';
    import Introduce from '../views/Introduce.vue';
    
    import Login from '../views/Login.vue';
    import Main from '../views/Main.vue';
    
    //动态导入,按需加载
    const Home=()=>import('../views/Home.vue');
    
    //快速定义组件的方法
    const NotFound={
        template:'<p>没有找到您要访问的内容!</p> <p>{{$route.params}}</p> <p>{{anypath}}</p>',
        props:['anypath']
    }
    
    //路由记录集合
    let routes:RouteRecordRaw[]=[
        {
            path:"/",
            component:Home
        },
        {
            path:"/home",
            component:Home,
            meta:{
                "key":"value",  //可以自定义任意数据对象
                "requiredAuth":false //是否需要验证
            }
        },
        {
            name:"movie",
            path:"/movie/:id?/:type?",  //如果在参数后增加问号则参数变成可选参数,否则为必选参数
            component:Movie,
            props:true,  //是否允许将参数映射到组件的props中
            alias:["/film","/m","/:mid(\\d+)"],
            beforeEnter:[RemoveParam,RemoveHash],
            meta:{
                "requiredAuth":true //是否需要验证
            }
        },
        {
            path:"/about",
            name:"about",
            components:{
                leftView:Culture,
                default:About,
                rightView:Introduce
            },
            redirect:"/about/culture",
            children:[  //子路由,不用以/开始
                {path:"culture",component:Culture},
                {path:"introduce",component:Introduce}
            ],
            meta:{
                "requiredAuth":false //是否需要验证
            }
        },
        {
            path:"/film",
            redirect:"/movie"  //重定向
        },
        {
            name:"login",
            path:"/login",
            component:import("../views/Login.vue"),
            meta:{
                "requiredAuth":false //是否需要验证
            }
        },
        {
            name:"main",
            path:"/main",
            component:Main,
            meta:{
                "requiredAuth":true //是否需要验证
            }
        },
        {
            path:"/:anypath(.*)*",
            component:NotFound,
            props:true,
            meta:{
                "requiredAuth":false //是否需要验证
            }
        }
    ];
    
    //创建路由器
    let router=createRouter({
        history:createWebHashHistory(),  //指定路由模式为hash模式(兼容性好,但带#)
        //history:createWebHistory(),  //HTML5模式,路径中没有#号,与正常的地址一样
        routes,
        linkActiveClass:"active",
        linkExactActiveClass:"exact"
    });
    
    // //添加导航守卫
    // //每一个导航的开始都必须经过该函数,钩子
    router.beforeEach((to,from)=>{
        //console.log("---开始导航了beforeEach---");
        //console.log("目标位置:",to);
        //console.log("原来位置:",from);
        //1、如果返回值为true或无返回值,没有next参数,则正常访问默认路径
        //return true;
        //2、如果返回值为false则阻塞访问,拒绝访问
        //return false;
        //3、返回路径对象,则导航到指定路径
        console.log(to.meta.key,to.meta.requiredAuth);
        if(to.meta.requiredAuth&&to.name!=="login"&&!authentication()){  //如果当前路由不是login则跳转到login
            return {name:"login",query:{returnUrl:to.path}}
        }
        return true; //放行
    });
    
    //添加导航守卫
    //每一个导航的开始都必须经过该函数,钩子
    // router.beforeEach((to,from,next)=>{
    //      console.log("---开始导航了beforeEach---");
    //     //1、如果不调用next则导航阻塞
    //     //2、如果直接调用next不带参数则表示放行
    //     //next();
    //     //3、可以在next中指定路径对象
    //     if(to.name!=="login"&&!authentication()){
    //         next({name:"login",query:{returnUrl:to.path}});
    //     }
    //     next();
    // });
    
    //导航后
    router.afterEach((to,from)=>{
        console.log("---结束导航了afterEach---");
    })
    
    //用户是否登录
    function authentication(){
        let username=localStorage.getItem("USERNAME");
        return !!username;
    }
    
    //移除导航中的参数
    function RemoveParam(to){
        console.log("---路由独享的守卫开始---");
        if(Object.keys(to.params).length){//如果有参数
            //return {name:to.name,path:to.path,query:to.query,hash:to.hash,params:{}}
            to.params={};
            return to;
        }
        return true;
    }
    //移除导航中的hash
    function RemoveHash(to){
        if(to.hash){//如果有hash
            to.hash=undefined;
            return to;
        }
        return true;
    }
    
    //导出
    export default router;
    View Code

     可见Login与Home组件被单独加载。

    4.3.4、把组件按组分块

    有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):

    const UserDetails = () =>
      import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
    const UserDashboard = () =>
      import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
    const UserProfileEdit = () =>
      import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')
    

    webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。

    import {createRouter,createWebHashHistory,createWebHistory,RouteRecordRaw} from 'vue-router'
    
    import Movie from '../views/Movie.vue';
    import About from '../views/About.vue';
    
    import Culture from '../views/Culture.vue';
    import Introduce from '../views/Introduce.vue';
    
    import Login from '../views/Login.vue';
    import Main from '../views/Main.vue';
    
    //动态导入,按需加载
    const Home=()=>import(/* webpackChunkName: "foo" */'../views/Home.vue');
    
    //快速定义组件的方法
    const NotFound={
        template:'<p>没有找到您要访问的内容!</p> <p>{{$route.params}}</p> <p>{{anypath}}</p>',
        props:['anypath']
    }
    
    //路由记录集合
    let routes:RouteRecordRaw[]=[
        {
            path:"/",
            component:Home
        },
        {
            path:"/home",
            component:Home,
            meta:{
                "key":"value",  //可以自定义任意数据对象
                "requiredAuth":false //是否需要验证
            }
        },
        {
            name:"movie",
            path:"/movie/:id?/:type?",  //如果在参数后增加问号则参数变成可选参数,否则为必选参数
            component:Movie,
            props:true,  //是否允许将参数映射到组件的props中
            alias:["/film","/m","/:mid(\\d+)"],
            beforeEnter:[RemoveParam,RemoveHash],
            meta:{
                "requiredAuth":true //是否需要验证
            }
        },
        {
            path:"/about",
            name:"about",
            components:{
                leftView:Culture,
                default:About,
                rightView:Introduce
            },
            redirect:"/about/culture",
            children:[  //子路由,不用以/开始
                {path:"culture",component:Culture},
                {path:"introduce",component:Introduce}
            ],
            meta:{
                "requiredAuth":false //是否需要验证
            }
        },
        {
            path:"/film",
            redirect:"/movie"  //重定向
        },
        {
            name:"login",
            path:"/login",
            component:import(/* webpackChunkName: "foo" */"../views/Login.vue"),
            meta:{
                "requiredAuth":false //是否需要验证
            }
        },
        {
            name:"main",
            path:"/main",
            component:Main,
            meta:{
                "requiredAuth":true //是否需要验证
            }
        },
        {
            path:"/:anypath(.*)*",
            component:NotFound,
            props:true,
            meta:{
                "requiredAuth":false //是否需要验证
            }
        }
    ];
    
    //创建路由器
    let router=createRouter({
        history:createWebHashHistory(),  //指定路由模式为hash模式(兼容性好,但带#)
        //history:createWebHistory(),  //HTML5模式,路径中没有#号,与正常的地址一样
        routes,
        linkActiveClass:"active",
        linkExactActiveClass:"exact"
    });
    
    // //添加导航守卫
    // //每一个导航的开始都必须经过该函数,钩子
    router.beforeEach((to,from)=>{
        //console.log("---开始导航了beforeEach---");
        //console.log("目标位置:",to);
        //console.log("原来位置:",from);
        //1、如果返回值为true或无返回值,没有next参数,则正常访问默认路径
        //return true;
        //2、如果返回值为false则阻塞访问,拒绝访问
        //return false;
        //3、返回路径对象,则导航到指定路径
        console.log(to.meta.key,to.meta.requiredAuth);
        if(to.meta.requiredAuth&&to.name!=="login"&&!authentication()){  //如果当前路由不是login则跳转到login
            return {name:"login",query:{returnUrl:to.path}}
        }
        return true; //放行
    });
    
    //添加导航守卫
    //每一个导航的开始都必须经过该函数,钩子
    // router.beforeEach((to,from,next)=>{
    //      console.log("---开始导航了beforeEach---");
    //     //1、如果不调用next则导航阻塞
    //     //2、如果直接调用next不带参数则表示放行
    //     //next();
    //     //3、可以在next中指定路径对象
    //     if(to.name!=="login"&&!authentication()){
    //         next({name:"login",query:{returnUrl:to.path}});
    //     }
    //     next();
    // });
    
    //导航后
    router.afterEach((to,from)=>{
        console.log("---结束导航了afterEach---");
    })
    
    //用户是否登录
    function authentication(){
        let username=localStorage.getItem("USERNAME");
        return !!username;
    }
    
    //移除导航中的参数
    function RemoveParam(to){
        console.log("---路由独享的守卫开始---");
        if(Object.keys(to.params).length){//如果有参数
            //return {name:to.name,path:to.path,query:to.query,hash:to.hash,params:{}}
            to.params={};
            return to;
        }
        return true;
    }
    //移除导航中的hash
    function RemoveHash(to){
        if(to.hash){//如果有hash
            to.hash=undefined;
            return to;
        }
        return true;
    }
    
    //导出
    export default router;
    View Code

     因为指定了chunk的名称,所有Home与Login被打包在一起了。

    https://cli.vuejs.org/zh/

    需要注意的是:/* webpackChunkName: "group-user" */ 魔术注释只是指定了[name],可以在配置文件中指定新的名称,修改vue.config.js文件如下:

    const { defineConfig } = require('@vue/cli-service')
    module.exports = defineConfig({
      transpileDependencies: true,
      runtimeCompiler:true,  //开启运行时编译
      configureWebpack: {
      output: {
        filename: `[name].[hash].js`,
        chunkFilename: `js/[name].[chunkhash].js`
      }
    }
    })

    运行效果如下:

     此处可以发现使用了文件的hash值,可以解决客户端缓存的问题。

    五、登录综合示例(Vue3+Vue-Router+JWT)

    如果你没有JWT基础,请查看:《SpringBoot学习笔记(八)——JWT》

  • 相关阅读:
    业余无线电A类考试准备笔记
    关于互联网技术基层绩效管理的一些思考
    适合产品经理的十本书 From 俞军
    从敏捷开发到微服务,maybe再到中台
    Golang内存模型
    CSS中的那点事儿(一)--- CSS中的单位2
    CSS中的那点事儿(一)--- CSS中的单位1
    design.js
    model.js
    云技术相关的概念
  • 原文地址:https://www.cnblogs.com/best/p/16870187.html
Copyright © 2020-2023  润新知