路由的定义

后端路由:对于普通的网站,所有的超链接都是URL地址,所有的URL地址都对应服务器上的资源。

前端路由:对于单页面应用程序来说,主要通过URL中的hash(#号)实现不同页面之间的切换。hash的特点是HTTP请求中不会包含hash相关的内容,类似于超链接中的锚点。这种通过改变hash来切换页面的方式称为前端路由。

路由可以让应用程序根据用户输入的不同地址动态挂载不同的组件。

Vue Router是Vue.js的官方路由管理器。

安装:

1
npm install vue-router@4

vue-router的基本使用

通过使用一个自定义组件router-link来创建链接,可以使得vue-router在不重新加载页面的情况下更改URL,并处理URL的生成及编码。<router-link>组件支持用户在具有路由功能的应用中单击导航。通过to属性指定目标地址,默认渲染成带有正确链接的<a>标签。

<router-link>比<a href=”…”>会更好一些,理由如下:

  1. 无论是HTML5 history模式还是hash模式,router-link的表现一致。
  2. 在HTML5 history模式下,router-link会守卫点击事件,让浏览器不再重新加载页面。
  3. 当在HTML5 history模式下使用base作为选项后,所有的to属性都不需要再写基路径。base配置作为createWebHistory的第一个参数传递。

Vue3中删除了<router-link>中的event和tag属性,可以使用v-slot API来完全定制<router-link>。

设置选中路由高亮

当目标路由成功激活时,链接元素会自动设置一个表示激活的CSS类名”router-link-active router-link-exact-active”

可以通过设置样式实现高亮。

另一种方式是使用router-link中的active-class属性或者exact-active-class。

1
2
3
4
5
6
7
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
// history:createWebHashHistory(process.env.BASE_URL), //hash模式
routes, // `routes: routes` 的缩写
linkActiveClass: "active",
linkExactActiveClass: "exact-active",
});

其中exact-active-class当链接精准激活时应用于渲染的<a>的class。

router-view

专门用来当做占位符,router-view将显示与url对应的组件。可以把它放在任何地方以适应布局。当跳转到router-link中指定的路由地址时,路由对应的组件内容会被加载到router-view中展示。

<router-view>渲染的组件还可以内嵌自己的<router-view>,并根据嵌套路径,渲染嵌套组件。因为<router-view>也是个组件,所以可以配合<transition>和<keep-alive>使用。

router/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
// 1. 定义路由组件,也可以从其他文件导入
// import Home from '../views/Home.vue';
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "Home",
component: () => import("../views/Home.vue"),
redirect: "/about", //页面一加载时,默认跳转到about组件
},
{
path: "/about",
name: "About",
//组件懒加载
component: () => import("../views/About.vue"),
},
];
// import {createWebHashHistory} from 'vue-router';
// 3. 创建路由实例并传递 `routes` 配置,你可以在这里输入更多的配置
const router = createRouter({
// 4. 内部提供了 history 模式的实现。
history: createWebHistory(process.env.BASE_URL),
routes, // `routes: routes` 的缩写
});

export default router;

router:路由匹配规则。每个路由规则都是一个对象,这个规则对象身上有以下两个必须的属性:

  1. path:表示监听哪个路由链接地址
  2. component:表示如果路由是前面匹配到的path,则展示component属性对应的那个组件
    注意:component的属性值必须是一个组件的模板对象,不能是组件的引用名称。

redirect:路由重定向,表示当浏览器访问根路由时自动跳转到指定组件。

通过调用app.use(router),我们可以在任意组件中以this.$router的形式访问它,并且以this.$route的形式访问当前路由。如果要在setup函数中访问路由,请调用useRouter或useRoute函数,因为在setup函数中是无法调用this对象的。

路由HTML5 History模式和hash模式

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

hash模式

hash模式是用createWebHashHistory()创建的
URL地址格式:http://localhost:8080/#/login,它在内部传递的实际URL之前使用了一个哈希字符(#)。
由于这部分URL从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。

HTML5 history模式

用createWebHistory创建HTML5模式,推荐使用。当使用这种历史模式时,URL会看起来很正常,例如http://localhost:8080/login。不过也有个问题,如果应用是单页面,如果没有适当的服务器配置,用户在浏览器中直接访问http://localhost:8080/login,就会得到一个404错误。需要在服务器上添加一个回退路由,即配置伪静态。

