案例-面经基础版

2.7k 词

案例-面经基础版#

032-案例06面经基础版_cut_1702997239810
分析:配路由+实现功能

  1. 配路由
    1. 首页、面经详情,两个一级路由
    2. 首页内嵌四个可切换页面(嵌套二级路由
  2. 实现功能
    1. 首页请求渲染
    2. 跳转传参(文章 id)到详情页,详情页渲染
    3. 组件缓存,优化性能

配路由#

一级路由#

首页组件Layout.vue,默认匹配路径/
面经详情组件ArticleDetail.vue,匹配路径/detail
路由规则:

1
2
3
4
5
6
const router = new VueRouter({
routes: [
{ path: '/', component: Layout },
{ path: '/detail', component: ArticleDetail },
],
});

二级路由#

四个二级路由属于首页路径下
路由规则不能与一级路由同级,否则在点击二级路由后会转到新页面而不是同一页面下切换组件
二级路由用children嵌套在首页路由规则中,children规则与routes一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const router = new VueRouter({
routes: [
{
path: '/',
component: Layout,
// 通过children配置项,可以配置嵌套子路由
// 1.在children配置项中配置规则
// 2.准备二级路由出口
children: [
{ path: '/article', component: Article },
{ path: '/collect', component: Collect },
{ path: '/like', component: Like },
{ path: '/user', component: User },
],
},
{ path: '/detail', component: ArticleDetail },
],
});

导航高亮#

用声明式导航router-link替换a标签,href换为to,且去掉原先路径的#

1
2
3
<a href="#/article">面经</a>
<!-- 改为: -->
<router-link to="/article">面经</router-link>

之后为导航高亮类router-link-active添加相应样式即可

实现功能#

首页请求渲染#

  1. 安装 axios
1
npm add axios
  1. 看接口文档,确认请求方式,请求地址,请求参数

请求地址:https://mock.boxuegu.com/mock/3083/articles
请求方式:get

  1. created 中发请求,获取数据并存储
1
2
3
4
5
6
7
8
9
10
data() {
return {
articles: [],
};
},
async created() {
const res = await axios.get('https://mock.boxuegu.com/mock/3083/articles');
this.articles = res.data.result.rows;
console.log(this.articles);
},
  1. 页面动态渲染

详情页渲染#

跳转传参(文章 id)到详情页

查询参数传参#

更适合多参数传参
?参数=参数值 => this.$route.query.参数名
文章盒子绑定点击事件,传递参数id
Article.vue:

1
2
3
<div v-for="item in articles" :key="item.id" @click="$router.push(`/detail?id=${item.id}`)" class="article-item">
<!-- ... -->
</div>

详情页使用this.$route.query.id获取即可

动态路由传参#

更适合单个参数传参
改造路由 => /路径/参数 => this.$route.params.参数名
首先改造路由
src/router/index.js:

1
2
3
{ path: '/detail', component: ArticleDetail },
// 改为:
{ path: '/detail/:id', component: ArticleDetail },

而文章盒子绑定的点击事件中,query 传参用?参数名=连接的参数改为直接用/连接:
Article.vue:

1
2
3
<div v-for="item in articles" :key="item.id" @click="$router.push(`/detail/${item.id}`)" class="article-item">
<!-- ... -->
</div>

详情页使用this.$route.params.id获取即可

‘/‘ 重定向#

访问到/路径时会出现空白,希望重定向到/article
在路由规则中使用redirect/重定向到指定路径即可
src/router/index.js:

1
2
3
4
5
6
7
8
9
10
11
12
routes: [
{
path: '/',
component: Layout,
// 重定向到article
redirect: '/article',
children: [
// ...
],
},
{ path: '/detail/:id', component: ArticleDetail },
],

返回上一页#

从详情页点击返回按钮返回上一页的功能
按钮绑定点击事件$router.back()即可
ArticleDetail.vue:

1
<span @click="$router.back()" class="back">&lt;</span>

渲染#

接口文档说明:

请求地址:https://mock.boxuegu.com/mock/3083/articles/:id
请求方式:get

组件缓存,优化性能#

问题:从面经点进详情页,又点返回,数据重新加载了,希望能回到原来下划的文章位置
原因:路由跳转后,组件被销毁了,返回之后组件又被重建了,所以数据重新被加载了
解决:利用keep-alive将组件缓存下来

keep-alive#

keep-alive是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
keep-alive是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件链中

优点
在组件切换过程中把切换出去的组件保留在内存中,防止重复渲染 DOM
减少加载时间及性能消耗,提高用户体验性
App.vue:

1
2
3
4
5
6
<!-- 包裹了keep-alive一级路由匹配的组件都会被缓存
Layout和Detail组件都会被缓存
-->
<keep-alive>
<router-view></router-view>
</keep-alive>

actived & deactived 钩子#

被缓存的组件会多两个生命周期钩子actived(被激活,进入页面触发)和deactived(失活,离开页面触发)
而原本的createdmounteddestroyed等钩子都会失效

keep-alive的三个属性#

  1. include:组件名数组,只有匹配的组件会被缓存
  2. exclude:组件名数组,任何匹配的组件都不会被缓存
  3. max:最多可以缓存多少组件实例

注意:组件名数组中的组件名以组件默认导出的name属性值为准,如 Layout.vue 组件的组件名:

1
2
3
4
export default {
// 组件名优先用name,没有name再找文件名作组件名
name: 'LayoutPage',
};

问题
在仅使用keep-alive包裹router-view后确实实现了从详情页回到首页下划位置不变的效果
但是,出现了问题:点击其他文章,进入到详情页所看到的内容还是上一篇文章的内容
原因
一级路由 LayoutArticle 属于同级,而 Article 组件不需要缓存而是重新加载
解决
使用include属性,为keep-alive添加需要被缓存的组件名数组

1
2
3
4
5
6
7
8
9
<!-- 包裹了keep-alive一级路由匹配的组件都会被缓存
LayoutPage组件(被缓存)- 多两个生命周期钩子,且原本的created、mounted等钩子失效
- actived 激活时,组件被看到时触发
- deactived 失活时,离开页面组件看不到触发
ArticleDetailPage组件(未被缓存)
-->
<keep-alive :include="keepArr">
<router-view></router-view>
</keep-alive>
1
2
3
4
5
6
data() {
return {
// 缓存组件名的数组
keepArr: ['LayoutPage'],
};
},

拓展#

加载动画#

结合自定义指令,内容未加载完成之前显示加载动画
位置:首页+详情页
自定义指令注册方式选取全局注册方式
main.js:

1
2
3
4
5
6
7
8
Vue.directive('loading', {
inserted(el, binding) {
binding.value ? el.classList.add('loading') : el.classList.remove('loading');
},
update(el, binding) {
binding.value ? el.classList.add('loading') : el.classList.remove('loading');
},
});

加载样式 App.vue:

1
2
3
4
5
6
7
8
9
.loading::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #fff url(./assets/loading.gif) no-repeat center;
}

