Vue全家桶开发的CNode社区单页web应用

为什么要写这篇文章呢?其目的标题写的也很明了,就是为了记录一下我的学习过程。在以后回顾此项目时,也可以更方便地发现此项目中的不足和精华。在此,感谢VNode社区提供的API。

源码在此Vue-CNode
预览地址使劲点我
你也可以扫描下面的二维码预览线上项目:

一、需求分析

要做一个项目之前,我觉得首先要把功能做一个总结,根据需求来写项目,从而做到有的放矢。

所以我根据API写了项目的需求,可见下图:

CNode功能需求分析
CNode功能需求分析

二、技术栈

本项目使用的技术栈就是标准的Vue全家桶,即:

Vue2.0: 构建项目,属于底层框架。
Vue-Router: 通过hash值的变化,从而改变页面结构的路由。
Vuex: Vue官方提供的状态管理模式。
Axios, Vue-Axios: http请求模块。
ES6: 应用于生产环境,普及度较高的新Javascript语法。
Sass: CSS预编译器。
Webpack: 用于打包项目。

三、项目初始化

利用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事件仍然存在。所以现在改用beforeRouteEnterbeforeRouteLeave这两个路由的生命周期钩子,分别实现载入路由时的scroll事件挂载、离开路由时的scroll事件卸载。从而防止主页内容的懒加载一直触发。

11.发布新文章或更新跳转至文章详情页面后,再按后退,怎么实现回到主页?

解决办法:现在初步是使用,路由跳转的时候,先跳到主页,再跳到文章详情页,再按后退时,就会回到主页。

12.如何实现点击评论右侧的回复按钮,添加@信息,并focus输入框?

解决办法:通过vuex来实时记录回复相关的信息,并通过watch输入框的value来判断是否focus。

13.有一个很奇怪的bug:ios下,如果在文章详情页返回主页时,此时的window.scrollY会保持文章详情页时的window.scrollY,如果此值满足异步加载更多数据的条件时,会导致异常加载数据。

解决办法:不得已,只好在beforeRouteEnter钩子中,绑定滚动事件的函数加一个定时器,使其在100ms后绑定事件,所以此时的window.scrollY就会变成之前的值。

七、后记

本人新手一枚,还在苦逼的找工作中T_T。如过您在代码中发现了bug,可以通过评论和我交流,互相学习!有什么好的想法,也可以提出来,一起讨论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • 转载 :OpenDiggawesome-github-vue 是由OpenDigg整理并维护的Vue相关开源项目库...
    果汁密码阅读 22,983评论 8 125
  • 来源:github.com Vue.js开源项目速查表:https://www.ctolib.com/cheats...
    zhangtaiwei阅读 11,516评论 1 159
  • 再见成都,不是别离的再见,而是再次相见的再见,成都,我学习生活了四年的城市,时隔半年,我又回到你的怀抱,你的怀抱是...
    无痛不青春阅读 1,088评论 0 3
  • 谁不想一直保持一副20岁的面容?尤其是女性,脸蛋是女人最大的门面,不管走到哪里,漂亮的女人永远是最受关注的。保持年...
    小蕾拉阅读 509评论 0 0
  • 这张照片2015年正月十五拍摄于东莞。朋友问我怎样拍的?喜欢摄影的人一定尝试一下拍烟花这一课。现在简单几句分享一下...
    深圳男人阅读 239评论 0 2