案例-面经基础版
分析:配路由+实现功能
- 配路由
- 首页、面经详情,两个一级路由
- 首页内嵌四个可切换页面(嵌套二级路由)
- 实现功能
- 首页请求渲染
- 跳转传参(文章 id)到详情页,详情页渲染
- 组件缓存,优化性能
配路由
一级路由
首页组件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: [ { 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
添加相应样式即可
实现功能
首页请求渲染
- 安装 axios
- 看接口文档,确认请求方式,请求地址,请求参数
请求地址:https://mock.boxuegu.com/mock/3083/articles
请求方式:get
- 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); },
|
- 页面动态渲染
详情页渲染
跳转传参(文章 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, redirect: '/article', children: [ ], }, { path: '/detail/:id', component: ArticleDetail }, ],
|
返回上一页
从详情页点击返回按钮返回上一页的功能
按钮绑定点击事件$router.back()
即可
ArticleDetail.vue:
1
| <span @click="$router.back()" class="back"><</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> <router-view></router-view> </keep-alive>
|
actived
& deactived
钩子
被缓存的组件会多两个生命周期钩子actived
(被激活,进入页面触发)和deactived
(失活,离开页面触发)
而原本的created
、mounted
、destroyed
等钩子都会失效
keep-alive
的三个属性
include
:组件名数组,只有匹配的组件会被缓存
exclude
:组件名数组,任何匹配的组件都不会被缓存
max
:最多可以缓存多少组件实例
注意:组件名数组中的组件名以组件默认导出的name
属性值为准,如 Layout.vue 组件的组件名:
1 2 3 4
| export default { name: 'LayoutPage', };
|
问题:
在仅使用keep-alive
包裹router-view
后确实实现了从详情页回到首页下划位置不变的效果
但是,出现了问题:点击其他文章,进入到详情页所看到的内容还是上一篇文章的内容
原因:
一级路由 Layout
和 Article
属于同级,而 Article
组件不需要缓存而是重新加载
解决:
使用include
属性,为keep-alive
添加需要被缓存的组件名数组
1 2 3 4 5 6 7 8 9
|
<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: [ { 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 :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: [ { 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: '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"> <div v-if="article.id" class="article-detail-page"> <nav class="nav"> <span @click="$router.back()" class="back"><</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';
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', }; </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>
|