前言
專案開始是因為工作需要一個聊天室功能,但是因為某些原因最終選用的是基於xmpp協議的Strophe.js寫的。於是就想用node自己寫一套,本來只是想簡單的寫個聊天頁面,但是寫完了又不滿意,所以不斷的重構(似乎可以理解產品經理為什麼老是改需求了๑乛◡乛๑)。
很多東西,比如mongodb,我也是第一次用,以前只接觸過mysql。所以都是一邊學一邊寫,利用工作之餘的時間,斷斷續續的寫了幾個月,包含了一整套的前後端互動。uI是按照自己的感覺來的,沒有設計天分(話說主題切換到現在還只有一套主題,實在是不好設計啊~),輕噴---。專案還有很多需要優化完善的地方,歡迎大家提到issues(文末有q群,歡迎一起學習交流)。
閒話少說,本文主要講專案的設計流程,以及部分功能實現思路。對專案感興趣的同學請移步原始碼 Vchat — 從頭到腳,擼一個線上聊天的web應用(vue + node + mongodb)。
這是分隔線---------------------------------------深夜碼字,最近真冷
相關地址
專案架構
技術棧
前端主要採用了vue全家桶,沒什麼多說的,腳手架構建專案,vuex狀態管理,vue-router控制路由,axios進行前後端互動。後端是基於node搭的服務,用的是express。我為什麼不用koa呢,純粹是圖方便,因為koa不熟(捂臉)。聊天最重要的當然是通訊,專案用socket.io來進行前後端通訊。
資料庫是mongoDB,主要有使用者、好友、群聊、訊息、表情、號碼池等。
功能概覽
功能設計
登入註冊
Vchat中使用者註冊時,會隨機指定一個code號碼,而這個code號是從預先生成的一個號碼池(號碼池存在mongodb)中取的。初始指定10000001-10001999的號碼段為使用者code, 100001-100999的號碼段為群聊code。使用者可以憑藉code號或者賬號登入。
// 號碼池設計
* code 號碼
* status 1 已使用 0 未使用
* type 1 使用者 2 群聊
* random 隨機數索引,用於隨機查詢某一條
// user表主要欄位
* name 賬號
* pass 密碼
* avatar 頭像
* signature 個性簽名
* nickname 暱稱
* email 郵件
* phone 手機
* sex 性別
* bubble 氣泡
* projectTheme 專案主題
* wallpaper 聊天桌布
* signUpTime 註冊時間
* lastLoginTime 最後一次登入時間
* chatColor 聊天文字顏色
* province 省
* city 市
* town 縣
* conversationsList 會話列表
* cover 封面列表
複製程式碼
註冊時,需要判斷賬號是否已存在,以及隨機取得的code需要在號碼池中標記為已被使用,使用者密碼用md5加密等。
// md5 密碼加密
const md5 = pass => { // 避免多次呼叫MD5報錯
let md5 = crypto.createHash('md5');
return md5.update(pass).digest("hex");
};
複製程式碼
登入同樣需要判斷使用者是否已註冊,以及支援賬號和code兩種方式登入。
const login = (params, callback) => { // 登入
baseList.users
.find({ // mongodb中可以直接用$or表示或關係
$or: [{"name": params.name}, {"code": params.name}]
})
.then(r => {
if (r.length) {
let pass = md5(params.pass);
if (r[0]['pass'] === pass) {
//更新最後一次登入時間 此處直接寫Date.now 會報錯 需要Date.now()!!!;
baseList.users.update({name: params.name}, {lastLoginTime: Date.now()}).then(raw => {
console.log(raw);
});
callback({code: 0, data: {name: r[0].name, photo: r[0].photo}});
} else {
callback({code: -1});
}
} else {
callback({code: -1});
}
})
};
複製程式碼
登入許可權管理
- 後端設定全域性中介軟體,將沒有登入的api請求統一返回status: 0
app.use('/v*', (req, res, next) => {
if (req.session.login) {
next();
} else {
if (req.originalUrl === '/v/user/login' || req.originalUrl === '/v/user/signUp') {
next();
} else {
res.json({
status: 0
});
}
}
});
複製程式碼
- 前端用axios統一設定攔截器
// http response 伺服器響應攔截器,這裡攔截未登入和401錯誤,並重新跳入登頁重新獲取token
instance.interceptors.response.use(
response => { // 攔截未登入
if (response.data.status === 0) {
router.replace('/');
}
return response;
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
// 這裡寫清除token的程式碼
router.replace('/');
}
}
return Promise.reject(error.response.data)
});
複製程式碼
訊息
vchat中,訊息種類包括好友或者加群申請、回覆申請(同意or拒絕)、入群通知、聊天訊息(文字、圖片、表情、檔案)
在實現訊息傳送之前,需要大體的瞭解一些socket.io
的api。詳細api文件可以檢視socket.io
// 所有的訊息請求都是建立在已連線的基礎上的
io.on('connect', onConnect);
// 傳送給當前客戶端
socket.emit('hello', 'can you hear me?', 1, 2, 'abc');
// 傳送給所有客戶端,除了傳送者
socket.broadcast.emit('broadcast', 'hello friends!');
// 傳送給同在 'game' 房間的所有客戶端,除了傳送者
socket.to('game').emit('nice game', "let's play a game");
// 傳送給同在 'game' 房間的所有客戶端,包括髮送者
io.in('game').emit('big-announcement', 'the game will start soon');
複製程式碼
- 加入房間
加入會話列表中的房間,會話列表在好友申請成功或者加群成功時會自動新增。但是你也可以手動移除或新增,移除後將不會再收到被移除會話的訊息(類似於遮蔽)。
// 前端 發起加入房間的請求
this.conversationsList.forEach(v => {
let val = {
name: this.user.name,
time: utils.formatTime(new Date()),
avatar: this.user.photo,
roomid: v.id
};
this.$socket.emit('join', val);
});
// 後端 接受請求後執行加入操作,記錄每個房間加入的成員,以及回信告知指定房間已上線成員
socket.on('join', (val) => {
socket.join(val.roomid, () => {
if (OnlineUser[val.name]) {
return;
}
OnlineUser[val.name] = socket.id;
io.in(val.roomid).emit('joined', OnlineUser); // 包括髮送者
});
});
複製程式碼
- 多房間 同時加入多個聊天房間會出現一個問題,socket可以加入多個房間並給指定房間傳送訊息,但是接受訊息的時候並不會區分房間。換句話說,所有房間的訊息,會一起傳送給客戶端。所以我們需要自己區分哪條訊息是哪個房間的並進行分發。這樣就需要一個房間標識來過濾,Vchat用的是房間id。
mes(r) { // 只有本房間的訊息才展示
if (r.roomid === this.currSation.id) {
this.chatList.push(Object.assign({}, r, {type: 'other'}));
}
}
複製程式碼
- 發訊息
// 前端
send(params, type = 'mess') { // 傳送訊息
if (!this.message && !params) {
return;
}
let val = {
name: this.user.name,
mes: this.message,
time: utils.formatTime(new Date()),
avatar: this.user.photo,
nickname: this.user.nickname,
read: [this.user.name],
roomid: this.currSation.id,
style: 'mess',
userM: this.user.id
};
this.chatList.push(Object.assign({},val,{type: 'mine'})); // 更新檢視
this.$socket.emit('mes', val);
this.message = '';
}
// 後端 接收訊息後儲存到資料庫,並轉發給房間內其他成員,不包括髮送者。
socket.on('mes', (val) => { // 聊天訊息
apiList.saveMessage(val);
socket.to(val.roomid).emit('mes', val);
});
複製程式碼
- 訊息記錄 所有的訊息都會存到mongodb中,當切換房間的時候,會獲取歷史訊息。而處在當前房間時,只會把最新訊息追加到dom中,不會從資料庫獲取。聊天視窗預設只展示最新100條訊息,更多訊息可在聊天記錄中檢視。
// 前端 獲取指定房間的歷史訊息
this.$socket.emit('getHistoryMessages', {roomid: v.id, offset: 1, limit: 100});
// 後端 關聯表、分頁、排序
messages.find({roomid: params.roomid})
.populate({path: 'userM', select: 'signature photo nickname'}) // 關聯使用者基本資訊
.sort({'time': -1})
.skip((params.offset - 1) * params.limit)
.limit(params.limit)
.then(r => {
r.forEach(v => { // 防止使用者修改資料後,資訊未更新
if (v.userM) {
v.nickname = v.userM.nickname;
v.photo = v.userM.photo;
v.signature = v.userM.signature;
}
});
r.reverse();
callback({code: 0, data: r, count: count});
}).catch(err => {
console.log(err);
callback({code: -1});
});
複製程式碼
專案展示
主頁
聊天視窗,可拖拽或縮放,聊天桌布及文字顏色設定。
個人設定
應用空間
相關閱讀
- Mongoose基礎入門
- socket.io文件
- Vchat主題切換實現方案來自於 d2-admin
交流群
qq前端交流群:960807765,歡迎各種技術交流,期待你的加入
公眾號
歡迎關注公眾號 前端發動機,江三瘋的前端二三事,專注技術,也會時常迷糊。希望在未來的前端路上,與你一同成長。
後記
本文主要講了Vchat的整體設計以及一些主要功能的實現,其實寫專案過程中坑還是挺多的,比如mongoose聯表查詢、檔案上傳等等,這裡就不在細說,以後有時間再更新。如果Vchat對你有幫助,記得star一下喲^_^。
- 文章倉庫 ??fe-code