使用Vue开发项目(黑马头条项目)–第九天

需要实现的主要功能如下:

资讯列表、标签页切换,文章举报,频道管理、文章详情、阅读记忆,关注功能、点赞功能、评论功能、回复评论、搜索功能、登录功能、个人中心、编辑资料、小智同学 …

今天要实现的功能主要是:小智同学,一些功能优化(主要页面的缓存,滚动条记忆)

1.创建组件

单独创建一个组件(chat.vue)及对应的路由来实现与小智的聊天功能。

1.1setting/chat.vue

<template>
	<div>
		XXX聊天室
	</div>
</template>
复制代码

1.2配置路由

router/index.js中添加路由:

 {
    path: '/setting/chat',
    name: 'settingChat',
    component: () => import('../views/setting/chat.vue')
  },
复制代码

1.3 组件结构及样式

<template>
  <div class="container">
    <!-- 固定导航 -->
    <van-nav-bar fixed left-arrow @click-left="$router.back()" title="小智同学"></van-nav-bar>

    <!-- 聊天主体区域 -->
    <div class="chat-list">
      <!-- 左侧是机器人小智 -->
      <div class="chat-item left">
        <van-image fit="cover" round src="https://img.yzcdn.cn/vant/cat.jpeg" />
        <div class="chat-pao">hi,你好!我是小智</div>
      </div>

      <!-- 右侧是当前用户 -->
      <div class="chat-item right">
        <div class="chat-pao">我是编程小王子</div>
        <van-image  fit="cover" round src="https://img.yzcdn.cn/vant/cat.jpeg" />
      </div>
    </div>

    <!-- 对话区域 -->
    <div class="reply-container van-hairline--top">
      <van-field v-model.trim="word" placeholder="说点什么...">
        <span  @click="send()" slot="button" style="font-size:12px;color:#999">提交</span>
      </van-field>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserChat',
  data () {
    return {
      word: ''
    }
  },
  methods: {
    send () {
      console.log(this.word)
    }
  }
}
</script>

<style lang="less" scoped>
.container {
  height: 100%;
  width: 100%;
  position: absolute;
  left: 0;
  top: 0;
  box-sizing: border-box;
  background:#fafafa;
  padding: 46px 0 50px 0;
  .chat-list {
    height: 100%;
    overflow-y: scroll;
    .chat-item{
      padding: 10px;
      .van-image{
        vertical-align: top;
        width: 40px;
        height: 40px;
      }
      .chat-pao{
        vertical-align: top;
        display: inline-block;
        min-width: 40px;
        max-width: 70%;
        min-height: 40px;
        line-height: 38px;
        border: 0.5px solid #c2d9ea;
        border-radius: 4px;
        position: relative;
        padding: 0 10px;
        background-color: #e0effb;
        word-break: break-all;
        font-size: 14px;
        color: #333;
        &::before{
          content: "";
          width: 10px;
          height: 10px;
          position: absolute;
          top: 12px;
          border-top:0.5px solid #c2d9ea;
          border-right:0.5px solid #c2d9ea;
          background: #e0effb;
        }
      }
    }
  }
}
.chat-item.right{
  text-align: right;
  .chat-pao{
    margin-left: 0;
    margin-right: 15px;
    &::before{
      right: -6px;
      transform: rotate(45deg);
    }
  }
}
.chat-item.left{
  text-align: left;
  .chat-pao{
    margin-left: 15px;
    margin-right: 0;
    &::before{
      left: -5px;
      transform: rotate(-135deg);
    }
  }
}
.reply-container {
  position: fixed;
  left: 0;
  bottom: 0;
  height: 44px;
  width: 100%;
  background: #f5f5f5;
  z-index: 9999;
}
</style>
复制代码

在页面中查看效果:

image.png

1.4 正确显示用户头像

image.png

