cnode社区基本架构
组件:
- Header头部
- PostList 列表
- Article 文章详情页
- SlideBar 侧边栏
- UserInfo 用户个人信息
- pagination 分页组件
起步:
打开终端,选择项目位置。
vue init webpack cnode 建立项目
npm install 安装依赖
npm run dev 启动项目
开始创建组件
先创建Header组件。
创建完成以后,在app.vue组件中通过import引入Header组件,然后注入app.vue的components中,再在app.vue的template中渲染。创建PostList组件。
因为这个组件要显示文章列表,可以设置一个正在加载的动画,当数据未返回的时候,显示正在加载的动画,提升用户体验。
API接口:https://cnodejs.org/api/v1/topics
- 接口参数:limit、page
- 拿到的参数分析:
- 头像:author. avatar_url
- 回复量/浏览量:reply_count/visit_count
- 帖子标题:title
- 需要使用过滤器:
- 时间:last_reply_at
- 帖子分类:
1、top:代表是否置顶
2、good:代表是否精华
3、tab:是表示出了置顶和精华之外的其他帖子分类,包括以下这些:
- share:分享
- ask:问答
- job:招聘
要获取到这些数据,我们需要主动向服务器发送请求。这里就可以用到axios这个插件。
什么是axios?
axios是基于promise(诺言)用于浏览器和node.js是http客户端。
axios的作用是什么呢:axios主要是用于向后台发起请求的,还有在请求中做更多是可控功能。
axios有以下特点:
1、从浏览器创建 XMLHttpRequest
2、从 node.js 创建http请求
3、支持Promise API
4、拦截请求和响应
5、转换请求数据和相应数据
6、能取消请求
7、自动转换JSON数据
8、客户端支持防御 XSRF
使用前要先安装 切换到项目目录下,然后:
npm install axios
安装好以后,要在main.js中引入。
一般情况下,全局使用的东西,都在main.js中引入,比如 router、Vue等;
引入之后,要挂载到Vue原型上:
Vue.prototype.$http = Axios
$http 是自定义的,随便写。然后我们就可以用 this.$http发送请求了。
我们可以在methods里,定义一个getData函数去获取数据。
methods:{
getData(){
this.$http.get('https://cnodejs.org/api/v1/topics',{
params:{
page:1,
limit:20
}
})
.then(res=>{
this.isLoading = false; //数据加载成功后,加载动画取消
console.log(res);
this.posts = res.data.data; //拿到数据以后,把数据复制给posts
})
.catch(err=>{
console.log(err);
})
}
}
拿到res之后,我们分析 res.data.data就是我们要的帖子的列表数据。我们可以先定义一个posts数据,以承载res.data.data。然后在li上 v-for="post in posts"以遍历数据,拿到想要的数据。比如头像,标题什么的。
getData函数定义好以后,要执行,什么时候执行呢?
在页面加载之前去执行,这里我们就要用到一个生命周期钩子函数
beforeMount了
beforeMount(){
// 在页面加载成功之前,显示正在加载的动画
this.isLoading = true;
// 在页面加载之前,发请求获取数据
this.getData();
}
PostList组件创建好以后,要在app.vue中引入,然后在app.vue的components中注入,注入之后,在app.vue的template中渲染。
<template>
<div id="app">
<Header></Header>
<PostList></PostList>
</div>
</template>
时间过滤器
因为服务器返回的最终回复时间,格式是 2019-03-12T03:09:11.934Z
而我们需要的时间格式是 几分钟前, 几小时前,几天前。
所以,我们需要一个过滤器,以转换时间格式。
这个过滤器,因为要全局使用,所以我们在main.js中定义。
Vue.filter('formatDate',function(str){
if(!str) return '';
var date = new Date(str);
var time = new Date().getTime()- date.getTime(); //time是当前时间的毫秒数,跟最终回复时间的毫秒数的差值
if(time<0){
return '';
}else if(time/1000 <30){
return '刚刚'
}else if(time/1000<60){
return parseInt(time/1000)+'秒前'
}else if(time/60000<60){
return parseInt(time/60000)+'分前'
}else if(time/3600000<24){
return parseInt(time/3600000)+'小时前'
}else if(time/86400000<31){
return parseInt(time /86400000)+'天前'
}else if(time/2592000000<12){
return parseInt(time/2592000000)+'月前'
}else{
return parseInt(time/31104000000)+'年前'
}
})
帖子类型判断过滤器
Vue.filter('tabFormatter',function(post){
if(post.good == true){
return '精华'
}else if(post.top == true){
return '置顶'
}else if(post.tab == 'ask'){
return '问答'
}else if(post.tab == 'share'){
return '分享'
}else{
return '招聘'
}
})
<!-- 帖子的分类 -->
//动态绑定class,class分为3中 top,good,还有其他。
<span :class="[{put_good:(post.good == true),put_top:(post.top == true),
'topiclist-tab':(post.good != true&& post.top != true)}]">
<span>
这里用过滤器显示分类的文字
{{post | tabFormatter}}
</span>
</span>
- 创建Article组件
API:https://cnodejs.org/api/v1/topic/:id
我们怎么进入Article组件呢,肯定是点击了某个帖子进入的,点击的时候把帖子的id传递给API,一起发送到服务器,请求到数据,拿到数据后,渲染到页面。
点击帖子,跳转到帖子详情,就需要用到路由了。
<!-- 标题 -->
<router-link :to="{
name:'post_content',
params:{
id:post.id
}
}">
<span>{{post.title}}</span>
</router-link>
// router/index.js里
export default new Router({
routes: [
{
name:'root',
path:'/',
components:{
main:PostList
}
},
{
name:'post_content', //路由的名字
path:'/topic/:id' , //这里的id 就是PostList组件里的帖子标题被点击的时候,传递过来的参数
components: { //path要指向那个组件呢? 需要在components里定义
main:Article //这里用到了Article组件,需要先引入这个组件
}
}
]
})
//定义好router-link之后,是要跳转的,这个跳转之后的页面显示到哪里呢? 由 router-view决定
所以我们在 app.vue里这样写:
<template>
<div id="app">
<Header></Header>
<div class="main">
<router-view name="main"></router-view>
</div>
</div>
</template>
4.创建UserInfo组件
API:https://cnodejs.org/api/v1/user/:loginname
创建之后,把路由写入 index.js
{
name:'user_info',
path:'/user/:loginname',
components:{
main:UserInfo
}
}
在Article组件里,点击头像和用户名的时候,可以跳转到UserInfo组件。所以需要router-link做跳转
<router-link :to="{
name:'user_info',
params:{
loginname:reply.author.loginname
}
}">
<img :src="reply.author.avatar_url" alt="头像">
<span>{{reply.author.loginname}}</span>
</router-link>
- SlideBar组件
侧边栏的API和UserInfo组件的API是一样的。
当我们点击某一个帖子的时候,侧边栏组件需要跟Article组件同时出现,所以,需要在post_content 这个路由里的components里,再加一个SlideBar组件。
{
name:'post_content', //路由的名字
path:'/topic/:id' , //这里的id 就是PostList组件里的帖子标题被点击的时候,传递过来的参数
components: { //path要指向那个组件呢? 需要在components里定义
main:Article, //这里用到了Article组件,需要先引入这个组件
SlideBar:SlideBar
}
}
路由里添加之后,在app.vue里,需要在template里做出渲染。
<template>
<div id="app">
<Header></Header>
<div class="main">
<router-view name="SlideBar"></router-view>
<router-view name="main"></router-view>
</div>
</div>
</template>
还有就是,因为跟UserInfo组件的API是一样的。所以请求数据的时候,需要传一个 loginname参数,这个参数哪里来呢?
因为SlideBar组件是我们点击帖子标题的时候,就要出现的,所以 loginname参数需要我们在点击帖子标题的时候就要传出去,之前点击帖子标题的时候已经传了一个id,所以现在需要loginname参数跟id一起传出去
<!-- 标题 -->
<router-link :to="{
name:'post_content',
params:{
id:post.id,
loginname:post.author.loginname
}
}">
<span>{{post.title}}</span>
</router-link>
SlideBar组件的渲染,需要向https://cnodejs.org/api/v1/user/:loginname发送请求。这个API是需要loginname参数的,而在PostList组件里,点击帖子标题的时候,已经把这个loginname参数传递到 index.js的post_content路由了,所以,我们可以通过this.$route.params.loginname拿到这个参数。
这样就可以发出请求了。
侧边栏做好以后
当我们点击侧边栏里,作者最近话题和最近回复的时候,发现无法跳转。
这是因为,当前页面的路由是:
http://localhost:8080/#/topic/5bd4772a14e994202cd5bdb7&author=alsotang
而我们随便点击一个title后,路由变成了
http://localhost:8080/#/topic/5c4c929a595cbd1e95088b3a&author=zhennann
不难发现,前后只是参数不一样,前边的path是一样的。这个时候点击的时候,走的是跟当前页面是同一个路由。同一个路由内是检测不到变化的。
所以,我们需要在Article组件里,定义一个检测路由变化执行的函数
beforeMount(){
this.isLoading = true;
this.getArticleData();
},
//watch这里用来检测路由的变化,并执行getArticleData()函数
watch:{
'$route'(to,from){
this.getArticleData();
}
}
- Pagination组件
这个分页组件,是显示在首页的,所以,我们可以定义了这个组件之后,可以在PostList组件里边引入,并渲染。
<template>
<div class="pagination">
<!-- 如果changeBtn不传递参数,会把默认的原生的event对象传递进去 -->
<button @click="changeBtn">首页</button>
<button @click="changeBtn">上一页</button>
<button v-if="judge" class="pagebtn">......</button>
<button v-for="btn in pagebtns" :key="btn.id" @click="changeBtn(btn)"
:class="[{currentPage:btn == currentPage},'pagebtn']">
{{btn}}
</button>
<button @click="changeBtn">下一页</button>
</div>
</template>
<script>
import $ from 'jquery'
export default {
name:"Pagination",
data(){
return {
pagebtns:[1,2,3,4,5,"......"],
currentPage:1,
judge:false
}
},
methods:{
changeBtn(page){
// 如果点击的是首页,上一页,下一页这3个按钮的话,传递进来的就不是数字了,而是event对象
if(typeof page != 'number'){
switch(page.target.innerText){
case '上一页':
// 当点击上一页的时候,当前备选按钮的上一个按钮,触发一次点击
$('button.currentPage').prev().click();
break;
case '下一页':
$('button.currentPage').next().click();
break;
case '首页':
this.pagebtns = [1,2,3,4,5,'......'];
this.changeBtn(1);
break;
default:
break;
}
return
}
this.currentPage = page;
//这里的判断目的是,当被点击页码大于4的时候,要在上一页后边出现一个'......'按钮。
if(page > 4){
this.judge = true;
}
if(page === this.pagebtns[4]){
this.pagebtns.shift(); //移出第一个
this.pagebtns.splice(4,0,this.pagebtns[3]+1); //添加最后一个
}else if(page === this.pagebtns[0]&& page !=1){
this.pagebtns.unshift(this.pagebtns[0]-1); //当点击第一个元素的时候,在数组第一个位置加一个元素
this.pagebtns.splice(5,1); //然后数组最后一个元素删除
}
// 子组件给父组件(PostList),传递参数,参数传递过去后,PostList根据传递的页数去渲染页面
this.$emit('handle',this.currentPage);
}
}
}
</script>
<style scoped>
.pagination {
margin-top: 5px;
margin-bottom: 20px;
background-color: white;
padding: 6px 20px;
border-radius: 5px;
/*box-shadow: 0px 2px 9px #888888;*/
border: 1px solid #888888;
}
button {
background-color: #fff;
border: 1px solid #ddd;
color: #778087;
border-radius: 3px;
outline: none;
height: 21px;
cursor: pointer;
padding: 0 2px;
width: 55px;
height: 29px;
}
.pagebtn {
position: relative;
bottom: 1px;
width: 40px;
margin: 0 4px;
}
.currentPage {
color: white;
background-color: #1f1b1b;
}
</style>
需要注意的是,Pagination组件,是放在PostList组件里渲染的,是它的子组件,当我们点击分页按钮,比如第3页的时候,需要把这个页码传递给PostList组件,然后PostList组件根据这个页码作为参数,去重新发送请求,获取到第3页的数据去重新渲染页面。