VueSocial(vue+express+socket.io實現可聊天可發圖片的社交平臺)

VueSocial

VueSocial something like QQ、weibo、weixin(仿微博、微信的聊天社交平臺)先後端分離的vue+express+socket.io練手項目 前端代碼在BlogPhone下,後端代碼在server下。若是你以爲這個項目還不錯的話,你的star是對我最好的鼓勵。javascript


內容

  1. 預覽
  2. 技術棧
  3. 已實現功能
  4. 待改進
  5. 安裝
  6. 分析總結

預覽

在線demo VueSocial(pc端按了f12後有個小問題,刷新一下就好,resize觸發的問題,待改進)
github地址html

首頁

微信圖片_20181118231148.jpg

登陸

微信圖片_20181118231159.jpg

消息

微信圖片_20181118231142.jpg

聊天

微信圖片_20181118231154.jpg

我的信息

微信圖片_20181118231120.jpg

我的主頁

微信圖片_20181118231131.jpg

分享動態

微信圖片_20181118231054.jpg

更改頭像

微信圖片_20181118231115.jpg

搜索頁面

微信圖片_20181118231126.jpg
微信圖片_20181118231137.jpg


技術棧

  1. vue:前端框架
  2. express:後端框架
  3. socket.io:實現實時消息推送
  4. axios:一個基於 Promise 的 HTTP 庫,發送ajax請求
  5. localStorage:瀏覽器本地存儲
  6. Webpack:模塊打包工具,前端項目構建工具
  7. mongoose:mongodb的框架
  8. better-scroll:一款重點解決移動端(已支持 PC)各類滾動場景需求的插件

已實現功能

  1. 登陸註冊
  2. 圖片分享:上傳本地圖片到雲服務器(我用的是阿里雲的oss,能夠根據本身的狀況修改router/upload.js的代碼)
  3. 頭像修改
  4. 評論:socket.io
  5. 實時消息推送
  6. 查看我的主頁
  7. 實時聊天:socket.io
  8. 首頁下拉刷新:better-scroll
  9. 搜索:搜索用戶與動態、使用localStorage保存歷史搜索記錄

待改進

  1. 同一個用戶多個設備同時登陸時socket.io會出現問題,因此要限制登陸?仍是修改數據庫結構?
  2. 移動端的坑:有的瀏覽器會卡頓、Safari監聽不到輸入框按下搜索鍵(心裏是崩潰的)
  3. resize時better-scroll的小bug
  4. 沒作分頁請求,都是一次性請求所有數據
  5. 評論。。。的名字直接用usename了。。。更名後會有問題。。。。有空在改

安裝

分別兩個文件目錄下安裝依賴npm install,在server文件夾下node app.js,在blogPhone下npm run dev,而後打開localhost:8081就能夠了前端


分析總結

socket.io

引入socket. io

服務端:vue

