實(shí)現(xiàn)一個(gè)聊天應(yīng)用

廢話不多說直接上效果圖

技術(shù)棧

前端:vuejs,vue-socket.io,better-scroll

后端:egg,egg-socket.io

數(shù)據(jù)庫:redis

實(shí)現(xiàn)流程

socket的連接

1.vuex中定義socket模塊芯砸,并且定義socket默認(rèn)事件

const state = {
    socketState: false,//連接狀態(tài)
    chat_list: getChatList(),//聊天記錄列表
}
const getters = {
    //消息未讀總數(shù)
    unread_num(state) {
        let count = 0;
        for (var i = 0; i < state.chat_list.length; i++) {
            count += state.chat_list[i].unread_num || 0;
        }
        return count;
    }
}
//socket默認(rèn)事件
const mutations = {
    socket_connect(state) {
        console.log("連接成功");
        state.socketState = true;
    },
    socket_reconnect(state, data) {
        console.log("重新連接" + data);
    },
    socket_reconnecting(state, data) {
        console.log("重新連接中" + data);
        Toast('重新連接中')
    },
    socket_disconnect(state) {
        console.log("斷開連接");
        state.socketState = false;
    },
}

2.客戶端發(fā)起socket連接(初始化socket)

if (!store.state.socket.socketState) {
    Vue.use(
    new VueSocketIO({
        debug: true,
        connection:
        ip + "?token=" + window.localStorage.getItem("token"),
        vuex: {
        store,
        actionPrefix: "socket_",
        mutationPrefix: "socket_"
        }
    })
    );
}

3.服務(wù)端響應(yīng)socket連接

const { app, socket } = ctx;
const token = ctx.request.query.token;
const id = socket.id;
let username = '';
try {
    username = (await ctx.app.jwt.verify(token, ctx.app.config.jwt.secret)).username;
    let data = { id, username };
    //判斷用戶是否在線 如果在線則強(qiáng)制退出
    if (await app.redis.exists(username)) {
        let receive = await app.redis.get(username);
        receive = JSON.parse(receive);
        console.log('已經(jīng)有人在線');
        ctx.socket.to(receive.id).emit('client_logout');
    }
    //把在線信息存入redis中
    await app.redis.set(username, JSON.stringify(data));
} catch (error) {
    //驗(yàn)證失敗直接拒絕socket連接
    console.log(error)
    socket.emit('connect_deny');
    socket.disconnect();
    return;
}

其中,const { app, socket } = ctx;中的socket對(duì)象给梅,是每一個(gè)客戶端連接都會(huì)生成的假丧。對(duì)象里面有socketid,這個(gè)id是每一個(gè)客戶端的唯一標(biāo)識(shí)符(私聊推送需要用到)动羽;<strong>我們可以想象成包帚,客戶端認(rèn)識(shí)用戶的賬號(hào)(username),服務(wù)端認(rèn)識(shí)socketid曹质,因此我們可以把這兩個(gè)標(biāo)識(shí)捆綁在一起婴噩,并且以u(píng)sername為key,value為{ username:'xxxx',socketid:'xxxx' }保存于redis中羽德。</strong>私聊推送的時(shí)候,前端知道推送的目標(biāo)用戶username耸成,后端redis也會(huì)緩存著每一個(gè)登錄用戶的信息跑筝,如此我們就可以通過username給指定用戶推送消息姻报。

實(shí)現(xiàn)邏輯大致為:服務(wù)端驗(yàn)證客戶端socket客戶端的合法性,通過驗(yàn)證的連接會(huì)去redis緩存中讀取key為username的記錄姨夹,如果記錄存在則觸發(fā)斷開socket事件。(單一登錄功能)

消息推送

客戶端推送

send (data, type) {
      this.$socket.emit("chat", data, () => {
        //消息推送成功回調(diào)函數(shù)
        this.chat_item.chat_list.push(data);
        //調(diào)用better事件矾策,讓聊天窗口拉到最底部
        this.$nextTick(() => {
          this.$refs.wrapper.refresh();
          this.$refs.wrapper.scrollToEnd();
        });
      });
    },

