写这篇文章就是为了记录一下我的学习过程。在以后回顾此项目时,也可以更方便地发现此项目中的不足和精华。在此,感谢VNode社区提供的API。
源码在此:Vue-CNode
预览地址:使劲点我
你也可以扫描下面的二维码预览线上项目:
一、需求分析
要做一个项目之前,我觉得首先要把功能做一个总结,根据需求来写项目,从而做到有的放矢。
所以我根据API写了项目的需求,可见下图:
二、技术栈
本项目使用的技术栈就是标准的Vue全家桶
,即:
Vue2.0: 构建项目,属于底层框架。
Vue-Router: 通过hash值的变化,从而改变页面结构的路由。
Vuex: Vue官方提供的状态管理模式。
Axios, Vue-Axios: http请求模块。
ES6: 新的Javascript语法。
Sass: CSS预编译器。
Webpack: 用于打包项目。
三、功能实现情况
[x] 首页列表
[x] 无限懒加载文章列表
[x] 切换内容主题
[x] 文章详情
[x] 在文章详情页时,可以后退至主页
[x] 回到顶部功能,并添加动画效果
[x] 关于
[x] 用户登录
[x] 用户退出
[x] 个人主页
[x] 我的收藏
[x] 点击用户头像,可以进入该用户的简介页面
[x] 登陆后,可在文章详情页点赞和评论
[x] 登陆后,在主页显示发布主题按钮,可以发布主题
[x] 消息通知,消息设置已读功能
[x] 对自己的文章可以进行编辑更新
[x] 操作成功或失败后的消息提醒
[ ] 增加markdown的编辑器组件和预览器组件
[ ] 评论排序功能
四、项目初始化
利用Vue-cli提供的初始化工具,运行以下代码:
# install vue-cli
$ npm install --global vue-cli
# create a new project using the "webpack" template
$ vue init webpack my-project
# install dependencies and go!
$ cd my-project
$ npm install
$ npm run dev
此时打开http://localhost:8080/就可以访问初始化后的页面了。
五、项目编写
注意:详细内容可以去源码自行查看。
完成初始化之后呢,我们就可以开始编写项目了。
代码分为四块,分别是:components(组件)、vue-router(路由)、vuex(状态管理模式)和common(放置公共样式,字体和通用的功能代码)。
在项目编写之前,受限于要安装依赖,代码如下:
# 安装vuex,vue-router,axios,vue-axios
$ npm install vuex vue-router axios vue-axios --save
// 安装sass依赖
# npm install node-sass sass-loader --save-dev
1.common公用文件
包括样式(style),字体(fonts)还有工具函数(utils, 包括时间格式化还有cookie存取功能)。
2.Components组件
现在暂时一共有14个组件,包括:
AboutMe
Article
ArticleCard
BackBar
BottomBar
Content
Loading
Login
MessageCard
MyCollect
navBar
Notification
Publish
UserDetail
具体内容可以参见后面的项目目录。
3. Vue-Router 路由配置
通过路由,分为一下七个页面:
① 主页
② 文章详情页
③ 用户详情页
④ 用户登录页
⑤ 发布文章页
⑥ 用户收藏页
⑦ 我的通知页
5. Vuex:状态管理模式
状态管理分为六个模块:content
(主页)、article
(文章页)、navbar
(导航栏)、user
(用户详情状态)、login
(用户登录状态)和notification
(通知)。
六、项目目录
.
├── build // webpack设置
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── config // 项目开发和打包设置
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── docs // 静态资源地址
│ ├── index.html
│ └── static
│ ├── css
│ │ └── app.d99bca81a0eef77c7e0d8c70f520707c.css
│ ├── fonts
│ │ ├── iconfont.8553d3c.ttf
│ │ └── iconfont.b29ac85.eot
│ ├── img
│ │ └── iconfont.d4553f2.svg
│ └── js
│ ├── app.cb09e437ae0bec6205b9.js
│ ├── manifest.aa9548ef140031379c30.js
│ └── vendor.f3d0844a66c0c2cabe0b.js
├── src // 项目文件位置
│ ├── App.vue // 组件总入口
│ ├── common // 通用文件
│ │ ├── fonts // 字体
│ │ │ ├── iconfont.eot
│ │ │ ├── iconfont.svg
│ │ │ ├── iconfont.ttf
│ │ │ └── iconfont.woff
│ │ ├── style // 样式
│ │ │ ├── animation.scss // 动画
│ │ │ ├── base.scss // 基本样式
│ │ │ └── icon.scss // iconfont的字体图标样式
│ │ └── utils // 工具函数
│ │ ├── cookie.js // cookie存取和删除
│ │ └── timeFormat.js // 格式化时间函数
│ ├── components // 所有组件
│ │ ├── AboutMe // 关于
│ │ │ └── AboutMe.vue
│ │ ├── Article // 文章详情页
│ │ │ └── Article.vue
│ │ ├── ArticleCard // 文章列表的单个文章卡片
│ │ │ └── ArticleCard.vue
│ │ ├── BackBar // 顶部的返回栏(返回主页和后退)
│ │ │ └── BackBar.vue
│ │ ├── BottomBar // 底部的回复栏(还包含收藏和编辑文件)
│ │ │ └── BottomBar.vue
│ │ ├── Content // 主页
│ │ │ └── Content.vue
│ │ ├── Loading // 正在加载组件
│ │ │ ├── Loading.vue
│ │ │ └── loading.svg
│ │ ├── Login // 登录
│ │ │ └── Login.vue
│ │ ├── MessageCard // 单个通知的详情卡片
│ │ │ └── MessageCard.vue
│ │ ├── MyCollect // 我的收藏页
│ │ │ └── MyCollect.vue
│ │ ├── Notification // 通知页
│ │ │ └── Notification.vue
│ │ ├── Publish // 发布文章和发布更新页
│ │ │ └── Publish.vue
│ │ ├── UserDetail // 用户详情页
│ │ │ └── UserDetail.vue
│ │ └── navBar // 主页的顶部导航栏
│ │ ├── cnodejs_light.svg
│ │ └── navBar.vue
│ ├── main.js // 项目的总入口
│ ├── pic // 和代码无关,README.md中的图片
│ │ ├── CNode�\212\237�\203��\234\200�\202�\210\206�\236\220.png
│ │ └── QR-Code.png
│ ├── router // 路由设置
│ │ └── index.js
│ └── store // 状态管理
│ ├── modules
│ │ ├── article // 文章详情页
│ │ │ ├── article-mutation-types.js
│ │ │ └── article.js
│ │ ├── content // 主页
│ │ │ ├── content-mutation-types.js
│ │ │ └── content.js
│ │ ├── login // 登录页
│ │ │ ├── login-mutation-types.js
│ │ │ └── login.js
│ │ ├── navbar // 主页导航栏
│ │ │ ├── navbar-mutation-types.js
│ │ │ └── navbar.js
│ │ ├── notification // 通知页
│ │ │ ├── notification-mutation-types.js
│ │ │ └── notification.js
│ │ └── user // 用户详情页
│ │ ├── user-mutation-types.js
│ │ └── user.js
│ └── store.js // 状态管理总入口
├── README.md
├── index.html
└── package.json
七、过程中遇到的问题
本项目算是本人第一个完整的手机和pc都兼容,有关于文章展示的项目。整个项目做下来,遇到的Bug很多,自然收获也是很多。总结下来如下:
1.很长的单词会超出边界,导致可视区域变宽。
解决办法:通过word-wrap: break-word;
实现打断效果。
2.第二次进入文章时,会残留(暂未解决)。
解决办法:通过路由的钩子函数beforeRouteEnter,来获取数据,未成功获取数据时,显示Loading页面,加载完成后,显示文章详情页,从而解决这个问题。
3.回到首页时,不能保留原来的状态(暂未解决)。
解决办法:
①此方法为容易固定高度的解决办法。(具体方法:用vuex和vue-router的钩子函数来解决这个问题,即通过scroll事件动态保存此时的scrollTop直,当路由的beforeRouteEnter出发时,恢复其scrollTop的值。)
② 如果没有固定高度,直接通过Vue自带的keep-alive组件,保留组件状态。
4.载入中的动画效果如何做?
解决办法:之前是通过CSS3绘制一个图形,但是后来发现太丑了,就直接用了Iconfont上的svg图,并添加了动画效果。
5.如何实现主页文章列表的懒加载?
解决办法:判断滑动的总高度 - 滑动距离顶部的距离 <= 屏幕的可用高度,也就是以下公式:
document.documentElement.offsetHeight - window.scrollY
<= window.screen.height
这里会出现一个bug,满足条件时,继续滑动,会加载多次。在此可以加入一个状态,表示此时正在加载(详细参见源代码),从而解决此bug。
6.回到顶部的动画怎么做?
解决办法:可以把现在的window.scrollY
分成n
份,然后再设置一个定时器,每隔m
秒,向上滚动一份的高度,当window.scrollY >= 0
时,再终止定时器。(其中的m, n
为任意数,根据情况设定)
7.如何控制正在加载页面的显示?
解决办法:因为加载数据是异步的,可以在加载之前和加载之后,分别更改一个类似于isLoading
(名称自己设定)的状态,从而控制加载页面的显示。
8.如何设置登录功能?
解决办法:因为官方只提供了access-token
,所以可以将此值和一些用户相关的数值,存入document.cookie
中,存入的函数我单独写了一个cookie
的工具函数,代码如下:
/**
* Created by jerryshen on 2017/7/15.
* 用户本地cookie的存取以及清空
* 函数的功能分别是:
* 设置单个,获取所有,获取单个,删除所有,删除单个
*/
export function setCookie (name, value, exdays = 30) {
var time = new Date()
time.setTime(time.getTime() + exdays * 24 * 3600 * 1000)
var expires = 'expires=' + time.toGMTString()
document.cookie = name + '=' + value + ';' + expires
}
export function getAllCookies () {
if (document.cookie === '') {
return {}
}
const cookies = document.cookie.split(';')
const newCookies = {}
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i].trim()
const splitCookie = cookie.split('=')
newCookies[splitCookie[0]] = splitCookie[1]
}
return newCookies
}
export function getCookie (name) {
const cname = name + '='
const cookies = document.cookie.split(';')
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i].trim()
if (cookie.indexOf(cname) === 0) {
return {
success: true,
cookie: {
name,
value: cookie.split(cname)[1]
}
}
} else {
return {
success: false,
cookie: {
name,
value: undefined
}
}
}
}
}
export function deleteAllCookie () {
document.cookie += ';expires=Thu, 01 Jan 1970 00:00:00 GMT'
}
export function deleteCookie (name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`
}
9.如何将API中的时间转换成 => ..年前,..月前,..天前等等,这种类型的格式呢?
解决办法:我自己写了一个格式化的工具函数,代码如下:
export default function timeFormat (date) {
// 获取当前时间和所传时间的Date对象
const nowTime = new Date()
const inDate = new Date(date)
if (nowTime.getYear() - inDate.getYear() > 0) {
// 年份差值 > 0,返回年
return `${nowTime.getFullYear() - inDate.getFullYear()}年前`
} else if (nowTime.getMonth() - inDate.getMonth() > 0) {
// 月份差值 > 0,返回月
return `${nowTime.getMonth() - inDate.getMonth()}个月前`
} else if (nowTime.getDate() - inDate.getDate() > 0) {
// 日期差值 > 0,返回日
return `${nowTime.getDate() - inDate.getDate()}天前`
} else if (nowTime.getHours() - inDate.getHours() > 0) {
// 小时差值 > 0,返回时
return `${nowTime.getHours() - inDate.getHours()}个小时前`
} else if (nowTime.getMinutes() - inDate.getMinutes() > 0) {
// 分钟差值 > 0,返回分钟
return `${nowTime.getMinutes() - inDate.getMinutes()}分钟前`
} else {
// 其他情况,也就是秒数差值 > 0,返回秒钟
return `${nowTime.getSeconds() - inDate.getSeconds()}秒前`
}
}
10.BUG:当进入其他路由时,仍然会触发主页的scroll事件。
解决办法:之前生命周期钩子用的是mounted
,因此进入其他路由时,scroll事件仍然存在。所以现在改用beforeRouteEnter
和beforeRouteLeave
这两个路由的生命周期钩子,分别实现载入路由时的scroll事件挂载、离开路由时的scroll事件卸载。从而防止主页内容的懒加载一直触发。
11.发布新文章或更新跳转至文章详情页面后,再按后退,怎么实现回到主页?
解决办法:现在初步是使用,路由跳转的时候,先跳到主页,再跳到文章详情页,再按后退时,就会回到主页。
12.如何实现点击评论右侧的回复按钮,添加@信息,并focus输入框?
解决办法:通过vuex来实时记录回复相关的信息,并通过watch输入框的value来判断是否focus。
13.有一个很奇怪的bug:ios下,如果在文章详情页返回主页时,此时的window.scrollY
会保持文章详情页时的window.scrollY
,如果此值满足异步加载更多数据的条件时,会导致异常加载数据。
解决办法:不得已,只好在beforeRouteEnter
钩子中,绑定滚动事件的函数加一个定时器,使其在100ms后绑定事件,所以此时的window.scrollY
就会变成之前的值。
14.如何实现全局的消息提醒?
解决办法:我是通过一个和路由同级的组件Messages
,并且创建了一个状态管理的模块messages
,在其中通过state: messages
存放现在显示的的通知数据,利用Messages
组件和vuex
的actions
控制其显示。
八、后记
本人新手一枚,还在苦逼的找工作中T_T。如过您在代码中发现了bug,可以通过评论和我交流,互相学习!有什么好的想法,也可以提出来,一起讨论。