Vue基础2 - 路由(router)以及SEO配置

路由模式

History模式的时候 刷新页面会404

当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404

所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理就可以了。

解决方法

1
2
3
location / {
try_files $uri $uri/ /index.html;
}

简单路由

基本配置

默认的路由配置是这样的

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
import Vue from "vue";
import VueRouter from "vue-router";
import Paike from "../views/Paike.vue";

Vue.use(VueRouter);

const routes = [
{
path: "/",
name: "index",
redirect: '/zouban'
},
{
path: "/paike",
name: "paike",
component: Paike
},
{
path: "/zouban",
name: "zouban",
component: () => import("../views/Zouban.vue")
},
];

const router = new VueRouter({
mode: 'hash',
routes,
});

export default router;

Vue的路由有两种模式 hashhistory

hash模式对应的路由是类似于这个样子的 http://localhost:8080/#/about

也就是说对于搜索引擎来说所有的页面只有一个路径 就不利于搜索引擎索引

history模式

history模式对应的路由是这个样子的http://localhost:8080/about

一般不建议使用history模式,这种需要服务器需要配置一下。

在使用 Vue 的 history 模式时,由于前端路由是通过修改 URL 来实现的,所以在部署到服务器上时,需要进行一些特殊的配置才能确保正常访问。

以下是一个常见的配置流程:

Nginx添加配置:

在 Nginx 的配置文件中,添加一个用于处理前端路由的 location 配置,如下所示:

1
2
3
location / {
try_files $uri $uri/ /index.html;
}

这个配置将会重定向所有请求到 index.html,这样访问任何一个路由都会返回同一个首页。

然后 Vue Router 会根据 URL 来动态加载相应的组件。

保存并重新启动 Nginx 服务,使配置生效。

配置说明:

其中try_files $uri $uri/ /index.html; 是一个 Nginx 配置指令,用于处理前端路由的重定向。

下面是对每个参数的解释:

  • $uri:尝试匹配当前请求的文件路径,如果找到该文件,则返回该文件。

  • $uri/:尝试匹配当前请求的目录路径,如果找到该目录下的索引文件(如 index.html),则返回该文件。

    这个参数主要用于处理目录路径的情况,例如 /about 这样的路由。

  • /index.html:如果上述两个尝试都失败,将请求重定向到 index.html,也就是前端路由的入口文件,但是URL不会变。

这个配置的目的是确保所有请求都重定向到前端的 index.html 文件。

由于前端路由是通过修改 URL 来实现的,所以服务器需要配置这样的规则来处理这些路由请求。

在重定向到 index.html 后,Vue Router 会根据 URL 来动态加载相应的组件。

这样的配置使得服务器在处理前端路由请求时不会报错,并且确保前端应用的正确渲染。

嵌套路由

Home.vue

1
2
3
4
5
<div id="nav">
<router-link to="/home/company">公司</router-link>
<router-link to="/home/about">关于页</router-link>
</div>
<router-view/>

router.js

1
2
3
4
5
6
7
8
9
10
{
path: '/home',
name: 'home',
component: Home,
redirect: "/home/about",
children: [
{path: '/home/about', component: About},
{path: '/home/company', component: Company}
]
}

redirect 用来跳转到home路由时 自动加载子路由

路由懒加载

一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)

1
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

路由跳转

标签形式

1
2
3
4
5
<div id="nav">
<router-link to="/home/company">公司</router-link>
<router-link to="/home/about">关于页</router-link>
</div>
<router-view/>

JS形式

基本

1
2
3
4
5
6
7
8
9
10
11
12
13
// 字符串
this.$router.push('/home');
// 对象
this.$router.push({ path: '/home' });
// 对象
this.$router.push({ name: 'home' });
// 替换 无法router.go(-1)返回
this.$router.replace({ name: 'home' });
this.$router.replace({ path: '/home' });
// 在浏览器记录中前进一步,等同于 history.forward()
this.$router.go(1);
// 后退一步记录,等同于 history.back()
this.$router.go(-1);