带参数的动态路由匹配

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
{{$route.params.name }}
<p> {{name}} </p>
<p> {{userData}} </p>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";

export default defineComponent({
setup() {
const route = useRoute();
const router = useRouter();
console.log(route.params, router);
const name = ref<string | string[]>("");
name.value = route.params.name;

const userData = ref();

// 当参数更改时获取用户信息
watch(
() => route.params,
(newParams) => {
console.log('newParams',newParams);
userData.value = newParams.name;
}
);

return { name, userData };
},
});
</script>

<style scoped>
</style>

注意:在模板中仍然可以访问$router和$route,所以不需要在setup中返回router或route。

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

响应路由参数的变化

需要注意的是,当用户从/user/A导航到/user/B时,相同的组件实例将被重复使用,这意味着组件的生命周期钩子函数不会被调用。

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

实例见上一小节。

编程式导航

在网页中,有以下两种界面跳转方式:

  • 使用a标签
  • 使用window.location.href或者this.$router.push({}),即编程式导航
1
2
3
4
const router = useRouter();

router.push("/book")
router.push({name: "book", params: {bookId: 1}})

路由传参query&params

路由传参通常有query和params两种方式,这两种方式都是通过修改URL地址实现的,路由对URL参数进行解析即可获取相应的参数。

query

1
<router-link to="/login?name=yujie&pwd=123">登录</router-link>
1
2
3
4
<template>
这是登录页面
<h3>登录组件---{{ $route.query.name }} --- {{ $route.query.pwd }}</h3>
</template>

params

1
<router-link to="/login/yujie/123">登录</router-link>
1
2
3
4
<template>
这是登录页面
<h3>登录组件---{{ $route.params.name }} --- {{ $route.params.pwd }}</h3>
</template>

路由命名与嵌套

通过一个名称来标识一个路由会更加方便,特别是在链接一个路由或者是执行一些跳转的时候,在创建Router实例时,可以在routes配置中通过设置name属性给某个路由设置名称。

1
2
3
4
5
{
path: "/detail",
name: "detail",
component: () => import("../views/Detail.vue"),
},
1
2
3
4
5
<router-link :to="{name: 'detail', params:{msg: 'welcome'}}">详情</router-link>

// 与以下代码等价
const router = useRouter();
router.push({name: 'detail', params:{msg: 'welcome'}})

一些应用程序的UI由多层嵌套的组件组成。在这种情况下,URL的片段通常对应于特定的嵌套组件结构,使用children属性可实现路由嵌套。在实际的web应用当中,通常根据不同的业务将应用划分为功能模块,菜单,子菜单,而为了更加方便管理,我们会对代码结构和业务功能进行统一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
path: "/sys-set",
component: () => import("../views/sys-set/index.vue"),
children: [
{
path: "user-manage",
component: () => import("../views/sys-set/user-manage/index.vue"),
},
{
path: "role-manage",
component: () => import("../views/sys-set/role-manage/index.vue"),
},
],
},

注意:以\开头的路径将被视为根路径。

过渡动效

1
2
3
<transition>
<router-view></router-view>
</transition>

也可设置单个路由的过渡和基于路由的动态过渡。

路由懒加载

如果把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效。

1
2
3
4
5
//将
// import Home from '../views/Home.vue';
// component: Home,
//替换成
component: () => import("../views/Home.vue"),

使用命名视图

一个页面只放一个同级别的<router-view>,如果我们想要在一个页面中放多个同级别的<router-view>,而不是嵌套展示,我们就要用到命名视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<router-view></router-view>
<div class="content">
<router-view name="sidebar"></router-view>
<router-view name="main"></router-view>
</div>

</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
setup() {
return {};
},
});
</script>

<style scoped>
body {
margin: 0px;
padding: 0px;
}
.header {
width: 100%;
height: 70px;
line-height: 70px;
background-color: lightyellow;
}
.content {
width: 100%;
position: absolute;
top: 70px;
height: calc(100% - 70px);
}
.sidebar {
width: 180px;
height: 100%;
background-color: lightgray;
float: left;
}
.mainbox {
width: calc(100% - 180px);
height: 100%;
background-color: lightgreen;
float: left;
}
</style>

keep-alive

