本文将开发一款仿“今日头条”的新闻App。该案例是基于 Vue3.0 + Vue Router + webpack + TypeScript
等技术栈实现的一款新闻资讯类App,适合有一定Vue框架使用经验的开发者进行学习。
项目源码在文章末尾
1 项目概述
该项目是一款“今日头条”的新闻资讯App,主要有以下功能。
- 新闻分类
- 首页新闻列表
- 刷新加载最新新闻
- 用户私信留言
- 新闻搜索
- 查看新闻详情
1.1 开发环境
本项目是基于Vue3框架开发的一款WebApp,使用Vue CLI脚手架工具创建项目。在指定的硬盘目录处启动命令行工具,例如,在 C:\project
目录下打开命令行工具,并执行以下命令。
#安装脚手架
npm i -g @vue/cli
#创建项目
vue create toutiao
项目创建成功后,继续在命令行工具中执行 cd toutiao 命令,进入项目根目录,安装Vant UI组件库和axios模块。执行命令如下:
#安装Vant3 UI组件库
npm i vant@next -S
#安装axios模块
npm i axios -S
项目的调试使用Google Chrome浏览器的控制台进行,在浏览器中按下F12键,然后单击“切换设备工具栏”,进入移动端的调试界面,可以选择相应的设备进行调试。
项目运行的效果如图1 所示。
图 1 项目效果图
1.2 项目结构
项目结构如图2所示,其中src文件夹是项目的源文件目录,src文件夹下的项目结构如图3所示。
图2 项目结构
图3 src文件夹
项目结构中主要文件说明如下。
- node_modules:项目依赖管理目录。
- public:项目的静态文件存放目录,也是本地服务器的根目录。
- src:项目源文件存放目录。
- package.json:项目npm配置文件。
- vue.config.js:项目构建配置文件
src文件夹目录说明如下。
- assets:静态资源文件存放目。
- components:公共组件存放目录。
- hooks:项目的静态数据和模块封装管理目录。
- router:路由配置文件存放目录。
- store:状态管理配置存放目录。
- views:视图组件存放目录。
- App.vue:项目的根组件。
- main.ts:项目的入口文件。
- Shims-vue.d.ts: typescript的适配定义文件。
2 入口文件
项目的入口文件有 index.html、main.ts和App.vue三个文件,这些入口文件的具体内容介绍如下。
2.1 项目入口页面
index.html是项目默认的主渲染页面文件,主要用于Vue实例挂载点的声明与DOM渲染。代码如下:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
2.2 程序入口文件
main.ts是程序的入口文件,主要用于加载各种公共组件和初始化Vue实例。本项目是基于Vue3开发的,在入口文件中引入createApp()来创建vue实例对象。项目中的路由设置和引用的Vant UI组件库也是在该文件中定义的。代码如下:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Vant from 'vant';
import 'vant/lib/index.css';
const app = createApp(App)
app.use(store)
app.use(router)
app.use(Vant)
app.mount('#app')
2.3 组件入口文件
App.vue是项目的根组件,所有的页面都是在App.vue下面切换的,所有的页面组件都是App.vue的子组件。在App.vue组件内只需要使用 组件作为占位符,就可以实现各个页面的引入。代码如下:
<template>
<router-view/>
</template>
2.4 路由文件
在src/router/index.ts文件中,定义了项目的所有跳转的路由,代码如下:
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import ('@/views/Home.vue')
},
{
path: '/search',
name: 'Search',
component: () => import ('@/views/Search.vue')
},
{
path: '/details',
name: 'Details',
component: () => import ('@/views/Details.vue')
},
{
path: '/message',
name: 'Message',
component: () => import ('@/views/Message.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
3 项目组件
项目中所有页面组件都在views文件夹中定义,所有的公共组件都在components文件夹中定义,具体组件内容介绍如下。
3.1 公共组件
在首页新闻列表和新闻内容页中,都需要使用到新闻列表的展示,为了增强代码的可扩展性,可以在components文件夹下创建NewsCard.vue新闻卡片的公共组件。代码如下:
<template>
<div class="news-card" @click="onClick">
<van-row gutter="20">
<van-col span="16">
<div class="news-title">{{title}}</div>
<div class="news-msg">
<span>{{author}}</span>
<span>{{date}}</span>
</div>
</van-col>
<van-col span="8">
<van-image :src="pic" style="height: 100%">
<template v-slot:error>加载失败</template>
</van-image>
</van-col>
</van-row>
</div>
</template>
<script>
export default {
props: {
title: String,
author: String,
date: String,
pic: String
},
setup(props,context) {
//新闻点击事件
const onClick = () => {
}
return {
onClick
}
}
}
</script>
<style scoped>
.news-card{
box-sizing: border-box;
padding: 0px 10px;
}
.van-row{
padding: 15px 0px;
border-bottom: 1px solid #F7F7F7;
}
.news-title{
color: #222;
font-size: 18px;
}
.news-msg{
font-size: 12px;
color: #999;
margin-top: 5px;
}
.news-msg span{
margin-right: 8px;
}
</style>
3.2 首页导航栏
App首页汇集了整个应用的核心功能入口,在头部导航栏部分,主要涉及了两个功能的入口按钮,分别是“私信”和“搜索”。在头部导航栏的下方,设计了新闻分类的标签页按钮,方便用户切换不同类别的新闻列表。效果如图4所示。
图 4 首页导航栏效果
Home.vue首页组件,代码如下:
<template>
<div class="page-body">
<!-- 头部 -->
<van-nav-bar fixed z-index="1000">
<template #title>
<span class="top-title">今日头条</span>
<van-icon name="replay" color="#fff" size="18"/>
</template>
<template #left>
<van-icon name="envelop-o" color="#fff" size="22" />
</template>
<template #right>
<van-icon name="search" color="#fff" size="22" />
</template>
</van-nav-bar>
<!-- 分类导航 -->
<van-tabs
v-model:active="active"
background="#F4F5F6"
sticky
offset-top="46"
title-active-color="#EE0A24"
>
<van-tab v-for="tab in tabsList"
:key="tab.key" :title="tab.name" :name="tab.key">
<div class="tab-views">
<!-- 此处为新闻列表 -->
</div>
</van-tab>
</van-tabs>
</div>
</template>
新闻类别的标签页数据使用的是本地静态数据,在项目中的src/hooks文件夹下创建tabs.ts文件,用于保存新闻分类的静态数据。代码如下:
const tabs = [
{
key: 'top',
name: '推荐'
},
{
key: 'guonei',
name: '国内'
},
{
key: 'guoji',
name: '国际'
},
{
key: 'yule',
name: '娱乐'
},
{
key: 'tiyu',
name: '体育'
},
{
key: 'junshi',
name: '军事'
},
{
key: 'keji',
name: '科技'
},
{
key: 'caijing',
name: '财经'
},
{
key: 'shishang',
name: '时尚'
},
{
key: 'youxi',
name: '游戏'
},
{
key: 'qiche',
name: '汽车'
},
{
key: 'jiankang',
name: '健康'
}
]
export default tabs
在Home.vue文件中,进入src/hooks/tabs.ts文件,并设置为响应式数据。代码如下:
<script lang="ts">
import { ref } from "vue";
import tabs from "../hooks/tabs";
export default {
name: "Home",
setup() {
const tabsList = ref(tabs);
return {
tabsList
};
},
};
</script>
3.3 首页新闻列表
首页新闻列表的效果如图5所示。
图5 首页新闻列表效果
在首页中引入NewsCard.vue新闻卡片的公共组件,并对axios获取数据进行封装。由于本项目中使用的是“聚合数据平台”的数据接口,在请求服务端接口时会出现跨域,需要先配置本地服务器的请求代理。
在项目根目录下创建vue.config.js配置文件,在配置文件中添加proxy的配置。代码如下:
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://v.juhe.cn',
changeOrigin: true,
pathRewrite: {
"^/api": ""
}
}
}
}
}
封装axios请求方法,在src/hooks目录下创建sendhttp.ts文件,代码如下:
import axios from 'axios';
import { ref } from 'vue';
import { Toast } from 'vant';
//聚合数据上申请的用户密钥
const key = 'xxx'
function sendhttp(api: string, query: string) {
const result = ref(null)
//加载数据
const getData = (query: string, toast?: any) => {
axios.get(api, {
params: {
type: query,
key
}
}).then(res => {
console.log(res.data)
result.value = res.data.result.data
if(toast){
toast.clear()
}
}).catch(err=>{
if(toast){
toast.clear()
}
})
}
getData(query)
//切换导航事件
const tabsChange = (name: string) => {
getData(name)
}
//刷新事件
const replay = (name: string) => {
const toast = Toast.loading({
message: "加载中...",
forbidClick: true,
duration: 0,
loadingType: "spinner",
});
getData(name,toast)
}
return {
result,
tabsChange,
replay
}
}
export default sendhttp
在Home.vue文件中,引入src/hooks/sendhttp.ts文件,并在setup()函数中发送请求,将请求成功后的数据遍历在标签页中。
Home.vue文件,代码如下:
<template>
<div class="page-body">
<!-- 头部 -->
<van-nav-bar fixed z-index="1000" @click-left="onClickLeft" @click-right="onClickRight">
<template #title>
<span class="top-title">新闻头条</span>
<van-icon name="replay" color="#fff" size="18" @click="replay" />
</template>
<template #left>
<van-icon name="envelop-o" color="#fff" size="22" />
</template>
<template #right>
<van-icon name="search" color="#fff" size="22" />
</template>
</van-nav-bar>
<!-- 分类导航 -->
<van-tabs
v-model:active="active"
background="#F4F5F6"
sticky
offset-top="46"
title-active-color="#EE0A24"
@change="tabsChange"
>
<van-tab v-for="tab in tabsList" :key="tab.key" :title="tab.name" :name="tab.key">
<div class="tab-views">
<news-card v-for="news in result"
:key="news.uniquekey"
:title="news.title"
:author="news.author_name"
:date="news.date"
:pic="news.thumbnail_pic_s"
@click="onClickNews(news.uniquekey)">
</news-card>
</div>
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts">
import { ref } from "vue";
import tabs from "../hooks/tabs";
import sendhttp from '../hooks/sendhttp';
import NewsCard from '../components/NewsCard.vue';
import { useRoute, useRouter } from 'vue-router'
export default {
name: "Home",
components: {
'news-card': NewsCard
},
setup() {
const router = useRouter()
const active = ref('top');
const tabsList = ref(tabs);
//获取新闻数据
const {result, tabsChange, replay} = sendhttp('/api/toutiao/index',active.value)
//导航栏左侧按钮点击事件
const onClickLeft = () => {
router.push({
path: 'message'
})
}
//导航栏右侧按钮点击事件
const onClickRight = () => {
router.push({
path: 'search'
})
}
//点击新闻
const onClickNews = (id:string) => {
router.push({
path: 'details',
query: {
id
}
})
}
return {
active,
replay,
tabsList,
tabsChange,
result,
onClickLeft,
onClickRight,
onClickNews
};
},
};
</script>
<style scoped>
.van-nav-bar {
background-color: #d43d3d;
}
.top-title {
font-size: 18px;
font-weight: bold;
color: #fff;
margin-right: 5px;
}
.van-tabs{
top: 46px;
}
</style>
3.4 新闻详情页
点击新闻列表,触发NewsCard.vue新闻卡片组件上的点击事件,通过useRouter路由模块,将新闻的id传给Details.vue新闻详情组件。代码如下:
<template>
<div class="page-body">
<!-- 分类导航 -->
<van-tabs
v-model:active="active"
background="#F4F5F6"
sticky
offset-top="46"
title-active-color="#EE0A24"
@change="tabsChange"
>
<van-tab v-for="tab in tabsList" :key="tab.key" :title="tab.name" :name="tab.key">
<div class="tab-views">
<news-card v-for="news in result"
:key="news.uniquekey"
:title="news.title"
:author="news.author_name"
:date="news.date"
:pic="news.thumbnail_pic_s"
@click="onClickNews(news.uniquekey)">
</news-card>
</div>
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts">
import NewsCard from '../components/NewsCard.vue';
import { useRoute, useRouter } from 'vue-router'
export default {
name: "Home",
components: {
'news-card': NewsCard
},
setup() {
const router = useRouter()
//其他代码...
//点击新闻
const onClickNews = (id:string) => {
router.push({
path: 'details',
query: {
id
}
})
}
return {
onClickNews
};
},
};
</script>
在新闻详情页中,通过useRoute组件获取参数,得到新闻的id,并再次发送axios请求,获取当前新闻的详细数据。Details.vue组件,代码如下:
<template>
<div>
<!-- 导航栏 -->
<van-nav-bar left-arrow fixed @click-left="onClickLeft">
<template #left>
<van-icon name="arrow-left" size="16" color="#999" />
<span style="margin-left:10px;">{{result.author_name}}</span>
</template>
<template #right>
<van-icon name="share-o" size="20" color="#999" />
</template>
</van-nav-bar>
<!-- 正文 -->
<div class="news-body">
<!-- 新闻标题 -->
<div class="news-title">{{result.title}}</div>
<!-- 新闻信息 -->
<div class="news-msg">
<span>{{result.author_name}}</span>
<span>{{result.date}}</span>
</div>
<!-- 新闻内容 -->
<div class="news-content" v-html="result.content"></div>
</div>
</div>
</template>
<script>
import { ref } from "vue";
import { useRoute } from "vue-router";
import axios from 'axios';
export default {
setup() {
const route = useRoute();
const id = route.query.id;
const result = ref({
title:'',
author_name: '',
date: '',
content: ''
});
//获取请求
axios.get('/api/toutiao/content',{
params: {
uniquekey: id,
key: '26dafe8731502872b632b9552feccf06'
}
}).then(res=>{
result.value = res.data.result.detail
}).catch(err => {
})
//点击导航栏左侧按钮
const onClickLeft = () => {
window.history.back();
};
return {
onClickLeft,
result
};
},
};
</script>
<style scoped>
.news-body{
margin-top: 60px ;
box-sizing: border-box;
padding: 0px 15px;
}
.news-title{
font-size: 22px;
color: #222;
font-weight: bold;
}
.news-msg{
font-size: 12px;
color: #999;
margin: 10px 0px 20px;
}
.news-msg span{
margin-right: 10px;
}
</style>
新闻详情页的效果如图6所示。
图6 新闻详情页效果
3.5 私信留言页
点击头部导航栏的左侧图标按钮,跳转到私信留言列表页面,效果如图7所示。
图7 私信留言列表
Message.vue私信列表组件,代码如下:
<template>
<div>
<!-- 导航栏 -->
<van-nav-bar
title="私信"
fixed
left-arrow
@click-left="onClickLeft">
<template #left>
<van-icon name="arrow-left" size="16" color="#999" />
</template>
</van-nav-bar>
<!-- 留言列表 -->
<div class="message-list">
<van-cell v-for="(msg,index) in list" :key="index" :value="msg.date">
<!-- 使用 title 插槽来自定义标题 -->
<template #title>
<van-badge :dot="!msg.read">
<span class="custom-title">{{msg.name}}</span>
</van-badge>
</template>
<template #label>
<span class="msg-content">{{msg.content}}</span>
</template>
</van-cell>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import messageData from '../hooks/messageData';
export default {
setup(){
const list = ref(messageData)
const onClickLeft = () => {
window.history.back()
}
return {
onClickLeft,
list
}
}
}
</script>
<style scoped>
.message-list{
margin-top: 50px ;
}
.msg-content{
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
</style>
本项目的私信留言使用的是本地静态数据,在src/hooks目录下创建messageData.ts文件,用于留言静态数据。代码如下:
const messageData = [
{
name: '独上归州',
date: '2021-3-18',
content: '你好,你的那篇文章的链接失效了,能不能再重新发布一次?',
read: false
},
{
name: '椒房殿°',
date: '2021-1-8',
content: '前端简历的模板发一下吧',
read: false
},
{
name: '你在逗我笑i',
date: '2020-12-5',
content: '你好啊!',
read: false
},
{
name: '猫与玫瑰 ะ',
date: '2020-8-10',
content: '你的文章写的很棒啊!',
read: true
},
{
name: '那只小猪像你',
date: '2020-8-1',
content: '已转发',
read: true
},
{
name: '橙子姑娘',
date: '2020-5-11',
content: '已转发',
read: true
},
{
name: '忽然之间',
date: '2020-3-6',
content: '点个赞',
read: true
},
{
name: '配角戏',
date: '2020-1-3',
content: '已转发',
read: true
}
]
export default messageData
3.6 新闻搜索页面
在首页的头部导航栏,点击右侧按钮,跳转到新闻搜索页面,效果如图8所示。
图 8 新闻搜索也效果
Search.vue新闻搜索组件,代码如下:
<template>
<div class="search">
<!-- 搜索框 -->
<van-search
v-model="keywords"
shape="round"
placeholder="请输入你感兴趣的"
show-action
action-text="搜索"
@cancel="onClickRight">
<template #left>
<van-icon name="arrow-left" @click="onClickLeft" style="margin-right:10px"/>
</template>
</van-search>
<!-- 搜索记录 -->
<div class="search-history">
<div class="search-history-title">
<span>搜索记录</span>
<van-icon name="delete-o" @click="clear" />
</div>
<div>
<van-tag v-for="(kw,index) in list" :key="index" closeable size="medium" type="primary" @close="delTag(index)">
{{kw}}
</van-tag>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import { Dialog } from 'vant';
export default {
setup() {
const keywords = ref('')
const list = ref(['千锋教育','前端教程'])
//点击导航栏左侧按钮
const onClickLeft = () => {
window.history.back()
}
//点击导航栏右侧搜索按钮
const onClickRight = () => {
list.value.push(keywords.value)
console.log(keywords)
}
//删除搜索记录
const delTag = (index) => {
list.value.splice(index,1)
}
//清空搜索记录
const clear = () => {
Dialog.confirm({
message: '确定要清空记录吗?',
}).then(() => {
list.value = []
}).catch(() => {});
}
return {
keywords,
onClickLeft,
delTag,
clear,
onClickRight,
list
}
}
}
</script>
<style scoped>
.van-search{
border-bottom: 1px solid #eee;
}
.search-history{
box-sizing: border-box;
padding: 0px 15px;
margin-top: 10px ;
}
.search-history-title{
height: 30px;
display: flex;
align-items: center;
justify-content: space-between;
}
.search-history-title>span{
font-size: 14px;
}
.van-tag{
margin: 6px 8px;
}
</style>
项目源码下载地址:
https://download.csdn.net/download/p445098355/89570498