由于前端把socket掛載到了vuex中磷账,因此可以通過this.$socket.emit("chat", data, cb)推送消息。其中chat為事件類型贾虽,與服務(wù)端中定義的socket路由對(duì)應(yīng);data為推送的數(shù)據(jù)逃糟,應(yīng)包括目標(biāo)用戶的id;cb為推送成功的回調(diào)函數(shù)蓬豁。<strong>推送成功之后绰咽,this.nextTick中調(diào)用better事件,讓聊天窗口拉到最底部地粪。(better滾動(dòng)條拉到最底是根據(jù)選擇器實(shí)現(xiàn)的取募,而選擇器是依賴于dom元素的,而vuedom更新是異步的蟆技,因此需要在this.$nextTick后再調(diào)用)</strong>

服務(wù)端定義路由

io.route('chat', app.io.controller.chat.index);//接收客戶端emit('chat')事件

服務(wù)端接收和推送

const { ctx } = this
//讀取用戶推送的消息
const message = this.ctx.args[0]
const cb = this.ctx.args[1]
//判斷目標(biāo)用戶是否在線
if (await app.redis.exists(message.receive_id)) {
    let receive = await app.redis.get(message.receive_id)
    receive = JSON.parse(receive)
    //向目標(biāo)用戶發(fā)送消息
    ctx.socket.to(receive.id).emit('client_receive_msg', message)
} else {
    console.log('不在線哦')
    //以message_+username為key維護(hù)一個(gè)隊(duì)列玩敏,隊(duì)列記錄著關(guān)于用戶的離線信息
    //插入數(shù)據(jù)庫
    await app.redis.lpush(
        'message_' + message.receive_id,
        JSON.stringify(message)
    )
}
cb && cb('推送成功啦')

服務(wù)端在chat.js中的index方法中拿到推送的消息斗忌,使用ctx.socket.to(receive.id)推送給目標(biāo)用戶;根據(jù)username去redis中尋找目標(biāo)用戶的socketid(<strong>如果不存在key為username的記錄代表目標(biāo)用戶不在線,把離線信息進(jìn)行緩存起來等目標(biāo)用戶上線統(tǒng)一推送</strong>)

客戶端接收

socket_client_receive_msg(state, data) {
    //判斷當(dāng)前用戶所在的頁面是否是當(dāng)前聊天用戶的頁面 如果是則未讀信息不加1
    let flag = false;
    if (router.currentRoute.name == "SoloChat" && router.currentRoute.query.username == data.send_info.username) { //接受過來的信息時(shí)刻 用戶正在此接收人的聊天窗內(nèi)
        flag = true;
    }
    for (var i = 0; i < state.chat_list.length; i++) {
        if (state.chat_list[i].username == data.send_info.username) { //如果已經(jīng)存在聊天對(duì)話框
            if (!flag) {
                state.chat_list[i].unread_num++;
            }
            state.chat_list[i].chat_list.push(data);
            //置頂
            state.chat_list.unshift(state.chat_list.splice(i, 1)[0]);
        }
    }
},

如上實(shí)現(xiàn)最簡(jiǎn)單版本的即時(shí)通訊旺聚,最后來個(gè)egg的目錄結(jié)構(gòu):


image

完整版git地址

前端

后端

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末飞蹂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子翻屈,更是在濱河造成了極大的恐慌陈哑,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伸眶,死亡現(xiàn)場(chǎng)離奇詭異惊窖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)厘贼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門界酒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嘴秸,你說我怎么就攤上這事毁欣。” “怎么了岳掐?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵凭疮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我串述,道長(zhǎng)执解,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任纲酗,我火速辦了婚禮衰腌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘觅赊。我一直安慰自己右蕊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布吮螺。 她就那樣靜靜地躺著饶囚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪规脸。 梳的紋絲不亂的頭發(fā)上坯约,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音莫鸭,去河邊找鬼闹丐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛被因,可吹牛的內(nèi)容都是我干的卿拴。 我是一名探鬼主播衫仑,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼堕花!你這毒婦竟也來了文狱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤缘挽,失蹤者是張志新(化名)和其女友劉穎瞄崇,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壕曼,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡苏研,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腮郊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摹蘑。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖轧飞,靈堂內(nèi)的尸體忽然破棺而出衅鹿,到底是詐尸還是另有隱情,我是刑警寧澤过咬,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布大渤,位于F島的核電站,受9級(jí)特大地震影響援奢,放射性物質(zhì)發(fā)生泄漏兼犯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一集漾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧砸脊,春花似錦具篇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瞳抓,卻和暖如春埃疫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背孩哑。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工栓霜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人横蜒。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓胳蛮,卻偏偏與公主長(zhǎng)得像销凑,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仅炊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容