传参

query带参数

1
2
3
// 带查询参数,变成 /register?userId=123
this.$router.push({ path: 'register', query: { userId: 123 }});
this.$router.replace({ path: 'register', query: { userId: 123 }});

取值方式

1
this.$route.query.userId;

params带参数

1
2
3
// 命名的路由
this.$router.push({ name: 'user', params: { userId: 123 }});
this.$router.replace({ name: 'user', params: { userId: 123 }});

对应的取值方式

1
this.$route.params.userId;

注意:

下面的这种方式传参是错误的

1
2
// 这里的 params 不生效
this.$router.push({ path: '/user', params: { userId: 123 }}) // -> /user

router带参数

1
2
3
4
5
6
{
path: '/operate/:id',//带参数
name: 'operate',
meta:{requireAuth:true},
component: ()=>import('@/page/user/operate')
},

然后带参数跳转

1
2
this.$router.push({path:'/operate/${id}'})
this.$router.replace({path:'/operate/${id}'})

获取参数

1
2
3
this.$route.params.id
//或者也可以通过props来获取参数
props: ['id']

简单的SEO配置

首先把路由模式设置为history,再添加meta信息 添加后的配置如下

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
import Vue from "vue";
import VueRouter from "vue-router";
import Paike from "../views/Paike.vue";

Vue.use(VueRouter);

const routes = [
{
path: "/",
name: "index",
redirect: '/zouban'
},
{
path: "/paike",
name: "paike",
component: Paike,
meta: {
title: "排课",
content: "排课页面"
}
},
{
path: "/zouban",
name: "zouban",
component: () => import("../views/Zouban.vue"),
meta: {
title: "走班",
content: "走班页面"
}
},
];

const router = new VueRouter({
mode: 'hash',
routes,
});

router.beforeEach((to, from, next) => {
/* 路由发生变化修改页面meta */
if (to.meta && to.meta.content) {
let head = document.getElementsByTagName('head');
let meta = document.createElement('meta');
meta.content = to.meta.content;
head[0].appendChild(meta)
}
/* 路由发生变化修改页面title */
if (to.meta && to.meta.title) {
document.title = to.meta.title;
}
next()
});

export default router;

上面是在路由配置的JS中,当然也可以在在main.js中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
router.beforeEach((to, from, next) => {
/* 路由发生变化修改页面meta */
if (to.meta && to.meta.content) {
let head = document.getElementsByTagName('head');
let meta = document.createElement('meta');
meta.content = to.meta.content;
head[0].appendChild(meta)
}
/* 路由发生变化修改页面title */
if (to.meta && to.meta.title) {
document.title = to.meta.title;
}
next()
});

注意上面的配置要放在创建Vue对象之前

1
2
3
4
5
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');

这样访问路由的时候 titlecontent 就会自动设置了。

Vue-meta的使用

安装

1
npm install --save vue-meta

router.js中添加

1
2
3
4
5
import Router from 'vue-router'
import VueMeta from 'vue-meta'

Vue.use(Router);
Vue.use(VueMeta);

页面中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
data() {
return {
mtitle: "登录"
}
},
metaInfo: function () {
return {
title: this.mtitle,
titleTemplate: '%s - 智慧课堂',
htmlAttrs: {
lang: 'zh'
},
script: [{innerHTML: 'console.log("hello world!")', type: 'text/javascript'}],
__dangerouslyDisableSanitizers: ['script']
}
}
}
</script>

vue-meta 把引号做了转义处理,加入 __dangerouslyDisableSanitizers: ['script'] 后,就不会再对这些字符做转义了,该字段使用需慎重!

