效果展示


创建项目
vue create demo-toutiao
勾选router,不要eslint
项目创建之后会多出来views和components文件夹,views用于放主页面,components用于放页面中的子组件
这个项目要做的主页面有Home.vue和User.vue,而子组件则有ArticleInfo.vue。
安装Vant
Vant是有赞团队开发的一个移动端UI组件,可以十分方便地生成美观的UI组件
安装指令为:
npm i vant -S
安装完成后,需要引入该组件,官方给出了三种引入方式:自动引入、按需引入和全部引入,官方推荐使用自动引入,但黑马教程觉得这种引入方式比较麻烦,所以选择了全部引入,接着在打包的时候剔除掉不需要的部分。
全部引入便是在main.js中添加一下代码:
| import Vant from 'vant';
import 'vant/lib/index.css';
Vue.use(Vant);
|
但这样做会使系统在打包的时候将Vant也一起打包进入,增加初始包的体积
这时就需要通过externals配置项将Vant排除在原始打包项目之外,然后在主html中引入Vant的js文件。
具体做法参考:给予externals配置CDN加速
引入之后我们就可以正式开发项目
制作顶部菜单
删除App.vue中大部分代码,在template中增加一下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <div id="app">
<router-view />
<van-tabbar route fixed placeholder>
<van-tabbar-item replace to="/home" icon="home-o">首页</van-tabbar-item>
<van-tabbar-item replace to="/user" icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
|
其中van-tabbar的route属性用于开启路由属性,
fixed用于将菜单固定到页面底部,
placeholder则需要跟fixed一起使用,它将生成一个跟van-tabbar等高的框框,避免文章内容拉到底的时候被遮盖住。(注意:该功能需要在2.6.0以上版本才能使用)
当然,也可以在内容页面上添加padding-bottom: 50px达到同样的效果。
接着是van-tabbar-item中的replace属性,用于替换历史访问记录,这个可以用在后退时不想让用户访问之前页面的需求上。
to就是传统的路由规则,
icon表示图标,具体查看:van-tabbar图标,其中有-o结尾的是空心图标。
最后注册路由
在router/index.js中添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import Home from '@/components/Home.vue';
import User from '@/components/User.vue';
routes = [
{ path: '/', redirect: '/home' },
{ path: '/home', component: Home },
{ path: '/user', component: User }
]
|
黑马在其代码中直接将’/home’定位’/‘,这样会出现选中Home菜单时,Home菜单没有切换成选中状态的BUG。因此不要这么写。
User组件
接着先制作比较简单的User组件,这里的User组件只需要实现页面就可以了,具体功能不需要,代码如下:
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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
| <template>
<div class="user-container">
<div class="user-card">
<van-cell>
<template #icon>
<img src="../../assets/logo.png" class="avatar" />
</template>
<template #title>
<span class="username">用户名</span>
</template>
<template #label>
<van-tag color="#fff" text-color="#007bff">申请认证</van-tag>
</template>
</van-cell>
<div class="user-data">
<div class="user-data-item">
<span>0</span>
<span>动态</span>
</div>
<div class="user-data-item">
<span>0</span>
<span>关注</span>
</div>
<div class="user-data-item">
<span>0</span>
<span>粉丝</span>
</div>
</div>
</div>
<van-cell-group class="action-card">
<van-cell title="编辑资料" icon="edit" is-link />
<van-cell title="小思同学" icon="chat-o" is-link />
<van-cell title="退出登录" icon="warning-o" is-link />
</van-cell-group>
</div>
</template>
<script>
export default {
name: "User"
}
</script>
<style lang="less" scoped>
.user-container {
background-color: #007bff;
color: white;
// 让组件距离顶部有一段距离,页面会更美观一些。
padding-top: 20px;
.user-card {
background-color: #007bff;
color: white;
&::after {
// 让下划线不显示
display: none;
}
.avatar {
width: 60px;
height: 60px;
background-color: #fff;
border-radius: 50%;
margin-right: 10px;
}
.username {
font-size: 14px;
font-weight: bold;
}
.van-cell__title {
margin-top: 5px;
}
}
.user-data {
display: flex;
// 等距排列
justify-content: space-evenly;
align-items: center;
font-size: 14px;
padding: 30px 0;
.user-data-item {
display: flex;
direction: column;
justify-content: center;
align-items: center;
flex: 1;
}
}
}
</style>
|
这个页面主要用到了van-cell和van-cell-group组件,这是一种移动端中非常常见的单元格,可以通过template
#icon等方式自定义其中的图标、标题和label等元素,属性title、label、icon也可以在默认样式下制定其内容,另外如果增加is-link属性,单元格右侧会出现>符号,标示该单元格可以点击跳转。
Home组件
Home组件其实就两部分:顶部导航条和文章内容,所以代码也很简单:
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
| <template>
<div class="home-container">
<van-nav-bar title="头条" fixed placeholder />
<ArticleInfo />
</div>
</template>
<script>
export default {
name: "Home"
}
</script>
<style lang="less" scoped>
.home-container {
.van-nav-bar {
background-color: #007bff;
// 由于导航条内部的title没有加上hash值,直接查找是找不到的,所以需要加上deep
/deep/ .van-nav-bar__title {
color: #fff;
}
}
}
</style>
|
这里我们需要用到axios来请求文章数据,所以首先需要封装一个API
首先安装axios
npm i axios -S
创建utils/request.js
1 2 3 4 5 6 7 8 9
| import axios from 'axios';
const request = axios.create({
baseURL: 'https://www.escook.cn/'
});
export default request;
|
这里封装了一个请求API,基础地址为https://www.escook.cn/
接着在封装一个文章请求API,
创建api/ArticleInfo.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import request from '@/utils/request.js';
const getArticleAPI = function(page, limit) {
return request.get('/articles', {
params: {
page,
limit
}
});
};
export default getArticleAPI;
|
最后即可在Home的代码部分请求数据
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
| import getArticleAPI from '@/api/ArticleInfo.js';
data() {
return {
page: 1,
limit: 10,
articleList: [];
}
},
created() {
this.initArticleInfo();
},
methods: {
async initArticleInfo() {
const { data: res } = await getArticleAPI(this.page, this.limit);
this.articleList = res;
}
}
|
拿到数据之后,就可以赋值给上面组件了
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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
| <ArticleInfo
v-for="item in articleList"
:key="item.id"
:title="item.title"
:cmt-count="item.comm_count"
:author="item.aut_name"
:time="item.pubdate"
:cover="item.cover"
/>
## 实现ArticleInfo组件
创建components/Article/ArticleInfo.vue:
<template>
<div class="articleinfo-container">
<van-cell>
<template #title>
<div class="title-box">
<span>{{ title }}</span>
<img :src="cover.images[0]" class="thumb" v-if="cover.type===1"/>
</div>
<div class="thumb-box" v-if="cover.type===3">
<img :src="item" v-for="(item, i) in cover.images" :key="i" class="thumb">
</div>
<template>
<template #label>
<div class="label-box">
<span> {{ author }} {{ cmtCount }}评论 发布日期 {{ time }}</span>
<van-icon icon="cross" />
</div>
</template>
</van-cell>
</div>
</template>
<script>
export default {
name: "ArticleInfo",
props: {
title: {
type: String,
default: ""
},
author: {
type: String,
default: ""
}
time: {
type: String,
default: ""
},
cover: {
type: Object,
default: { type: 0 }
},
cmtCount {
type: [String, Number],
default: ""
}
}
}
</script>
<style lang="less" scoped>
.title-box {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.thumb-box {
display: flex;
justify-content: space-between;
}
.label-box {
display: flex;
justify-content: space-between;
align-items: center;
}
.thumb {
// 矩形黄金比例0.618
width: 113px;
height: 70px;
border-radius: 50%;
background-color: #fff;
}
</style>
|
上拉刷新
上拉刷新可以借助van-list实现,在Home.vue中实现代码如下:
1 2 3 4 5 6
| <van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<ArticleInfo ....../>
</van-list>
|
在script中添加如下代码:
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 105 106 107 108 109 110
| data() {
return {
loading: true,
finished: false
}
},
methods: {
onLoad() {
this.page ++;
this.initArticleList()
},
async initArticleList() {
this.loading = false;
const { data: res } = await getArticleAPI(this.page, this.limit);
if (res.length == 0) {
this.finished = true;
return;
}
this.articleList = [...this.articleList, ...res];
}
}
## 下拉刷新
下拉刷新则需要用到van-pull-refresh组件
<van-pull-refresh v-model="refreshing" :disabled="finished" @refresh="onRefresh">
<......>
</van-pull-refresh>
在script中添加如下代码:
data() {
return {
refreshing: true
}
},
methods: {
onRefresh() {
this.page ++;
this.initArticleInfo(true);
},
async initArticleInfo(isDownScroll = false) {
this.loading = false;
this.refreshing = false;
const { data: res } = await this.getArticlAPI(this.page, this.limit);
if (res.length == 0) {
this.finished = true;
return;
}
if (isDownScroll) {
this.articleList = [...res, ...this.articleList];
return;
}
this.articleList = [...this.articleList, ...res];
}
}
|
逻辑基本和van-list一致,就是finished赋值给:disabled属性,添加一个isDownScroll参数来区分刷新方式。
定制主题
具体步骤参考:定制主题
首先修改main.js ,将css文件改成less文件
import ‘vant/lib/index.less’;
创建一个vue.config.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
| module.exports = {
css: {
loaderOptions: {
less: {
// 当less-loader的版本小于6,下面这一级可以去掉
lessOptions: {
modifyVars: {
'nav-bar-background-color': red;
}
}
}
}
}
}
|
但是这种方式每次修改都需要重启服务器才能看到效果,并不适合日常使用,所以就有了下面的做法:
创建theme.less:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @blue:
@nav-bar-background-color: @blue;
@nav-bar-text-color:
然后在vue.config.js中引入该文件:
const path = require('path');
const theme = path.join(__dirname, './src/theme.less');
......
modifyVars: {
hack: `true; @import "${theme}";`
}
......
|
记录位置
这个功能需要用到防抖函数,该函数集成在lodash库中,需要进行安装:
npm i lodash -S
首先要给内容组件添加vue内置的缓存组件keep-alive,让内容在刷新页面后还依然保存着:
修改App.vue:
1 2 3 4 5
| <keep-alive include="Home">
<router-view />
</keep-alive>
|
其中include属性指明要缓存的组件,这里只缓存Home.vue组件
在路由中添加meta信息,用于永久保存上次的浏览位置:
{ path: ‘/home’, component: Home, meta: { isRecord: true, top: 0 } }
接着在路由中获取上次保存的浏览位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const router = new VueRouter({
routes,
scrollBehaviour(to, from, savePosition) {
if (savePosition) {
return savePosition;
}
else {
return { x: 0, y: to.meta.top || 0 };
}
}
})
|
最后在Home.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
| import _ from 'lodash';
let fn = null;
export default {
activated() {
fn = this.scrollHandler();
window.addEventListener('scroll', fn);
},
deactivated() {
window.addEventListener('scroll', fn);
fn = null;
},
methods: {
scrollHandler() {
return _.debounce(
() => {
this.$route.meta.top = window.scrollY;
},
50,
{ trailing: true }
)
}
}
}
|
参考:
Vue2.0-01.初始化 -
创建并梳理项目结构
Vue2.0-02.初始化 -
安装和配置Vant组件库
Vue2.0-03.初始化 -
使用Tabbar组件并开启路由模式
Vue2.0-04.初始化 -
通过路由展示对应的Tabbar页面
Vue2.0-05.初始化 -
使用Navbar组件
Vue2.0-06.初始化 -
覆盖Navbar的默认样式
Vue2.0-07.文章列表 -
了解获取列表数据的API接口
Vue2.0-08.文章列表 -
封装utils目录下的request模块
Vue2.0-09.文章列表 -
在Home组件中封装
Vue2.0-10.文章列表 -
封装articleAPI模块
Vue2.0-11.文章列表 -
封装ArticleInfo组件
Vue2.0-12.文章列表 -
为ArticleInfo组件封装props属性
Vue2.0-13.文章列表 -
为ArticleInfo封装cover属性
Vue2.0-14.上拉加载更多 -
了解List组件的基本用法
Vue2.0-15.上拉加载更多 -
初步使用List组件
Vue2.0-16.上拉加载更多 -
实现上拉加载更多效果
Vue2.0-17.下拉刷新 -
实现下拉刷新的功能
Vue2.0-18.定制主题 -
说明Vant定制主题的核心原理
Vue2.0-19.定制主题 -
直接覆盖主题变量
Vue2.0-20.定制主题 -
less的正确打开方式
Vue2.0-21.定制主题 -
通过theme.less定制主题,推荐形式
Vue2.0-22.最后