let serve = app.listen(3001);

  const io = socketio(serve);

  io.on('connection', socket => {

    socket.on('login', (username) => {

                console.log(username+'上線了!');

            });

  }

客戶端:java

在index中引入node

<script src="http://47.107.66.252:3001/socket.io/socket.io.js"></script>

    <script type="text/javascript">

      const socket = io.connect('http://47.107.66.252:3001');

    </script>

總體思路

把須要用到的數據存放在vuex中,在app.vue的updateBySocket()函數中總體監聽服務端emit的事件,根據路由信息判斷數據是要作通常處理仍是交給對話框頁面進行處理ios

核心代碼

服務端(express實現)

let serve = app.listen(3001);
        const io = socketio(serve);
        io.on('connection', socket => {
            const socketId = socket.id;
            //登陸時創建一個username到socketId的映射表
            socket.on('login', (username) => {
                socketHandler.saveUserSocketId(username, socketId)
            });

            socket.on('chat',(data) => {
                Idtoid.findOne({
                    username: data.to_user
                }).then((rs) => {
                //根據用戶名在映射表中找到對應的socketId
                    io.to(rs.socketid).emit('starChat',{
                        from_user:data.from_user,
                        message:data.message,
                        time:data.time,
                        avater:data.avater,
                        _id:data._id
                    })
                })
            })
        })

app.vue

update_chatList:更新聊天列表的mutationgit

...mapMutations([
        'update_chatList'
      ]),
updateBySocket() {
        socket.removeAllListeners();
        socket.on('receiveMsg', (data) => {
          let from_user = data.from_user;
          //若是當前頁面爲與from_user的對話框,則交由對話框頁面處理
          if (this.$route.query.chatwith == from_user) {
            return;
          }
          this.update_chatList(data);
        })
      }

對話框頁面 chat.vue

dataList:當前對話框的聊天記錄github

//發送消息
      sendMessage() {
        if (!this.userInfo._id){
          Toast("請先登陸!");
          return;
        }
        if (this.content == '') {
          return;
        }
        this.axios.post('/chat/chatwith', {//向後端傳輸聊天記錄
          chatWithId: this.tUserInfo._id,
          user_id: this.userInfo._id,
          content: this.content
        }).then((result) => {
          //把本身發送的內容更新到dataList中
          this.dataList.push({
            user_id: {//這個有點亂了,這個是本身的信息
              avater: this.userInfo.avater
            },
            chatWith: {
              _id: this.chatWithId
            },
            addTime: Date.now(),
            content: this.content
          });
          //更新聊天用戶的列表
          this.update_chatList({
            _id: this.tUserInfo._id,//本身的id
            from_user: this.chatWith,//與你聊天的用戶
            message: this.content,//消息內容
            time: Date.now(),//時間);
            me: true,//判別是否是本身發送的
            avater:this.tUserInfo.avater
          });
          //要發送給對方的數據
          let data = {
            from_user: this.userInfo.username,//發送方
            to_user: this.chatWith,//接收方
            message: this.content,//消息內容
            time: Date.now(), //時間);
            avater: this.userInfo.avater,
            _id: this.userInfo._id
          };
          socket.emit('chat', data);
          this.content = '';
        })
      },
      updateBySocket() {
        socket.on('receiveMsg', (data) => {
          //判斷一下是否是當前的對話框
          if (data.from_user == this.chatWith) {
            //把收到的消息保存到聊天記錄中
            this.dataList.push({
              chatWith: {
                _id: this.userInfo._id
              },
              user_id: {//本身的信息
                avater: data.avater
              },
              addTime: data.addTime,
              content: data.message
            });
            this.update_chatList({
              _id: this.tUserInfo._id,
              from_user: this.chatWith,//與你聊天的用戶
              message: data.message,//消息內容
              time: data.addTime,//時間);
              me: true,//判別是否是本身當前頁面
              avater:this.tUserInfo.avater
            });
          }
        })
      }

vuex mutation.js

[types.UPDATE_CHATLIST](state, data) {
    let flag = 0;//判斷新的聊天是否存在於當前的列表中
    state.chatList.forEach((item)=>{
      if (item.chatWith.username == data.from_user) {
        flag = 1;
        if (!data.me) {//判斷當前是否在對話框頁面中
          item.unread++;
          state.unread++;
        }
        //更新
        item.content = data.message;
        item.addTime = data.time;
        //按添加時間排序
        state.chatList.sort((a, b) => {
          return new Date(b.addTime) - new Date(a.addTime)
        });
        //跳出循環
        return false;
      }
    });
    //是新的而且不在對話框頁面
    if (!flag&&!data.me) {
      //添加到第一條
      state.chatList.unshift({
        chatWith: {
          avater: data.avater,
          username: data.from_user,
          _id: data._id
        },
        addTime: data.time,
        content: data.message,
        unread: 1
      });
        state.unread++;
    }else if (!flag&&data.me){//新的而且在對話框頁面,不須要增長unread
      state.chatList.unshift({
        chatWith: {
          avater: data.avater,
          username: data.from_user,
          _id: data._id
        },
        addTime: data.time,
        content: data.message,
      });
    }
  }

總結

socket.io的簡單使用其實並不難,只要掌握好如下幾個函數ajax

socket.emit():向創建該鏈接的客戶端發送消息

socket.on():監聽客戶端發送信息

io.to(socketid).emit():向指定客戶端發送消息

socket.broadcast.emit():向除去創建該鏈接的客戶端的全部客戶端廣播

io.sockets.emit():向全部客戶端廣播

vue

總結一些項目遇到的難點

  1. ajax在生命週期函數created發起,dom操做在生命週期函數mounted中操做,若是須要dom元素徹底掛起後在操做則還須要在$nextTick中操做,例如:
mounted() {
      this.$nextTick(() => {
        this.initImg();
      })
    }
  1. 動態生成(例如經過v-for)的dom元素在mounted中經過ref是獲取不到的,須要在生命週期函數updated中獲取
  2. keepalive後的組件若是須要在跳轉進入時進行操做可經過路由守衛和生命週期函數actived配合使用,如:
beforeRouteEnter(to, from, next) {
      if (from.path == '/upload' ) {
        next(vm => {
          vm._getList = true
        })
      } else {
        next()
      }
    }
activated() {
      this.$nextTick(() => {
        if (this._getList) {
          this.getPyqLists();
        }
      })
    }

圖片上傳及預覽部分

html部分主要是藉助了weui的樣式

<template>
  <div>
    <myheader :title="'發佈動態'">
      <i class="iconfont icon-fanhui1 left" slot="left" @click="goback"></i>
    </myheader>
    <div class="upload">
      <div v-if="userInfo._id">
        <!--圖片上傳-->
        <div class="weui-gallery" id="gallery">
          <span class="weui-gallery__img" id="galleryImg"></span>
          <div class="weui-gallery__opr">
            <a href="javascript:" class="weui-gallery__del">
              <i class="weui-icon-delete weui-icon_gallery-delete"></i>
            </a>
          </div>
        </div>
        <div class="weui-cells weui-cells_form">
          <div class="weui-cell">
            <div class="weui-cell__bd">
              <textarea class="weui-textarea" v-model="content" placeholder="你想說啥" rows="3"></textarea>
            </div>
          </div>
          <div class="weui-cell">
            <div class="weui-cell__bd">
              <div class="weui-uploader">
                <div class="weui-uploader__bd">
                  <ul class="weui-uploader__files" id="uploaderFiles">
                    <li ref="files" class="weui-uploader__file" v-for="(image,index) in images" :key="index"
                        :style="'backgroundImage:url(' + image +' )'"><span @click="deleteimg(index)" class="x">&times;</span></li>
                  </ul>
                  <div v-show="images.length < maxCount" class="weui-uploader__input-box">
                    <input @change="change" id="uploaderInput" class="weui-uploader__input " type="file"
                          multiple accept="image/*">
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <a class="weui-btn weui-btn_primary btn-put" style="margin: 20px " @click.prevent.once="put">發送</a>
      </div>
      <unlogin v-else> </unlogin>
    </div>
  </div>
</template>

重點部分在於

<ul class="weui-uploader__files" id="uploaderFiles">
  <li ref="files" class="weui-uploader__file" v-for="(image,index) in images" :key="index"
      :style="'backgroundImage:url(' + image +' )'"><span @click="deleteimg(index)" class="x">&times;</span></li>
</ul>
<div v-show="!this.$refs.files||this.$refs.files.length < maxCount" class="weui-uploader__input-box">
  <input @change="change" id="uploaderInput" class="weui-uploader__input" type="file"
         multiple accept="image/*">
</div>

經過 @change="change"監聽圖片的上傳,把圖片轉成base64後(後面會講怎麼轉base64)將base64的地址加入到images數組,經過 v-for="(image,index) in images"把要上傳的圖片在頁面中顯示出來,即達到了預覽的效果

js部分
data部分

data() {
      return {
        content: '',//分享動態的文字內容
        maxSize: 10240000 / 2,//圖片的最大大小
        maxCount: 8,//最大數量
        filesArr: [],//保存要上傳圖片的數組
        images: []//轉成base64後的圖片的數組
      }
    }

delete方法

deleteimg(index) {
        this.filesArr.splice(index, 1);
        this.images.splice(index, 1);
      }

change方法

change(e) {
        let files = e.target.files;
        // 若是沒有選中文件,直接返回
        if (files.length === 0) {
          return;
        }
        if (this.images.length + files.length > this.maxCount) {
          Toast('最多隻能上傳' + this.maxCount + '張圖片!');
          return;
        }
        let reader;
        let file;
        let images = this.images;
        for (let i = 0; i < files.length; i++) {
          file = files[i];
          this.filesArr.push(file);
          reader = new FileReader();
          if (file.size > self.maxSize) {
            Toast('圖片太大,不容許上傳!');
            continue;
          }
          reader.onload = (e) => {
            let img = new Image();
            img.onload = function () {
              let canvas = document.createElement('canvas');
              let ctx = canvas.getContext('2d');
              let w = img.width;
              let h = img.height;
              // 設置 canvas 的寬度和高度
              canvas.width = w;
              canvas.height = h;
              ctx.drawImage(img, 0, 0, w, h);
              let base64 = canvas.toDataURL('image/png');
              images.push(base64);
            };
            img.src = e.target.result;
          };
          reader.readAsDataURL(file);
        }
      }

put方法把filesArr中保存的圖片經過axios發送到後端,注意要設置headers信息

put() {
        Indicator.open('發佈中...');
        let self = this;
        let content = this.content;
        let param = new FormData();
        param.append('content', content);
        param.append('username', this.userInfo._id);
        this.filesArr.forEach((file) => {
          param.append('file2', file);
        });
        self.axios.post('/upload/uploadFile', param, {
          headers: {
            "Content-Type": "application/x-www-form-urlencoded"
          }
        }).then(function (result) {
          console.log(result.data);
          self.$router.push({path: '/home'});
          Indicator.close();
          Toast(result.data.msg)
        })
      }

後端經過multer模塊保存傳輸的圖片,再把保存下來的圖片發送到阿里雲oss(這個能夠根據本身的使用狀況變化)

let filePath;
let fileName;

let Storage = multer.diskStorage({
    destination: function (req, file, cb) {//計算圖片存放地址
        cb(null, './public/img');
    },
    filename: function (req, file, cb) {//圖片文件名
        fileName = Date.now() + '_' + parseInt(Math.random() * 1000000) + '.png';
        filePath = './public/img/' + fileName;
        cb(null, fileName)
    }
});
let upload = multer({storage: Storage}).any();//file2表示圖片上傳文件的key

router.post('/uploadFile', function (req, res, next) {
    upload(req, res, function (err) {
        let content = req.body.content || '';
        let username = req.body.username;
        let imgs = [];//要保存到數據庫的圖片地址數組
        if (err) {
            return res.end(err);
        }
        if (req.files.length === 0) {
            new Pyq({
                writer: username,
                content: content
            }).save().then((result) => {
                res.json({
                    result: result,
                    code: '0',
                    msg: '上傳成功'
                });
            })
        }
        /*client.delete('public/img/1.png', function (err) {
            console.log(err)
        });*/
        let i = 0;
        req.files.forEach((item, index) => {
            let filePath = `./public/img/${item.filename}`;
            put(item.filename,filePath,(result)=>{
                imgs.push(result.url);
                i++;
                if (i === req.files.length) {
                //forEach循環是同步的,但上傳圖片是異步的,因此用一個i去標記圖片是否所有上傳成功
                //這時才把數據保存到數據庫
                    new Pyq({
                        content: content,
                        writer: username,
                        pimg: imgs
                    }).save().then(() => {
                        res.json({
                            code: '0',
                            msg: '發佈成功'
                        });
                    })
                }
            })
        })
    })
});

更新中...

若是以爲這個項目對你有幫助,請留下你的star,謝謝(^-^)

相關文章
相關標籤/搜索