当然我们也可以在这里引用JS和CSS(不建议这样处理)

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
<script>
export default {
data() {
return {
mtitle: "登录"
}
},
metaInfo: function () {
return {
title: this.mtitle,
titleTemplate: '%s - 智慧课堂',
htmlAttrs: {
lang: 'zh'
},
script: [
{innerHTML: 'console.log("hello hello!")', type: 'text/javascript'},
{src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js', type: 'text/javascript'}
],
__dangerouslyDisableSanitizers: ['script'],
link: [
{
rel: "stylesheet",
type: "text/css",
href: "/css/reset.css"
}
]
}
}
}
</script>

路由的渲染

如果我们的页面有嵌套路由的时候 页面渲染的时候会先渲染子路由对应的页面 导致如果子路由获取外层的高度的时候获取的一直是0,因为外层还未渲染 解决的方法就是:在父页面渲染后通知子页面获取

比如我用vuex保存外层的高度

1
2
3
4
5
6
7
export default new Vuex.Store({
state: {
content_height: '0'
},
mutations: {},
actions: {}
});

添加公共的事件监听

1
2
var vue_event = new Vue();
window.vue_event = vue_event;

父页面

1
2
3
this.page_height = window.document.body.offsetHeight - this.$refs.header.offsetHeight - parseInt(getComputedStyle(this.$refs.main).marginBottom) - 2 + "px";
this.$store.state.content_height = parseInt(this.page_height);
vue_event.$emit('main_page_loaded', {});

子页面接收

1
2
3
4
5
6
mounted() {
var _this = this;
vue_event.$on("main_page_loaded", function (data) {
_this.test_height = _this.$store.state.content_height - _this.$refs.test_title.offsetHeight - 24 + "px";
});
}

路由守卫

判断登录状态路由跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
router.beforeEach((to, from, next) => {
let loginUser = localStorage.getItem("loginUser");
if (to.path !== "/login") {
if (loginUser) {
next();
} else {
next({
path: "/login",
query: {},
});
}
} else {
next();
}
});

路由面包屑

1
2
3
4
5
6
7
8
9
<el-breadcrumb separator="/" style="margin-left: 10px">
<el-breadcrumb-item :to="{ path: '/main' }">首页</el-breadcrumb-item>
<el-breadcrumb-item
v-for="(item, index) in $store.state.nav_list"
:key="index"
:to="{ path: item.path }"
>{{ item.name }}</el-breadcrumb-item
>
</el-breadcrumb>

vuex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
state: {
nav_list: [],
},
mutations: {
mut_nav_list(state, list) {
state.nav_list = list;
},
},
actions: {},
modules: {},
});

router

1
2
3
router.afterEach((to) => {
router.app.$options.store.commit("mut_nav_list", to.matched);
});

排除根层级

1
2
3
4
5
6
7
8
router.afterEach((to) => {
router.app.$options.store.commit(
"mut_nav_list",
to.matched.filter((item) => {
return item.name !== "数据中台";
})
);
});

后记

现在有这个一个需求,我们像让面包屑中出现类似的结构 首页=>用户列表=>用户详情,但是用户详情不是用户列表的子路由,所以用to.matched是无法匹配到的,所以只能我们自己来处理路由的数组。

路由的代码改成如下即可:

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
let routers_all = [];

for (const mroute of routes) {
routers_all.push(mroute);
if (mroute.children && mroute.children.length > 0) {
addChildRouter(mroute);
}
}

function addChildRouter(item) {
let children = item.children;
for (const item_child of children) {
routers_all.push(item_child);
if (item_child.children && item_child.children.length > 0) {
addChildRouter(item_child);
}
}
}

router.afterEach((to) => {
let temp_arr = [];
for (const item of routers_all) {
if (to.path.indexOf(item.path) === 0) {
temp_arr.push(item);
}
}
router.app.$options.store.commit("mut_nav_list", temp_arr);
});