<!-- 聊天主体区域 -->
    <div class="chat-list">
      <!-- 左侧是机器人小智 -->
      <div
        class="chat-item"
        v-for="(item, idx) in list"
        :key="idx"
        :class="item.name === 'xz' ? 'left' : 'right'"
      >
        <template v-if="item.name === 'xz'">
          <van-image
            fit="cover"
            round
            src="https://img.yzcdn.cn/vant/cat.jpeg"
          />
          <div class="chat-pao">{{ item.msg }}</div>
        </template>
        <!-- template可以将元素包裹起来,它只是逻辑上的容器,渲染时页面不会渲染它,不会产生DOM -->
        <!-- 右侧是当前用户 -->
        <template v-else>
          <div class="chat-pao">{{ item.msg }}</div>
          <van-image
            fit="cover"
            round
            :src="https://juejin.cn/post/$store.state.user.userInfo.photo"
          />
        </template>
        <!-- $store.modules.user.userInfo.photo -->
      </div>
    </div>
复制代码
created(){
// 创建时获取用户信息
    this.$store.dispatch('user/getProfile')
}    
复制代码

2 约定聊天数据并渲染

return {
      list: [ // 对话记录
        { name: 'xz', msg: '你好,我是无所不知的小智同学!', timestamp: Date.now() },
        { name: 'me', msg: '小智同学, 请问你知道你不知道什么吗?', timestamp: Date.now() },
        { name: 'xz', msg: '我有点晕了', timestamp: Date.now() }
      ],
      word: ''
    }
复制代码

name:xz: 表示是机器人。

name:me 表示当前用户。

2.1 用list渲染视图

<template>
  <div class="container">
    <!-- 固定导航 -->
    <van-nav-bar
    fixed
    left-arrow
    @click-left="$router.back()"
    title="小智同学"></van-nav-bar>

   <!-- 聊天主体区域 -->
    <div class="chat-list">
      <!-- 左侧是机器人小智 -->
      <div
        class="chat-item"
        v-for="(item, idx) in list"
        :key="idx"
        :class="item.name === 'xz' ? 'left' : 'right'"
      >
        <template v-if="item.name === 'xz'">
          <van-image
            fit="cover"
            round
            src="https://img.yzcdn.cn/vant/cat.jpeg"
          />
          <div class="chat-pao">{{ item.msg }}</div>
        </template>
        <!-- template可以将元素包裹起来,它只是逻辑上的容器,渲染时页面不会渲染它,不会产生DOM -->
        <!-- 右侧是当前用户 -->
        <template v-else>
          <div class="chat-pao">{{ item.msg }}</div>
          <van-image
            fit="cover"
            round
            :src="https://juejin.cn/post/$store.state.user.userInfo.photo"
          />
        </template>
        <!-- $store.modules.user.userInfo.photo -->
      </div>
    </div>

    <!-- 对话区域 -->
    <div class="reply-container van-hairline--top">
      <van-field v-model.trim="word" placeholder="说点什么...">
        <span  @click="send()" slot="button" style="font-size:12px;color:#999">提交</span>
      </van-field>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserChat',
  data () {
    return {
      list: [ // 对话记录
        { name: 'xz', msg: '你好,我是无所不知的小智同学!', timestamp: Date.now() },
        { name: 'me', msg: '小智同学, 请问你知道你不知道什么吗?', timestamp: Date.now() },
        { name: 'xz', msg: '明天休息!', timestamp: Date.now() },
        { name: 'xz', msg: '我有点晕了', timestamp: Date.now() }
      ],
      word: ''
    }
  },
  methods: {
    send () {
      console.log(this.word)
    }
  }
}
</script>
复制代码

image.png

2.2 socket.io

具体步骤

  • 安装包 npm i socket.io-client
  • 导入使用 import io from 'socket.io-client'
  • 建立连接 const socket = io('地址',{额外传参})
  • 发消息给服务器:socket.emit('自定义消息名', 内容)
  • 连接成功的回调socket.on('connect', function(){})
  • 从服务器收消息:socket.on('自定义消息名', function(msg){})

2.3用socket.io来实现与小智同学的对话功能

image.png
实现功能的具体步骤:

  • 安装包 npm i socket.io-client
  • 在创建组件时
    • 创建websocket连接
    • 监听connect事件,连接成功,模拟小智打个招呼
    • 监听message事件,收到回复时,添加到聊天记录中
  • 点击发送按钮后
    • 接接口要求,封装消息对象
    • 通过emit发出去
    • 清空说话内容