keep-alive是Vue提供的一个抽象组件,用来对组件进行缓存,从而提升性能,由于是一个抽象组件,所以keep-alive在页面渲染完毕后不会被渲染成一个DOM元素。

通常我们可以配置整个页面缓存或只让特定的某个组件保持缓存信息,配置了keep-alive的路由或者组件,只会在页面初始化的时候执行created -> mounted生命周期,第二次及以后再次进入该页面时将不会再次执行created -> mounted生命周期,而是去读取缓存信息。

vuex

vuex是一个专为Vue.js应用程序开发的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。主要功能如下:

  1. 可以实现vue不同组件之间的状态共享
  2. 可以实现组件里面数据的持久化

在vuex中,默认有5种基本的对象:

  1. state:存储状态(变量,数据)
  2. getters:对数据获取之前的再次编译,可以理解为state的计算属性。在组件中使用$.store.getters.fun()
  3. mutations:修改状态,并且是同步的。在组件中使用$store.commit(‘’, params)。
  4. actions:异步操作。在组件中使用$store.dispath(‘’)
  5. modules:store的子模块,在开发大型项目时,可以方便状态管理。

vuex是Vue配套的公共数据管理工具,它可以把一些共享的数据保存到vuex中,方便整个程序中的任何组件直接获取或修改我们的公共数据。vuex中的数据是响应式的,也就是说,只要vuex中的数据一变化,所有引用了vuex中数据的组件都会自动更新。如果组件的数据不需要共享,就没有必要放到vuex中,只要放到组件的data中即可。

安装:

  1. CDN引用

  2. 直接下载

    1
    2
    <script src="path/to/vue.js"></script>
    <script src="path/to/vuex.js"></script>
  3. NPM和Yarn

    1
    2
    npm install vuex@next - save
    yarn add vuex@next - save

配置vuex

  1. 引入vuex

    注意,要在Vue引用之后引用vuex

    1
    2
    import { createApp } from 'vue';
    import { createStore } from 'vuex';
  2. 通过createStore得到一个数据仓储对象,通常我们将数据仓储对象操作封装到一个独立的文件store/index.ts中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import { createStore } from 'vuex';
    import appGlobal from './app-global';

    export default createStore({
    state: {
    count: 0,
    },
    mutations: {
    //自增
    increment(state) {
    state.count++;
    },
    //自减
    subtract(state, obj) {
    state.count -= obj.val;
    },
    },
    getters: {
    optCount: function(state) {
    return '当前最新的count值是:' + state.count;
    },
    },
    actions: {
    incrementAsync ({ commit }) {
    setTimeout(() => {
    commit('increment')
    }, 1000)
    }
    },
    modules: {
    appGlobal
    },
    });

    想要在组件中访问store中的数据,只能通过this.$store.state.数据属性。

    想要操作也只能调用mutations中的方法,this.$store.commit(‘方法名’)

    actions:执行mutations里面的方法,异步操作需要放在actions中。想要调用actions中的方法,需要使用this.$store.dispatch(‘方法名’)。

  3. 将vuex创建的store挂载到App实例上,只要挂载到了App上,任何组件都能使用store来存取数据。

    1
    2
    3
    4
    5
    6
    7
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'

    const app=createApp(App);
    app.use(store).use(router).mount('#app');
  4. 组件调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    <template>
    <div style='background-color: lightblue;'>
    <!-- <h3>{{ $store.state.count }}</h3> -->
    <h3>{{ $store.getters.optCount }}</h3>
    </div>
    <div style="background-color: lightcoral;">
    <input type="button"
    value="减少"
    @click="remove">
    <input type="button"
    value="增加"
    @click="add">
    <br>
    <input type="text"
    v-model="$store.state.count">
    </div>
    <h3>{{count}}</h3>
    <h3>{{count2}}</h3>
    <h3>{{count3}}</h3>
    <h3>{{count4}}</h3>
    <h3>{{optCount}}</h3>
    <h3>{{optCount1}}</h3>
    </template>

    <script>
    import { defineComponent } from "vue";
    // import store from '../store/index';
    import { mapState } from 'vuex'
    import { mapGetters,mapMutations } from 'vuex'

    export default defineComponent({
    methods: {
    add () {
    // 千万不要这么使用,违背了vuex 的设计理念
    // this.$store.state.count++;
    this.$store.commit("increment");
    },
    remove () {
    this.$store.commit("subtract", { val: 1 });
    },
    ...mapMutations([
    'increment', // 将`this.increment()`映射为`this.$store.commit('increment')`
    ]),
    ...mapMutations({
    add: 'increment' // 将`this.add()`映射为`this.$store.commit('increment')`
    })
    },
    computed: {
    // 使用对象展开运算符将getter混入computed对象中
    ...mapGetters(["optCount"]),
    ...mapGetters({
    optCount1: "optCount"
    }),
    ...mapState({
    count3: (state) => state.count
    }),
    //或者
    ...mapState([
    "count"
    ]),
    // count(){
    // return store.state.count;
    // },
    count2 () {
    return this.$store.state.count;
    },
    count4 () {
    return this.$store.getters.optCount;
    }
    },
    setup () {
    return {};
    },
    });
    </script>

    <style scoped>
    </style>

