【VUE】day08黑马头条小项目
- [1. 项目创建](#1. 项目创建)
- [2. 安装和使用Vant](#2. 安装和使用Vant)
- [3. 使用Tabbar组件并开启路由模式](#3. 使用Tabbar组件并开启路由模式)
- [4. 使用Navbar组件](#4. 使用Navbar组件)
- [5. 覆盖Navbar的默认样式](#5. 覆盖Navbar的默认样式)
- [6. 了解获取列表数据的API接口](#6. 了解获取列表数据的API接口)
1. 项目创建


2. 安装和使用Vant
官网地址:https://vant.pro/vant/#/zh-CN
Vant 是一个轻量、可定制的移动端组件库,于 2017 年开源。
目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,并由社区团队维护 React 版本和支付宝小程序版本。

main.js
javascript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// 导入并安装 Vant 组件库
import Vant from 'vant'
// 切记:为了能够覆盖默认的 less 变量,这里一定要把后缀名改为 .less
import 'vant/lib/index.less'
Vue.use(Vant)
# 在生产环境中vue2中自动移除开发模式的调试信息
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')
index.js
javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
// 导入需要的组件
import Home from '@/views/Home/Home.vue'
import User from '@/views/User/User.vue'
// 把 VueRouter 安装为 Vue 的插件
Vue.use(VueRouter)
// 路由规则的数组
const routes = [
// 定义首页的路由规则
{ path: '/', component: Home },
// 定义我的路由规则
{ path: '/user', component: User }
]
// 创建路由实例对象
const router = new VueRouter({
routes
})
export default router
注意1: 为什么不需要Vue.use(router)
在 Vue 2 中,不需要显式调用 Vue.use(router)
,这是因为 Vue Router
是一个 插件,但它的注册方式与其他 Vue 插件(如 Vant)有所不同。
Vue.use(plugin)
主要用于全局注册 Vue 插件,这个方法会调用插件的 install 方法,例如:
javascript
Vue.use(Vant); // Vant 组件库是 Vue 插件
然而,Vue Router 并不是通过 Vue.use(router)
来注册的,而是通过 在 Vue 实例中传递 router 配置项 来使用的:
javascript
new Vue({
router, // 这里直接传入了 router
render: h => h(App)
}).$mount('#app');
这样 Vue 会自动将 router 实例挂载到 Vue 实例上,并让所有组件可以通过 this.$router
和 this.$route
访问路由对象。
注意2: 什么时候需要 Vue.use(VueRouter)?
虽然 Vue.use(router)
不会被调用,但 Vue Router 本身仍然需要 Vue.use(VueRouter)
来注册 Vue 插件,通常在 router.js 文件中会有类似:
javascript
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter); // 这里才是 Vue Router 的正确注册方式
const routes = [
{ path: '/', component: () => import('@/views/Home.vue') },
{ path: '/about', component: () => import('@/views/About.vue') }
];
const router = new VueRouter({
mode: 'history',
routes
});
export default router;
然后在 main.js 中直接导入 router 并传递给 Vue 实例,这样就不需要 Vue.use(router)
了。
总结:
-
Vue Router 需要 Vue.use(VueRouter),但它是在 router.js 里注册的,所以 main.js 里不需要再 Vue.use(router)。
-
Vant 组件库等插件需要 Vue.use(Vant),否则组件无法使用。
-
在
new Vue({...})
里传入 router,Vue 就会自动绑定路由,不需要额外 Vue.use(router)。
所以,在 main.js 里:
- 你需要 Vue.use(Vant),否则 Vant 组件不可用;
- 但不需要 Vue.use(router),因为 router 作为 Vue 实例的参数传入即可。
一个是实例一个是插件
3. 使用Tabbar组件并开启路由模式
javascript
<template>
<div class="home-container">
<!-- 头部区域 -->
<van-nav-bar title="黑马头条" fixed />
<!-- 导入,注册,并使用 ArticleInfo 组件 -->
<!-- 在使用组件的时候,如果某个属性名是"小驼峰"形式,则绑定属性的时候,建议改写成"连字符"格式。例如: -->
<!-- cmtCount 建议写成 cmt-count -->
<van-pull-refresh v-model="isLoading" :disabled="finished" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<ArticleInfo
v-for="item in artlist"
:key="item.id"
:title="item.title"
:author="item.aut_name"
:cmt-count="item.comm_count"
:time="item.pubdate"
:cover="item.cover"
></ArticleInfo>
</van-list>
</van-pull-refresh>
</div>
</template>
<script>
// 按需导入 API 接口
import { getArticleListAPI } from '@/api/articleAPI.js'
// 导入需要的组件
import ArticleInfo from '@/components/Article/ArticleInfo.vue'
export default {
name: 'Home',
data() {
return {
// 页码值
page: 1,
// 每页显示多少条数据
limit: 10,
// 文章的数组
artlist: [],
// 是否正在加载下一页数据,如果 loading 为 true,则不会反复触发 load 事件
// 每当下一页数据请求回来之后,千万要记得,把 loading 从 true 改为 false
loading: true,
// 所有数据是否加载完毕了,如果没有更多数据了,一定要把 finished 改成 true
finished: false,
// 是否正在下拉刷新
isLoading: false
}
},
created() {
this.initArticleList()
},
methods: {
// 封装获取文章列表数据的方法
async initArticleList(isRefresh) {
// 发起 GET 请求,获取文章的列表数据
const { data: res } = await getArticleListAPI(this.page, this.limit)
if (isRefresh) {
// 证明是下拉刷新;新数据在前,旧数据在后
// this.artlist = [新数据在后, 旧数据在前]
this.artlist = [...res, ...this.artlist]
this.isLoading = false
} else {
// 证明是上拉加载更多;旧数据在前,新数据在后
// this.artlist = [旧数据在前, 新数据在后]
this.artlist = [...this.artlist, ...res]
this.loading = false
}
if (res.length === 0) {
// 证明没有下一页数据了,直接把 finished 改为 true,表示数据加载完了!
this.finished = true
}
},
// 只要 onLoad 被调用,就应该请求下一页数据
onLoad() {
console.log('触发了 load 事件!')
// 1. 让页码值 +1
this.page++
// 2. 重新请求接口获取数据
this.initArticleList()
},
// 下拉刷新的处理函数
onRefresh() {
console.log('触发了下拉刷新!')
// 1. 让页码值 +1
this.page++
// 2. 重新请求接口获取数据
this.initArticleList(true)
}
},
// 注册组件
components: {
ArticleInfo
}
}
</script>
<style lang="less" scoped>
.home-container {
padding: 46px 0 50px 0;
}
</style>
App.vue
javascript
<template>
<div>
<!-- 路由占位符 -->
<router-view></router-view>
<!-- Tabbar 区域 -->
<van-tabbar route>
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
<van-tabbar-item icon="user-o" to="/user">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style lang="less" scoped></style>
4. 使用Navbar组件
Home.vue
javascript
<template>
<div class="home-container">
<!-- 头部区域 -->
<van-nav-bar title="黑马头条" fixed />
<!-- 导入,注册,并使用 ArticleInfo 组件 -->
<!-- 在使用组件的时候,如果某个属性名是"小驼峰"形式,则绑定属性的时候,建议改写成"连字符"格式。例如: -->
<!-- cmtCount 建议写成 cmt-count -->
<van-pull-refresh v-model="isLoading" :disabled="finished" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<ArticleInfo
v-for="item in artlist"
:key="item.id"
:title="item.title"
:author="item.aut_name"
:cmt-count="item.comm_count"
:time="item.pubdate"
:cover="item.cover"
></ArticleInfo>
</van-list>
</van-pull-refresh>
</div>
</template>
<script>
// 按需导入 API 接口
import { getArticleListAPI } from '@/api/articleAPI.js'
// 导入需要的组件
import ArticleInfo from '@/components/Article/ArticleInfo.vue'
export default {
name: 'Home',
data() {
return {
// 页码值
page: 1,
// 每页显示多少条数据
limit: 10,
// 文章的数组
artlist: [],
// 是否正在加载下一页数据,如果 loading 为 true,则不会反复触发 load 事件
// 每当下一页数据请求回来之后,千万要记得,把 loading 从 true 改为 false
loading: true,
// 所有数据是否加载完毕了,如果没有更多数据了,一定要把 finished 改成 true
finished: false,
// 是否正在下拉刷新
isLoading: false
}
},
created() {
this.initArticleList()
},
methods: {
// 封装获取文章列表数据的方法
async initArticleList(isRefresh) {
// 发起 GET 请求,获取文章的列表数据
const { data: res } = await getArticleListAPI(this.page, this.limit)
if (isRefresh) {
// 证明是下拉刷新;新数据在前,旧数据在后
// this.artlist = [新数据在后, 旧数据在前]
this.artlist = [...res, ...this.artlist]
this.isLoading = false
} else {
// 证明是上拉加载更多;旧数据在前,新数据在后
// this.artlist = [旧数据在前, 新数据在后]
this.artlist = [...this.artlist, ...res]
this.loading = false
}
if (res.length === 0) {
// 证明没有下一页数据了,直接把 finished 改为 true,表示数据加载完了!
this.finished = true
}
},
// 只要 onLoad 被调用,就应该请求下一页数据
onLoad() {
console.log('触发了 load 事件!')
// 1. 让页码值 +1
this.page++
// 2. 重新请求接口获取数据
this.initArticleList()
},
// 下拉刷新的处理函数
onRefresh() {
console.log('触发了下拉刷新!')
// 1. 让页码值 +1
this.page++
// 2. 重新请求接口获取数据
this.initArticleList(true)
}
},
// 注册组件
components: {
ArticleInfo
}
}
</script>
<style lang="less" scoped>
.home-container {
padding: 46px 0 50px 0;
}
</style>
5. 覆盖Navbar的默认样式
6. 了解获取列表数据的API接口
后端Java写一个接口
java
package com.study.docker.dockertest.service.impl;
import com.study.docker.dockertest.service.TouTiaoService;
import com.study.docker.dockertest.vo.ArticleVo;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
public class TouTiaoServiceImpl implements TouTiaoService {
/**
* 获取文章列表
*
* @return List<ArticleVo> 文章列表
*/
@Override
public List<ArticleVo> list() {
List<ArticleVo> articles = new ArrayList<>();
// 第一篇文章(图片中完整展示的)
ArticleVo article1 = new ArticleVo();
article1.setArt_id("8163");
article1.setTitle("ios原生混合RN开发最佳实践");
article1.setAut_id("1111");
article1.setComm_count("254");
article1.setPubdate("2019-03-11 09:00:00");
article1.setAut_name("黑马先锋");
article1.setIs_top(0);
ArticleVo.Cover cover1 = article1.new Cover();
cover1.setType(3);
cover1.setImages(Arrays.asList(
"https://inews.gtimg.com/om_bt/O17etAI6P8U6pnm0sfosjXWwgPp-PFOsiHCu0wxhhBK6MAA/1000",
"https://inews.gtimg.com/om_bt/O6G9Or2buWkNlHJlLnn9c_9g1GLLPyIpRz8P5y4MzeE90AA/641",
"https://inews.gtimg.com/om_bt/ORmcG_mJOQJ7iSRNsd9fTekT4T5ReR9o0mGUSxAIrAYVoAA/1000"
));
article1.setCover(cover1);
articles.add(article1);
// 第二篇文章(图片中部分展示的)
ArticleVo article2 = new ArticleVo();
article2.setArt_id("8089");
article2.setTitle("Typescript玩转设计模式之创建型模式");
article2.setAut_id("2222");
article2.setComm_count("189");
article2.setPubdate("2019-03-10 14:30:00");
article2.setAut_name("技术达人");
article2.setIs_top(1);
ArticleVo.Cover cover2 = article2.new Cover();
cover2.setType(1);
cover2.setImages(Arrays.asList(
"https://inews.gtimg.com/om_bt/OMFylDibTEK3jdSDnrdFppgs2veVhDmbgdIAbGCPngDJkAA/641"
));
article2.setCover(cover2);
articles.add(article2);
// 补充第三篇文章
ArticleVo article3 = new ArticleVo();
article3.setArt_id("7952");
article3.setTitle("Spring Boot微服务架构实战");
article3.setAut_id("3333");
article3.setComm_count("312");
article3.setPubdate("2019-03-09 10:15:00");
article3.setAut_name("架构师老王");
article3.setIs_top(0);
ArticleVo.Cover cover3 = article3.new Cover();
cover3.setType(2);
cover3.setImages(Arrays.asList(
"https://img14.360buyimg.com/n1/jfs/t1/221090/2/40815/112550/6645a642F1e63e250/e3a9ea63c4692f2d.jpg",
"https://img10.360buyimg.com/img/jfs/t1/236460/19/20812/60229/66b2f8adF04ae08f4/b33b92ffacb11e39.jpg.avif"
));
article3.setCover(cover3);
articles.add(article3);
// 补充第四篇文章
ArticleVo article4 = new ArticleVo();
article4.setArt_id("7821");
article4.setTitle("Flutter跨平台开发指南");
article4.setAut_id("4444");
article4.setComm_count("176");
article4.setPubdate("2019-03-08 16:45:00");
article4.setAut_name("移动开发专家");
article4.setIs_top(1);
ArticleVo.Cover cover4 = article4.new Cover();
cover4.setType(3);
cover4.setImages(Arrays.asList(
"https://img10.360buyimg.com/img/jfs/t1/68728/39/27995/65022/66b2f8deFdaedabc0/3ff4998c66b69a12.jpg.avif",
"https://img10.360buyimg.com/img/jfs/t1/74904/21/26801/184607/66b310d1F964a7169/947ad3d66611a423.jpg.avif",
"https://img10.360buyimg.com/img/jfs/t1/84175/19/27426/113247/66b3107bFdea12089/a405df49190b43d4.jpg.avif"
));
article4.setCover(cover4);
articles.add(article4);
return articles;
}
}

前端调用这个接口

javascript
Home.vue
<template>
<div class="home-container">
<!-- 头部区域 -->
<van-nav-bar title="黑马头条" fixed />
<!-- 导入,注册,并使用 ArticleInfo 组件 -->
<!-- 在使用组件的时候,如果某个属性名是"小驼峰"形式,则绑定属性的时候,建议改写成"连字符"格式。例如: -->
<!-- cmtCount 建议写成 cmt-count -->
<van-pull-refresh v-model="isLoading" :disabled="finished" @refresh="onRefresh">
<van-list v-model="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<ArticleInfo
v-for="item in artlist"
:key="item.id"
:title="item.title"
:author="item.aut_name"
:cmt-count="item.comm_count"
:time="item.pubdate"
:cover="item.cover"
></ArticleInfo>
</van-list>
</van-pull-refresh>
</div>
</template>
<script>
// 按需导入 API 接口
import { getArticleListAPI } from '@/api/articleAPI.js'
// 导入需要的组件
import ArticleInfo from '@/components/Article/ArticleInfo.vue'
export default {
name: 'Home',
data() {
return {
// 页码值
page: 1,
// 每页显示多少条数据
limit: 10,
// 文章的数组
artlist: [],
// 是否正在加载下一页数据,如果 loading 为 true,则不会反复触发 load 事件
// 每当下一页数据请求回来之后,千万要记得,把 loading 从 true 改为 false
loading: true,
// 所有数据是否加载完毕了,如果没有更多数据了,一定要把 finished 改成 true
finished: false,
// 是否正在下拉刷新
isLoading: false
}
},
created() {
this.initArticleList()
},
methods: {
// 封装获取文章列表数据的方法
async initArticleList(isRefresh) {
// 发起 GET 请求,获取文章的列表数据
const { data: { data: res = [] } = {} } = await getArticleListAPI(this.page, this.limit)
if (isRefresh) {
// 证明是下拉刷新;新数据在前,旧数据在后
// this.artlist = [新数据在后, 旧数据在前]
this.artlist = [...res, ...this.artlist]
this.isLoading = false
} else {
// 证明是上拉加载更多;旧数据在前,新数据在后
// this.artlist = [旧数据在前, 新数据在后]
this.artlist = [...this.artlist, ...res]
this.loading = false
}
if (res.length === 0) {
// 证明没有下一页数据了,直接把 finished 改为 true,表示数据加载完了!
this.finished = true
}
},
// 只要 onLoad 被调用,就应该请求下一页数据
onLoad() {
console.log('触发了 load 事件!')
// 1. 让页码值 +1
this.page++
// 2. 重新请求接口获取数据
this.initArticleList()
},
// 下拉刷新的处理函数
onRefresh() {
console.log('触发了下拉刷新!')
// 1. 让页码值 +1
this.page++
// 2. 重新请求接口获取数据
this.initArticleList(true)
}
},
// 注册组件
components: {
ArticleInfo
}
}
</script>
<style lang="less" scoped>
.home-container {
padding: 46px 0 50px 0;
}
</style>
javascript
ArticleInfo.vue
<template>
<div>
<van-cell>
<!-- 标题区域的插槽 -->
<template #title>
<div class="title-box">
<!-- 标题 -->
<span>{{ title }}</span>
<!-- 单张图片 -->
<img :src="cover.images[0]" alt="" class="thumb" v-if="cover.type === 1" />
</div>
<!-- 三张图片 -->
<div class="thumb-box" v-if="cover.type === 3">
<img :src="item" alt="" class="thumb" v-for="(item, i) in cover.images" :key="i" />
</div>
</template>
<!-- label 区域的插槽 -->
<template #label>
<div class="label-box">
<span>作者 {{ author }} {{ cmtCount }} 评论 发布日期 {{ time }}</span>
<!-- 关闭按钮 -->
<van-icon name="cross" />
</div>
</template>
</van-cell>
</div>
</template>
<script>
export default {
name: 'ArticleInfo',
// 自定义属性
props: {
// 文章的标题
title: {
type: String,
default: ''
},
// 作者名字
author: {
type: String,
default: ''
},
// 评论数
cmtCount: {
// 通过数组形式,为当前属性定义多个可能的类型
type: [Number, String],
default: 0
},
// 发布日期
time: {
type: String,
default: ''
},
// 封面的信息对象
cover: {
type: Object,
// 通过 default 函数,返回 cover 属性的默认值
default: function() {
// 这个 return 的对象就是 cover 属性的默认值
return { type: 0 }
}
}
}
}
</script>
<style lang="less" scoped>
.label-box {
display: flex;
justify-content: space-between;
align-items: center;
}
.thumb {
// 矩形黄金比例:0.618
width: 113px;
height: 70px;
background-color: #f8f8f8;
object-fit: cover;
}
.title-box {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.thumb-box {
display: flex;
justify-content: space-between;
}
</style>