在要使用加载效果的组件中,初始化一个isLoading参数用于控制加载动画是否显示
在要使用加载动画的盒子上添加自定义指令v-loading='isLoading'
请求结果拿到后,将isLoading状态修改为false
ArticleDetail.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
data() {
return {
article: {},
isLoading: true,
};
},
async created() {
const id = this.$route.params.id;
const { data } = await axios.get(`https://mock.boxuegu.com/mock/3083/articles/${id}`);
this.article = data.result;
this.isLoading = false;
console.log(this.article);
},

404 notfound#

添加路由规则,没有资源的路由匹配到 NotFound 组件

1
2
3
4
5
routes: [
// ...
// 404放到最后
{ path: '*', component: NotFound },
],

源码#

App.vue:

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
<template>
<div class="h5-wrapper">
<!-- 包裹了keep-alive一级路由匹配的组件都会被缓存
LayoutPage组件(被缓存)- 多两个生命周期钩子,且原本的created、mounted等钩子失效
- actived 激活时,组件被看到时触发
- deactived 失活时,离开页面组件看不到触发
ArticleDetailPage组件(未被缓存)
-->
<keep-alive :include="keepArr">
<router-view></router-view>
</keep-alive>
</div>
</template>

<script>
export default {
name: 'h5-wrapper',
data() {
return {
// 缓存组件名的数组
keepArr: ['LayoutPage'],
};
},
};
</script>