获取vuex中的state

方法一:按需引入

在用到的组件里面引入store,然后计算属性里面获取state数据(此种方法不推荐)。

1
2
3
4
5
6
7
<h3>{{count}}</h3>
import store from '../store/index'
computed: {
count(){
return store.state.count;
}
}

方法二:全局配置this.$store

1
2
3
4
5
6
<h3>{{count2}}</h3>
computed: {
count2(){
return this.$store.state.count;
}
}

不需要单独引入store,可直接通过this.$store调用。

方法三:mapState助手

1
2
3
4
5
6
7
8
9
10
11
12
<h3>{{count2}}</h3>
import { mapState } from 'vuex'

computed: {
...mapState({
count3: (state) => state.count
}),
//或者
...mapState([
"count"
]),
}

获取Getter,调用Mutations和Actions

定义Getter

1
2
3
4
5
6
7
8
9
10
export default createStore({
state: {
count: 0,
},
getters: {
optCount: function(state) {
return '当前最新的count值是:' + state.count;
},
}
});

访问可以使用store.getter.或this.$store.getters或mapGetters辅助函数。

调用Mutations和Actions

在组件中使用this.$store.commit(‘’)或者用mapMutations辅助方法,可以将组件的方法映射到store.commit调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
methods: {
add () {
// 千万不要这么使用,违背了vuex 的设计理念
// this.$store.state.count++;
this.$store.commit("increment");
},
remove () {
this.$store.commit("subtract", { val: 1 });
},
...mapMutations([
'increment', // 将`this.increment()`映射为`this.$store.commit('increment')`
]),
...mapMutations({
add: 'increment' // 将`this.add()`映射为`this.$store.commit('increment')`
})
},
}

Composition API方式使用 vuex

在vuex4 中,新增了 Composition API 的方式来使用 store。在 setup 钩子函数中访问store,你可以调用 useStore 方法,这等价于通过 Option API 的方式在组件中使用 this.$store。

1
2
3
4
5
6
7
import {useStore} from 'vuex';
export default defineComponent({
setup(){
const store = useStore();
return{}
}
})

访问 State and Getters

为了方便访问 state 和 getters,你可以通过调用 computed方法实现响应式属性的引用,这其实相当于使用 Option API的方式创建一个 computed属性。

访问 Mutations and Actions

当访问 Mutations and Actions 时,你可以直接在 setup 子函数当中调用 commit和dispatch 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
{{count}}
</div>
</template>

<script lang="ts">
import { defineComponent, computed } from "vue";
import { useStore } from "vuex";

export default defineComponent({
setup() {
const store = useStore();
return {
//通过computed反复访问state
count: computed(() => store.state.count),
//调用mutation方法
increment: () => store.commit("increment"),
// 调用action方法
asyncIncrement: () => store.dispatch("asyncIncrement"),
};
},
});
</script>

<style scoped>
</style>

Modules模块

由于使用单一状态树,应用程序的所有状态都包含在一个大对象中。然而,随着应用程序规模的扩大,store对象可能会变得非常臃肿。为了解决这个问题,Vuex 允许我们将 store分割成模块,每个模块都可以包含自己的state,mutation,action,getter,甚至嵌套子模块,从上至下进行同样方式的分割。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}

const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})

store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state

想让模块直接相互独立,互不影响,可以通过添加namespace:true的方式使其成为带命名空间的模块。