这个百度贴吧的项目是 vue + koa + sequelize 的项目。
由于没有百度贴吧API接口,所以自己写了后端
项目部分截图(GIF)
项目的依赖以及准备工作
前端:vue + vuex + axios + better-scroll +iview + stylus (还有零散的依赖,完成某些小模块的,如 moment 完成项目内时间的需求)
前端依赖项
"dependencies": {
"axios": "^0.19.0",
"better-scroll": "^1.15.2",
"iview": "^3.4.2",
"js-cookie": "^2.2.0",
"jsonwebtoken": "^8.5.1",
"moment": "^2.24.0",
"vue": "^2.5.2",
"vue-photo-preview": "^1.1.3",
"vue-router": "^3.0.1",
"vuex": "^3.1.1"
},
后端:koa + koa-router + mysql2 + sequelize (这些是主要的,还有很多的中间件)
后端依赖项
"dependencies": {
"bcrypt": "^3.0.6",
"env2": "^2.2.2",
"jsonwebtoken": "^8.5.1",
"koa": "^2.7.0",
"koa-body": "^4.1.0",
"koa-bodyparser": "^4.2.1",
"koa-jwt": "^3.5.1",
"koa-router": "^7.4.0",
"koa-session": "^5.12.0",
"koa-static": "^5.0.0",
"koa2-cors": "^2.0.6",
"mysql2": "^1.6.5",
"sequelize": "^5.8.12"
},
前端代码初始化是用来vue-cli3的脚手架。
图标是来自阿里巴巴矢量图标库,找到的图标尽量和百度贴吧一致
前端的UI框架选择了iview(选择有些失误,因为百度贴吧的项目是移动端的,应该采用像vant这类优秀的移动端UI框架会更合适)
准备工作是添加一些路径项 在build目录下的webpakc.base.conf.js文件中的alias选项中添加一些接下来项目中常用的路径
有几点要前面说一下
- 该项目的权限控制只采用了一个token,比较简单,但不是很安全。有兴趣可以看看权限控制
- 由于前后端分离写的代码,所以导致登录认证时设置的cookie 不能在前端取到(应该是域名访问的问题,取出来是undefined),所以只能将token信息传给前端,让前端自己设置cookie(权益之计,不建议采用)
- 没有API,也没写爬虫,所以数据都是手写的,数据量很少,所以一些效果无法实现。
- 最近看过的帖子和最近访问的吧, 这两个模块的数据是采用localStorage存数据,其它的数据均来自后端
- 因为后端写的不是很好,就不讲后端的内容了
项目启动
Tabbar
这是成品效果
采用了vue-router 中 router-link to属性配置的效果
点击的该项会多出两个class属性
- router-link-exact-active (精确匹配规则,路由路径完全一致时会有)
- router-link-active (全包含匹配规则,也就是父路由也会有)
将linkActiveClass: 'active'
添加至router 配置中会将router-link-active 替换成active
这时就也可自定义active的样式了
页面结构
大体分为5个部分,和零散的几个页面,5个部分中其中4个(除info)分别对应4个tabbar选项,将页面划分开来。
info中有三个page,分别是userInfo,baInfo,tieInfo路由与权限控制
路由将5个部分分开定义,最后汇总
因为该项目的登录认证对页面变化很大,所以在路由的beforeEach钩子函数中进行登录状态的判断,对必须登录才能访问的页面进行强制的登录判断,不通过则跳转到登录页。
router.beforeEach((to, from, next) => {
// 一定需要登录的url 没有认证信息 进行登录
if (routerLoginRole.some(route => to.path === route) && !Cookies.get('username')) {
next('/login')
return
}
}
必须登录的页面如下
export const routerLoginRole = [
'/release',
'/message',
'/user',
'/focuslist',
'/fanslist',
'/focusbalist',
'/tielist',
'/setting',
'/userhome',
'/useredit',
'/browsehistory',
'/collection',
'/release',
'/like'
]
同时,不同的页面 tabbar显示和消失状态也不同,将tabbar的显示状态放入vuex 中,在beforeEach钩子函数中统一管理
if (TabbarRoutes.some(route => (to.path.indexOf(route) === 0))) {
store.dispatch('hiddenTabbar')
} else {
store.dispatch('showTabbar')
}
需要隐藏tabbar的页面如下
export const TabbarRoutes = [
'/login',
'/register',
'/search',
'/focuslist',
'/fanslist',
'/focusbalist',
'/tielist',
'/setting',
'/userhome',
'/useredit',
'/userinfo/',
'/tieinfo/',
'/bainfo/',
'/browsehistory',
'/release',
'/collection',
'/like'
]
抽离基本配置以及插件
- iview组件的引入方式采用按需引入,在utils目录下创建一个iview.js 将iview引入的代码进行统一管理。
import Vue from 'vue'
import {
Button,
Input,
Switch,
Progress,
Form,
FormItem,
Icon,
Upload,
Dropdown,
DropdownMenu,
DropdownItem,
Message,
Spin
} from 'iview'
import 'iview/dist/styles/iview.css'
Vue.component('Button', Button)
Vue.component('i-input', Input)
Vue.component('i-switch', Switch)
Vue.component('Progress', Progress)
Vue.component('Form', Form)
Vue.component('FormItem', FormItem)
Vue.component('Icon', Icon)
Vue.component('Upload', Upload)
Vue.component('Dropdown', Dropdown)
Vue.component('DropdownMenu', DropdownMenu)
Vue.component('DropdownItem', DropdownItem)
Vue.component('Spin', Spin)
Vue.prototype.$Message = Message
export default Vue
- filter 全局过滤器,使用moment实现对时间的处理,数字的处理,统一进行管理
-
对axios设置了拦截器,功能有三:
1.设置axios请求的默认路径以及运行其携带cookie信息
2.部分api接口需要登录验证 则需要传给服务器包括token的cookie凭证
3.请求数据时的加载动画
axios.defaults.baseURL = 'http://192.168.1.4:3000/'
axios.defaults.withCredentials = true
axios.defaults.timeout = 5000
axios.interceptors.request.use(
config => {
// 给$http请求设置cookie请求头
store.dispatch('showLoading')
const token = Cookies.get('username')
const isTokenRight = !!(token && JsonWebToken.decode(token))
if (isTokenRight) {
config.headers.common['Authorization'] = 'Bearer ' + token
}
return config
},
error => {
Message.error('网络异常,请稍后再试')
store.dispatch('hiddenLoading')
return Promise.reject(error)
}
)
axios.interceptors.response.use(
response => {
store.dispatch('hiddenLoading')
if (response.data.statusCode !== 200) {
Message.error(response.data.message)
return Promise.reject(response)
}
return response
},
error => {
Message.error('网络异常,请稍后再试')
store.dispatch('hiddenLoading')
return Promise.reject(error)
}
)
部分功能具体实现
首页刷新功能
效果图
分为两个阶段- 首先是监听滚动的事件,滚动值超过某个值(个人设置500),触发事件将tabbar中的首页替换成刷新按钮。
- 点击刷新按钮,将vuex中的refreshData属性变为true。然后在首页中监听这个属性,重新加载数据并且触发better-scroll的滚动事件,让其滚动到最上方。
其中还是有些小问题的:
- tabbar是采用router-link, 会在其事件冒泡阶段阻止事件冒泡,进行其自身的路由跳转事件,从而无法触发自定义的事件,解决办法是在其内部处理掉点击事件(也就是点击事件冒泡到其渲染的元素之前处理掉点击事件,并阻止点击事件的冒泡)
tabbar页面中首页选项的html结构
<router-link tag="span"
class="tabbar-item"
to="/home">
<div class="tabbar-item-icon icon-home"
v-show="!this.$store.getters.isRefresh">
</div>
<div class="tabbar-item-icon icon-home refresh"
v-show="this.$store.getters.isRefresh"
@click.stop.prevent="refresh">
</div>
<span class="tabbar-item-label"
v-show="!this.$store.getters.isRefresh">首页</span>
<span class="tabbar-item-label refresh"
v-show="this.$store.getters.isRefresh"
@click.stop.prevent="refresh">刷新</span>
</router-link>
其触发的刷新方法
refresh () {
if (this.$store.getters.isRefresh) {
this.$store.commit('updateRefreshData', true)
}
},
- 要避免多余的加载数据,所以采用了keep-alive标签,在触发better-scroll的scrollTo方法时,方法无效,具体原因不知,但后面发现自己的better-scroll的版本是基础型的,后面换成全能力滚动后,就可以正常使用了(有知道具体原因的朋友可以聊一下)
实现优质的左右滚动
效果图
html可分为两部分 顶部的三个tab 和 下面的三个页面顶部的tab动画效果为css的transition: all 0.3s ease
页面采用了better-scroll,并在scrollEnd钩子函数中进行相关操作。
顶部的标记滑动事件
romve (index) {
let name = 'message-title-' + index
this.index = index
this.oldX = -index * this.$refs['message-content'].offsetWidth
this.$refs.unline.style.left = this.$refs[name].offsetLeft + 'px'
this.contentScroll.scrollTo(-this.$refs['message-content'].offsetWidth * index, 0, 300)
},
左右滚动的初始化
initScroll () {
this.$nextTick(() => {
if (!this.contentScroll) {
this.contentScroll = new BScroll(this.$refs['message-content'], {
startX: 0,
click: true,
tap: true,
scrollX: true,
scrollY: false,
momentum: false // 不让其生成滚动的滑行动画
})
} else {
this.contentScroll.refresh()
}
this.contentScroll.on('scrollEnd', ({ x }) => {
let width = this.$refs['message-content'].offsetWidth // 获取单个页面的宽度
if (x !== -width && x !== -2 * width && x !== 0) { // 避免自动滚动后继续滚动
if (Math.abs(x - this.oldX) < width / 4) { // 滚动量小于整个页面的1/4时 自动复原
this.contentScroll.scrollTo(-this.index * width, 0, 300)
} else if (this.oldX > x) { // 向左滑动时 自动滚到到右边页面
this.contentScroll.scrollTo(-(++this.index) * width, 0, 300)
} else { // 向右滑动时 自动滚到到左边页面
this.contentScroll.scrollTo(-(--this.index) * width, 0, 300)
}
this.oldX = -this.index * width // 重新计算值
this.romve(this.index) // 使顶部的标记滑动到对应的位置
}
})
this.romve(0) // 顶部的标记初始化到第一个
})
}
页面布局
- html方面
- 主要采用div,span,img,p 等标签
- css方面
- 选择器采用的class选择器
- 布局主要采用 flex 布局
- z-index的层级上是 1-9 ,文字层级低,动画蒙版等层级略高。
- icon图标的定义是采用了background 的形式
background-size: 30px 30px
background-repeat: no-repeat
background-position: center center
background-image: url('../../assets/icon/left.png')
要注意的问题:
- z-index的层级问题,子元素会继承父元素的层级。
- 设置了层级,还是会出现下层元素上浮的问题,有可能是上层元素是透明的,添加背景颜色就可以。
- 应减少使用js的setInterval形成的帧动画,而应该采用css形成的补间动画。
结语
各个页面的小功能写的不多,但很多模块难度问题不大,没有详细写的必要,具体可看源码。
整个项目写下来,也有不少的问题。主要还是整体性上有欠缺,组件化不够彻底。UI风格上还是没办法和百度贴吧一致(有一定的偏差)。
如果你喜欢这篇文章或者可以帮到你,给作者一点鼓励,点个赞在走吧!同时也非常希望看到这篇文章的你能发表一点见解!