2.4 代码实现

created () {
    // 创建websocket连接
    // const url = 'http://47.114.163.79:3003'
    // const url = 'http://ttapi.research.itcast.cn'
    this.socket = io('http://localhost:8001', {
      query: {
        token: this.$store.state.tokenInfo.token
      },
      transports: ['websocket']
    })
    console.log(this.socket)
    this.socket.on('connect', () => {
      this.list.push({
        name: 'xz',
        msg: '我已经准备好了,你可以来撩我了',
        timestamp: Date.now()
      })
      // console.log('连接成功....', this.socket)
    })
    this.socket.on('message', obj => {
      console.log('从服务器端发过来的数据是', obj)
      this.list.push({
        name: 'xz',
        msg: obj.msg,
        timestamp: obj.timestamp
      })
    })
  },
  methods: {
    hSend () {
      if (this.word === '') {
        return
      }
      // 1. 通过socket.io向服务器发消息
      this.socket.emit('message', {
        msg: this.word,
        timestamp: Date.now()
      })
      // 2. 把消息添加列表中
      this.list.push({
        name: 'me',
        msg: this.word,
        timestamp: Date.now()
      })
      
      // 3. 清空内容
      this.word = ''
    }
  }
复制代码

2.5 让聊天窗口中的滚动条到达底部

在methods中补充一个方法

methods: {
  // 省略其他...
  scrollToBottom () {
    const dom = document.querySelector('.chat-list')
    dom.scrollTop = dom.scrollHeight
	}
}

复制代码

在每次push完成之后(一共有三个地方),调用scrollToBottom

2.6 聊天窗口中的滚动条不能精确滚动到达底部

原因:

vue异步更新。数据变了,视图会跟着变,但是,不是立即跟着变(异步更新)。如果我们希望获取到数据变化之后对应的视图,则可以在this.$nextTick()回调中去写代码。

scrollToBottom () {
  this.$nextTick(() => {
    const dom = this.$refs.refList
    // scrollTop 是dom元素的属性,可以去手动设置
    //   它表示当前dom元素中的滚动条距离元素顶部的距离
    dom.scrollTop = dom.scrollHeight
  })
}
复制代码

3 功能优化 解决从文章详情中返回时,主页要重新加载

现状

进入某个文章的详情页之后,再次退回到主页时,layout/layout.vue这个组件会被再次重新加载一次,频道又回到了推荐频道。

原因

通过vue调试工具来观察效果。

得出结论:路由的跳转,会导致组件的销毁和重建,而组件的销毁和重建,肯定会让主页重新加载

优化思路

通过keep-alive组件来进行对路由组件进行缓存,缓存的意思是:路由跳转时,不去删除组件,而是将组件留在内存中

什么是keep-alive? 它是一个容器,放在某中的组件并不会删除。

代码实现

app.vuelayout.vue中分别在路由出口外面嵌套一层keep-alive即可

<keep-alive :include="['layout']">
    <router-view/>
</keep-alive>
复制代码
<keep-alive :include="['home']">
    <router-view/>
</keep-alive>
复制代码

4 功能优化 文章列表的滚动条位置没有记忆

现状

从主页上的某个频道中改变一下滚动条的位置,然后点击进入文章详情,再次返回,发现滚动条的位置回到了起点。

原因

我们虽然对主页组件进行了缓存,但是这个缓存并不能记录当前文章列表的滚动条的位置。滚动条的位置信息并不属于组件的一部分。

思路

  1. 为每个articleList.vue组件补充数据scrollTop来记录当前的滚动条位置

  2. 当它的状态变成激活时(从文章详情页中返回的),去恢复滚动条位置

实现代码

在articleList.vue中,给根元素(类为scroll-wrapper)添加scroll事件监听

为了达到从文章详情页回到列表时,能还原它的滚动条的位置:

  1. 在组件内监听scroll,实时记录这个位置
  2. 再次回到本组件(从文章详情页回来),去恢复滚动条的位置

