• Vue.js 创建一个 CNODE 社区(完)


    实战

    经过了前面的 Vue 基础的铺垫,现在终于开始进行实战部分了。

    代码链接:GitHub

    预览链接: Git Pages

    图片预览:图片预览

    cnode 社区基本架构

    • Header 头部

    • PostList 列表

    • Article 文章详情页

    • SideBar 侧边栏

    • UserInfo 个人信息

    • Pagination 分页组件


    安装 vue-cli

    安装教程


    项目 Header 组件,主要展示 logo 及 一级菜单。


    PostList

    项目中的 文章列表,其中包括作者、点击量、评论量、文章标题、发表时间等。

    • 数据获取

    通过官方提供的 API : https://cnodejs.org/api/v1/topics 获取帖子列表

    然后通过 Chrome 的一个小插件 yformater 格式化 API 返回的 JSON 文件,分析需要获取的数据。

    JSON格式化

    "id": "5baee8de9545eaf107b9c6f3",   // 文章ID
    
    "author_id": "51f0f267f4963ade0e08f503",    // 作者ID  
    
    "tab": "share",     // 文章分类-分享 表示除了置顶和精华之外的其余分区 ­share 分享 / ask 问答 ­/ job 招聘
    
    "content": ...,     // 文章内容
    
    "title": "Node 地下铁第七期「深圳站」线下沙龙邀约 -  Node.js 新生态",    // 文章标题
    
    "last_reply_at": "2018-10-12T00:40:26.741Z",    // 文章最后回复时间
    
    "good": false,      // 代表是否精华
    
    "top": true,        // 代表是否置顶
    
    "reply_count": 13,  // 回复数量
    
    "visit_count": 1420,    // 浏览数量
    
    "create_at": "2018-09-29T02:52:14.701Z",    // 文章发表时间
    
    "author": {
    
        "loginname": "lellansin",   // 作者名称
    
        "avatar_url": "https://avatars2.githubusercontent.com/u/2081487?v=4&s=120"      // 作者头像
    }
    
    • 引入 axios

    使用 axios 教程

    • 把获取的数据渲染到页面上

    • 运用 filter

    使用 filter 对时间戳进行处理:

    Vue.filter('formatDate', function (str) {
        if (!str) return ''
        var date = new Date(str)
        var time = new Date().getTime() - date.getTime() //现在的时间-传入的时间 = 相差的时间(单位 = 毫秒)
        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 / 31536000000) + '年前'
        }
    })
    

    使用过滤器来判断帖子分类:

    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 '招聘'
      }
    })
    
    • 运用 v-bind

    使用 v-bind 动态绑定样式:

    个人整理博客:v-bind

    <span :class="[
        {put_good:(post.good === true)},
        {put_top:(post.top === true)},
        {'topiclist-tab':(post.good !== true && post.top !== true)}
        ]">
        {{ post | tabFormatter}}
    </span>
    

    Article

    文章详情页,其中包括文章标题、发布日期、正文、评论等内容

    API https://cnodejs.org/api/v1/topic/ + 帖子ID

    • router-link

    主要利用 router-link 从文章列表 PostList 跳转到文章详情页 Article。

    实现思路:

    1.在 PostList.vue 中的每条文章添加 router-link:

    <router-link :to="{name:'post_content',params:{id:post.id}}">
        <span>{{ post.title}}</span>
    </router-link>
    

    2.点击后带着参数 id:post.id 找到 router 中的 index.js 中设定的路径 name:'post_content'

    3.打开 url path:'/topic/:id',渲染组件 Article

    4.Article 中通过 API 获取了单篇文章的数据 this.$axios.get(`https://cnodejs.org/api/v1/topic/${this.$route.params.id}`),然后赋值给了组件中的 data,在页面中渲染出来。

    总得来说就是: router-link -> router/index.js -> router-view


    userInfo

    API https://cnodejs.org/api/v1/user/ + username

    然后就是重复 router-link 的套路

    问题 说说 markdown-github.css

    通过 API 返回了一篇文章的内容 content,content 是由 markdown 语法编写的。

    一开始的处理思路是,引入 assets 目录下的 markdown-github.css ,然后在组件中引入 @import url('../assets/markdown-github.css'); ,接着通过 v-html 在页面中把内容渲染出来,但是发现没有效果,样式没有起到变化。

    然后从遇到同样问题的同学那里得到了解决方法:

    1.在项目中安装: cnpm i markdown-github-css

    2.在 main.js 中引入:import markdown-github-css

    3.在容器div添加类名 markdown-body

    <div v-html="post.content" class="topic_content markdown-body"></div>
    

    展示侧边栏,包括作者信息、最近主题,最近回复等。

    使用 computed 对取得的文章列表做一个筛选,只显示前 5 条:

    computed:{
        topicLimitBy5(){
            // 这里不用 length 判断是因为刚开始渲染的时候 userinfo 是空的,是没有 length 的,所以会报错
            if(this.userinfo.recent_replies){
                return this.userinfo.recent_replies.slice(0,5);
            }
        },
        repliesLimitBy5(){
            if(this.userinfo.recent_replies){
                return this.userinfo.recent_replies.slice(0,5);
            }
        }
    },
    

    问题 提示 [vue-router] missing param for named route "user_info": Expected "name" to be defined

    点击链接后 url 有变化,但是不跳转。

    <!-- SideBar.vue -->
    <li v-for="item in topicLimitBy5">
        <router-link :to="{name:'post_content',params:{id:item.id,name:item.author.loginname}}">
        {{item.title}}
        </router-link>
    </li>
    

    原因:没有对路由进行检测。

    在 vue.js 的文档中,他是这样解释的:

    响应路由参数的变化

    提醒一下,当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
    复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route 对象

    在这个错误中,因为点击的链接路由名称都是 /topic/...,所以相当于复用了组件,没有对路由参数的变化做出响应,因此做出修改:

    // Article.vue
    watch:{
        '$route'(to,from){
        // 通过 id 获取文章详情
          this.getArticleData()
        }
      }
    

    每当 Article 检测到路由发生变化,则执行方法,通过新的文章 id 获取文章数据,渲染新的页面。


    Pagination

    分页器

    • 使用 :class 绑定样式,@click='changebBtn'实现点击不同页码后按钮样式切换,同时通过 $emit 向父组件发出信息,从 API 获取不同页码的数据,渲染在页面上。
    <!-- Pagination.vue -->
    <button v-for="btn in pagebtns" :class="[{currentPage:btn === currentPage},{pagebtn:true}]">
        {{btn}}
    </button>
    
    data() {
        return {
    
            // 先给分页器一个固定的数组
            'pagebtns':[1,2,3,4,5,'...'],
    
            // 给每个按钮一个「坐标」
            currentPage:1,
    
            isEllipsis:false
        };
      },
      methods:{
          changeBtn(page){
              if(typeof page !== 'number'){
                  switch (page.currentTarget.innerText){
                      case '首页':
                        this.pagebtns = [1,2,3,4,5,'...']
                        this.changeBtn(1)
                        break;
                      case '上一页':
                        $('button.currentPage').prev().click()
                        break;
                      case '下一页':
                        $('button.currentPage').next().click()
                        break;
                      default:
                        break;
                  }
                  return
              }
              if(page >4){
                  this.isEllipsis = true
              }else{
                  this.isEllipsis = false
              }
              this.currentPage = page
    
            //   当点击的按钮是第5个时
              if(page === this.pagebtns[4]){
                  this.pagebtns.shift()
                  this.pagebtns.splice(4,0,this.pagebtns[3]+1 )
    
            // 当点击的按钮是第1个时
              }else if(page === this.pagebtns[0] && this.pagebtns[1]>2){
                  this.pagebtns.splice(4,1)
                  this.pagebtns.unshift(this.pagebtns[0]-1)
              }
    
            //   传递数据给父组件 PostList
              this.$emit('handleList',this.currentPage)
          }
      },
    

    完善

    tab 菜单

    可以选择不同的主题进行浏览:

    tab菜单

    1.tab 菜单中每个选项绑定点击事件,点击后根据传入参数的不同获取不同主题的内容:

    <!-- PostList.vue -->
    <span @click="changeTab('')">全部</span>
    <span @click="changeTab('good')">精华</span>
    <span @click="changeTab('share')">分享</span>
    <span @click="changeTab('ask')">问答</span>
    <span @click="changeTab('job')">招聘</span>
    
    // PostList.vue
    changeTab(value){
        this.tab = value
        this.getData()
    }
    

    改变 tab,重新执行方法 getData(),获取不同主题帖子的数据,在页面中渲染出来。

    2.点击了 tab 菜单后,页码回到该主题的第1页

    如果只停留在上一步,则会出现这样的问题:点击 问答 -> 跳转到第6页 -> 再点击 首页 -> 页码显示停留在 首页 的第6页,但是内容实际上是 首页 的第1页

    也就是他的样式没有转换过来。

    解决方法:父组件把 tab 当成参数传递给子组件,子组件 watch 这个 tab,一旦这个 tab 发生变化,则回到这个 tab 对应的主题的第一页:

    <!-- PostList.vue -->
    <Pagination @handleList='renderList' :tab='tab'></Pagination>
    
    // Pagination
    ...
    
    props:[
        'tab'
      ],
    
    ...
    
    watch:{
        tab:function(val,oldVal){
          this.pagebtns = [1,2,3,4,5,'...']
          this.changeBtn(1)
        }
      }
    
    

    增加对文章标题的长度限制

    对文章中同时包含了中英文的字符串的长度进行解析,限制字符串长度

    Vue.filter('postListConversion',function(str,len){
      var result = "";
      var strlen = 0;
      for(var i = 0;i < str.length; i++){
          if(str.charCodeAt(i) > 255){
            strlen += 2; //如果是汉字,则字符串长度加2
          } else {
            strlen++;
          }
          result += str.substr(i,1);
          if(strlen >= len){
              break;
          }
      }
      if(strlen < len){
        return result
      }else{
        return `${result}...`;
      }
    })
    

    媒体查询 响应移动端

    如:

    @media screen and (max- 979px){
      .autherinfo{
        float: none;
        position: absolute;
        bottom: -4px;
        left: 22px;
      }
      ul a{
        max- 96%;
        -o-text-overflow: ellipsis;
        white-space: nowrap;
        display: inline-block;
        vertical-align: middle;
        line-height: 30px;
        overflow: hidden;
        text-overflow: ellipsis;
      }
    }
    

    当设备分辨率宽度小于 979px 时,样式会生效。

    本来打算是另写一个 css 文件,存放在 /assets/css 的文件目录下,然后在 main.js 中通过 import './assets/css/main.css' 引入的,但是查阅资料的时候看到说这样做并不好,到时候需要修改样式会很麻烦,所以就写在了每个组件的 <style> 中。


    Registered / Login

    注册和登录页

    1.注册页:

    使用 localStorage 存储注册用户的用户名和密码,v-model 绑定输入框的 value ,判断 localStorage 里有没有 value:

    有,可以直接登录;无,则注册成功。

    methods:{
          submitInfo(){
              //判断是否存在此用户名
              if (localStorage.getItem(this.username) === null) {
                  this.usernameIsRight = false
    
                  //存入用户名和密码
                  localStorage.setItem(this.username,this.password)
                  this.isWorks = true
                  setTimeout(()=>{
    
                    // 跳转到首页
                    this.$router.push({path:'/login'})
                  },2000)
                }else{
                    this.usernameIsRight = true
                }
          }
      }
    

    2.登录页:

    通过 localStorage 判断输入框 value,匹配则转到首页,不匹配则提示密码错误或者用户未注册。

    methods:{
          submitInfo(){
              //判断是否存在此用户名
              if (localStorage.getItem(this.username) === null) {
                  this.usernameIsRight = true
    
                //   判断用户名和密码是否匹配
                }else if(localStorage.getItem(this.username) !== this.password){
                    this.passwordIsRight = true
                }
    
                // 用户名和密码匹配则带着参数(用户名)跳转到 /user/
                else if(localStorage.getItem(this.username) === this.password){
                    this.usernameIsRight = false
                    this.$router.push({name:'user',params:{name:this.username}})
    
                    // 这里先留个坑,如果不刷新的话,则页面登录状态不会改变,应该是和组件的生命周期有关系,目前暂时没有搞清楚
                    window.location.reload()
                }
          }
      }
    

    3.首页:

    通过 url 参数拿到 用户名:username:this.$route.params.name

    然后把用户名渲染到页面中。

    这里说说没有解决的 bug :

    本来打算使用 eventBus 来传递数据,登录的用户名传给首页,然后首页判断用户名是否存在 localStorage 中,再去渲染,这样感觉流程比较流畅;但是点击提交按钮后页面会跳转到首页,组件的生命周期也会变化,所有没有想到好的接收数据的方法。

    我想我应该试试 vuex。

  • 相关阅读:
    C++中的字符串可以这样换行写
    2020年最受欢迎的 10 门编程语言
    10 本最适合初学者和高级程序员的Python书籍
    手把手教你用 Python + Flask 搭建个人博客
    CentOS VS Ubuntu,谁才是更好的 Linux 版本?
    Python丨为什么你学不好设计模式?
    突破C++瓶颈,在此一举!
    从零上手 GDB 调试,看这个教程就够了~
    搞机器学习需要数学基础吗?
    用 Python 读写 Excel 表格,就是这么的简单粗暴且乏味
  • 原文地址:https://www.cnblogs.com/No-harm/p/9790483.html
Copyright © 2020-2023  润新知