<style>
body {
margin: 0;
padding: 0;
}
</style>
<style lang="less">
.loading::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #fff url(./assets/loading.gif) no-repeat center;
}
.h5-wrapper {
.content {
margin-bottom: 51px;
}
.tabbar {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
display: flex;
background: #fff;
border-top: 1px solid #e4e4e4;
a {
flex: 1;
text-decoration: none;
font-size: 14px;
color: #333;
-webkit-tap-highlight-color: transparent;
&.router-link-active {
color: #fa0;
}
}
}
}
</style>

main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue';
import App from './App.vue';
import router from './router';

Vue.config.productionTip = false;
Vue.directive('loading', {
inserted(el, binding) {
binding.value ? el.classList.add('loading') : el.classList.remove('loading');
},
update(el, binding) {
binding.value ? el.classList.add('loading') : el.classList.remove('loading');
},
});
new Vue({
render: h => h(App),
router,
}).$mount('#app');

router/index.js:

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
import Vue from 'vue';
import VueRouter from 'vue-router';

// 一级路由
import Layout from '@/views/Layout';
import ArticleDetail from '@/views/ArticleDetail';

// 二级路由
import Article from '@/views/Article';
import Collect from '@/views/Collect';
import Like from '@/views/Like';
import User from '@/views/User';

import NotFound from '@/views/NotFound';
Vue.use(VueRouter);

const router = new VueRouter({
routes: [
{
path: '/',
component: Layout,
redirect: '/article',
// 通过children配置项,可以配置嵌套子路由
// 1.在children配置项中配置规则
// 2.准备二级路由出口
children: [
{ path: '/article', component: Article },
{ path: '/collect', component: Collect },
{ path: '/like', component: Like },
{ path: '/user', component: User },
],
},
{ path: '/detail/:id', component: ArticleDetail },
{ path: '*', component: NotFound },
],
});

export default router;

Layout.vue:

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
<template>
<div class="h5-wrapper">
<div class="content">
<!-- 二级路由出口 -->
<router-view></router-view>
</div>
<nav class="tabbar">
<router-link to="/article">面经</router-link>
<router-link to="/collect">收藏</router-link>
<router-link to="/like">喜欢</router-link>
<router-link to="/user">我的</router-link>
</nav>
</div>
</template>

<script>
export default {
// 组件名优先用name,没有name再找文件名作组件名
name: 'LayoutPage',
};
</script>

<style>
body {
margin: 0;
padding: 0;
}
</style>
<style lang="less" scoped>
.h5-wrapper {
.content {
margin-bottom: 51px;
}
.tabbar {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
display: flex;
background: #fff;
border-top: 1px solid #e4e4e4;
a {
flex: 1;
text-decoration: none;
font-size: 14px;
color: #333;
-webkit-tap-highlight-color: transparent;
&.router-link-active {
color: #fff;
background-color: #ff3555;
}
}
}
}
</style>