如果这个组件被缓存了,则它会多个两个特殊的钩子函数
activated() { } 激活
deactivated () {} 失活

  <div class="scroll-wrapper" ref="refScroll" @scroll="hScroll">
      <van-pull-refresh v-model="isLoadingNew" @refresh="onRefresh">
      // 省略其他...
      </van-pull-refresh>
   </div>
复制代码
// 监听用户滚动条
hScroll () {
   // 保存一下
   console.log(this.$refs.refScroll.scrollTop)
   // 对象普通的属性, 不是响应式的数据
   this.scrollTop = this.$refs.refScroll.scrollTop
}
复制代码

当articleList.vue激活时恢复

activated () {
  console.log('激活 activated...')
  if (this.scrollTop) {
     this.$refs.refScroll.scrollTop = this.scrollTop
  }
},
复制代码

5 打包发布之前的准备工作

需要一些小优化

1.静态资源路径问题
2.路由懒加载
3.vant组件的按需导入
4.去掉代码中的所有console.log

5.1 静态资源路径问题

分析index.html打包后的代码发现, 所有静态资源的目录都是 /开头的,意思就是要访问url根目录下的文件, 但是服务器是vscode根目录为准, 路径找不到
让打包后的路径都以./ 相对路径为准。在vue.config.js – 配置中, 添加一下配置

module.exports = {
  // https://cli.vuejs.org/zh/config/#publicpath
  // 设置打包后访问资源方式,以是相对路径来访问
  // dist目录下的index.html就可以直接双击打开
  publicPath: './'
}
复制代码

5.2 路由懒加载

将路由的引入写成如下格式:

webpackChunkName: "layout"这个name决定了打包之后文件的名字是什么

component: () => import(/* webpackChunkName: "layout" */ '../views/layout/layout.vue'),

复制代码

5.3 vant组件的按需导入

安装依赖
npm i babel-plugin-import -D

在项目根目录创建:babel.config.js

module.exports = {
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ],
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}
复制代码

在main.js中,只引入本项目开发中需要的部分组件即可,由于使用的组件过多,我们新建plugin/vant.js(注意:这其中组件内容缺一不可)

import Vue from 'vue'
import {
  Button,
  Tabbar,
  TabbarItem,
  Form,
  Field,
  NavBar,
  Toast,
  Tabs,
  Tab,
  List,
  Lazyload,
  CellGroup,
  Cell,
  Icon,
  Grid,
  GridItem,
  Popup,
  Row,
  Col,
  Tag,
  Image,
  Divider,
  PullRefresh,
  ActionSheet,
  Loading,
  Search,
  Dialog,
  DatetimePicker
} from 'vant'

Vue.use(Button)
Vue.use(Tabbar)
Vue.use(TabbarItem)
Vue.use(Form)
Vue.use(Field)
Vue.use(NavBar)
Vue.use(Toast)
Vue.use(Tabs)
Vue.use(Tab)
Vue.use(List)
Vue.use(Lazyload)
Vue.use(CellGroup)
Vue.use(Cell)
Vue.use(Icon)
Vue.use(Grid)
Vue.use(GridItem)
Vue.use(Popup)
Vue.use(Row)
Vue.use(Col)
Vue.use(Tag)
Vue.use(Image)
Vue.use(PullRefresh)
Vue.use(ActionSheet)
Vue.use(Divider)
Vue.use(Loading)
Vue.use(Search)
Vue.use(Dialog)
Vue.use(DatetimePicker)

复制代码

在main.js中引入

// 省略其他...

// 删除vant的引入
import './plugin/vant.js'
复制代码

5.4 去掉代码中的所有console.log

安装依赖npm install terser-webpack-plugin -D

修改配置文件vue.config.js(根目录下,它是vue项目的配置文件)

module.exports = {
 
  configureWebpack: (config) => {
      // 在webpack的配置对象 config的 optimization属性的minimizer数组的第一个元素的options中设置....
      // 在打包之后的js中去掉console.log
     config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
  },
  publicPath: './'
}
复制代码

6 打包

用vue-cli内部集成的webpack,把 .vue, .less, .js 等打包成浏览器可直接执行的代码.html,.css,.js。

结果:会在项目根目录下创建 /dist目录,在这个目录下产出打包后的结果

执行命令:npm run build

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享