Menu回显

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
<Menu
theme="dark"
:active-name="currPath"
:open-names="['/pingtai/home']"
class="leftmenu"
@on-select="select_menu_action"
>
<Submenu name="/pingtai/home">
<template slot="title">
{{ $store.state.menu_hide ? "资源管理".substr(0, 2) : "资源管理" }}
</template>
<MenuItem name="/pingtai/home/jiqun">
<Tooltip content="集群管理" placement="right" :transfer="true">
<span class="iconfont icon-jiqun"></span
>{{ $store.state.menu_hide ? "" : "集群管理" }}
</Tooltip>
</MenuItem>
<MenuItem name="/pingtai/home/useinfo">
<Tooltip content="使用情况" placement="right" :transfer="true"
><span class="iconfont icon-pinpaisucaishiyongliucheng-05"></span
>{{ $store.state.menu_hide ? "" : "使用情况" }}
</Tooltip>
</MenuItem>
</Submenu>
</Menu>

监听

1
2
3
4
5
6
7
8
9
10
11
12
export default{
data() {
return {
currPath: "/pingtai/home/jiqun",
};
},
watch: {
$route() {
this.currPath = this.$route.path;
},
},
}

路由重置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const createRouter = () =>
new VueRouter({
scrollBehavior: () => ({
y: 0
}),
routes: getRoutes()
});
const router = createRouter();
//写一个重置路由的方法,切换用户后,或者退出时清除动态加载的路由
function resetRouter() {
const newRouter = createRouter();
router.matcher = newRouter.matcher; // 新路由实例matcer,赋值给旧路由实例的matcher,(相当于replaceRouter)
router.options.routes = newRouter.options.routes;
}
router.resetRouter = resetRouter;

动态添加路由

1
2
3
4
5
6
7
8
9
10
let routes = [
{
path: "/home2",
name: "home",
component: () => import("./views/HomeView.vue"),
},
];
router.addRoutes(routes);
// 这个主要是在vue-devtools中显示路由
router.options.routes = router.getRoutes();

注意

默认添加路由后,在vue-devtools中并不能看到新路由。

要添加

1
router.options.routes = router.getRoutes()

常见问题

push相同路由报错

push相同路由报错的解决方式

1
2
3
4
5
6
//获取原型对象上的push函数
const originalPush = VueRouter.prototype.push;
//修改原型对象中的push方法
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch((err) => err);
};

replace相同路由报错

替换原函数

1
2
3
4
5
6
//获取原型对象上的replace函数
const originalReplace = VueRouter.prototype.replace;
//修改原型对象中的replace方法
VueRouter.prototype.replace = function replace(location) {
return originalReplace.call(this, location).catch((err) => err);
};

添加判断

1
2
3
4
5
if (this.$route.path !== "/home/index") {
this.$router.replace({
path: "/home/index",
});
}

添加参数

1
2
3
4
this.$router.replace({
path: "/home/index",
query: { timeunix: Date.now() },
});

使用location.href

1
location.href = "/#/home/index";

路由相同参数不同

路由相同参数不同 无法触发路由。

推荐方式2和方式3。

解决方法:

方式1

给 router-view 设置 key 属性为路由的完整路径

1
2
3
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>

这种方法我觉得应该是一劳永逸的方法,可能对性能造成一定损耗。

不适用于一个tab切换路由并加载列表的组件,会造成页面白屏。

方式2

官方给出的方法是通过 watch 监听路由变化,做判断路由路径然后调用响应的方法

页面初始化处理放在pageLoad,不要放在mounted中。

监听路由

1
2
3
4
5
6
7
8
9
10
11
12
watch: {
$route() {
if (this.$route.path === "/pingtai/home/jiqun") {
this.pageLoad();
}
},
},
methods: {
pageLoad() {
console.info(this.$route.query.id);
},
},

方式3

监听参数

页面初始化处理放在pageLoad,不要放在mounted中。

1
2
3
4
5
6
7
8
9
10
11
watch: {
"$route.query.id": {
handler: "pageLoad", //调用方法
immediate: true, //进入立即执行一次
},
},
methods: {
pageLoad() {
console.info(this.$route.query.id);
},
},