ArticleDetail.vue:

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
<template>
<div v-loading="isLoading">
<!-- 优化:当article有数据时再显示 -->
<div v-if="article.id" class="article-detail-page">
<nav class="nav">
<span @click="$router.back()" class="back">&lt;</span>
面经详情
</nav>
<header class="header">
<h1>{{ article.stem }}</h1>
<p>{{ article.createdAt }} | {{ article.views }} 浏览量 | {{ article.likeCount }} 点赞数</p>
<p>
<img :src="article.creatorAvatar" alt="" />
<span>{{ article.creatorName }}</span>
</p>
</header>
<main class="body">{{ article.content }}</main>
</div>
</div>
</template>

<script>
import axios from 'axios';
export default {
name: 'ArticleDetailPage',
data() {
return {
article: {},
isLoading: true,
};
},
async created() {
const id = this.$route.params.id;
const { data } = await axios.get(`https://mock.boxuegu.com/mock/3083/articles/${id}`);
this.article = data.result;
this.isLoading = false;
console.log(this.article);
},
};
</script>

<style lang="less" scoped>
.article-detail-page {
.nav {
height: 44px;
border-bottom: 1px solid #e4e4e4;
line-height: 44px;
text-align: center;
.back {
font-size: 18px;
color: #666;
position: absolute;
left: 10px;
top: 0;
transform: scale(1, 1.5);
}
}
.header {
padding: 0 15px;
p {
color: #999;
font-size: 12px;
display: flex;
align-items: center;
}
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
}
.body {
padding: 0 15px;
}
}
</style>

Article.vue:

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<template>
<div v-loading="isLoading" class="article-page">
<div v-for="item in articles" :key="item.id" @click="$router.push(`/detail/${item.id}`)" class="article-item">
<div class="head">
<img :src="item.creatorAvatar" alt="" />
<div class="con">
<p class="title">{{ item.stem }}</p>
<p class="other">{{ item.creatorName }} | {{ item.createdAt }}</p>
</div>
</div>
<div class="body">{{ item.content }}</div>
<div class="foot">点赞 {{ item.likeCount }} | 浏览 {{ item.views }}</div>
</div>
</div>
</template>

<script>
import axios from 'axios';

// 首页请求渲染
// 1.安装axios
// 2.看接口文档,确认请求方式,请求地址,请求参数
// 请求地址:https://mock.boxuegu.com/mock/3083/articles
// 请求方式:get
// 3.created中发请求,获取数据并存储
// 4.页面动态渲染

// 跳转详情页传参
// 1.查询参数传参 ?参数=参数值 => this.$route.query.参数名
// 2.动态路由传参 改造路由 => /路径/参数 => this.$route.params.参数名
// 1)访问 / 重定向到 /article (redirect)
// 2)返回上一页 $router.back()
export default {
name: 'ArticlePage',
data() {
return {
articles: [],
isLoading: true,
};
},
async created() {
const res = await axios.get('https://mock.boxuegu.com/mock/3083/articles');
// 用定时器模拟延长加载过程
setTimeout(() => {
this.articles = res.data.result.rows;
this.isLoading = false;
}, 2000);
},
};
</script>
<style lang="less" scoped>
.article-page {
background: #f5f5f5;
}
.article-item {
margin-bottom: 10px;
background: #fff;
padding: 10px 15px;
.head {
display: flex;
img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
.con {
flex: 1;
overflow: hidden;
padding-left: 15px;
p {
margin: 0;
line-height: 1.5;
&.title {
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
white-space: nowrap;
}
&.other {
font-size: 10px;
color: #999;
}
}
}
}
.body {
font-size: 14px;
color: #666;
line-height: 1.6;
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.foot {
font-size: 12px;
color: #999;
margin-top: 10px;
}
}
</style>

Collect.vue/Like.vue/User.vue:

1
2
3
4
5
6
7
8
9
<template>
<div>Collect/Like/User</div>
</template>

<script>
export default {
name: 'CollectPage', // 'LikePage' 'UserPage'
};
</script>

NotFound.vue:

1
2
3
4
5
6
7
<template lang="">
<div><h1>404 Not Found</h1></div>
</template>
<script>
export default {};
</script>
<style lang="less" scoped></style>