聊天室应用开发实践(二):实现基于 Web 的聊天室
?在上一篇內容中,作者?monkeyHi?已經分享了聲網Agora?信令?SDk?的基本使用,并分析了服務器端?Demo?的接口原理。這一篇,作者將帶大家進行簡單的實踐,實現一個基于?Web?的簡單的聊天室雛形。 歡迎訪問?RTC 開發者社區,獲取?Demo?鏈接,與作者交流。
基于Agora已經發布的?Web?版?Demo,只要簡單配置就已經具備最基本的實時聊天室功能。
獲取 Web 版 demo
我們基于官方的?Demo?來修改出一個聊天室。下載好?Web?Demo?后解壓,目錄如下:
|---Agora_Signaling_Web|---libs // sdk 在這里|---samples // demo在這里|---Agora-Signaling-Tutorial-Web // 聊天室demo用Visual?Studio?Code?打開?samples?目錄下的?Agora-Signaling-Tutorial-Web。
|——Agora-Signaling-Tutorial-Web|—src| ├─assets| │ ├─images| │ └─stylesheets| ├─pages| │ ├─index| │ └─meeting| └─utils|—static|--agora.config.js // 這里配置AppId|--AgoraSig.js // 復制它到 assets 目錄安裝依賴包和運行demo
先簡單說一下 Web 端的 SDK,這個?SDK?包裝程度很高,甚至不需要開發者懂websockt和WebRTC。只要調用對應的功能接口即可。
接下來,我們一起跑起Demo,這個Demo是一個webpack項目,專業的Web開發工程師一定不會覺著陌生。
-
首先,復制 AgoraSig.js到asstes目錄
-
其次,npm install
-
接下來,配置 appid 打開 src\static\agora.config.js ,修改AGORAAPPID 為我們自己的AppId
-
最后,npm start
這時,瀏覽器應該已經彈出一個頁面
隨便輸入一個用戶名,我們就可以進入聊天室了,當然大家可以添加自己的用戶鑒權業務。
我們打開兩個標簽頁,分別用accontA?和accontB的身份加入同一個p2p頻道,嘗試互發消息。
筆者發現,我們這個聊天室并不需要另外運行server端。而前面介紹的server端,從接口上看,其功能更傾向于做系統廣播、系統消息通知。因此,如果你想實現具備云消息備份的功能的聊天軟件,必須要在端上實現存儲和上傳備份。
毫不夸張的說,這個聊天室,只要一個?page?服務就可以跑起來了。
Demo 代碼講解
拿到一個web端項目,我們首先要看的就是package.json。我們可以從中看到項目依賴哪些Package以及項目的啟動腳本。
"scripts": {"test": "jest ./test", // 測試"lint": "eslint .", // eslint格式化當前目錄"format": "eslint . --fix", // eslint fix當前目錄下的代碼格式"dev": "cross-env NODE_ENV=development webpack-dev-server --open", // 這個會啟動 webpack-dev-server 并用瀏覽器打開頁面"start": "npm run dev", // 功能同上一條"build": "cross-env NODE_ENV=production webpack --progress --hide-modules" // 編譯},?
配置文件 static/agora.config.js
只需要在以下兩行代碼中進行配置。
const AGORA_APP_ID = 'appid' // appid const AGORA_CERTIFICATE_ID = '' // 如果開啟了token模式,這里要配置certificate_idSDK文件 static/AgoraSig.js
這里和lib目錄中的SDK文件是一樣的,可以用新的sdk來替換。新下載下來的demo?,還應該復制該文件到?src/assets目錄下。
utils 目錄
utils目錄中封裝了一些工具類。
signalingClient.js
這個文件中,主要對信令SDK做了進一步封裝,將一些Action?轉換為promise,?同時用?Event?替代了Callback。
/*** Wrapper for Agora Signaling SDK* Transfer some action to Promise and use Event instead of Callback*/ import EventEmitter from 'events';// 信令客戶端類 export default class SignalingClient {constructor(appId, appcertificate) {this._appId = appId;this._appcert = appcertificate;// Init signal using signal sdkthis.signal = Signal(appId) // eslint-disable-line // init event emitter for channel/session/callthis.channelEmitter = new EventEmitter();this.sessionEmitter = new EventEmitter();}/*** @description login agora signaling server and init 'session'* 登錄 信令服務,并初始化會話* @description use sessionEmitter to resolve session's callback* @param {String} account* @param {*} token default to be omitted* @returns {Promise}*/login(account, token = '_no_need_token') {this.account = account;return new Promise((resolve, reject) => {this.session = this.signal.login(account, token);// Proxy callback on session to sessionEmitter['onLoginSuccess','onError','onLoginFailed','onLogout','onMessageInstantReceive','onInviteReceived'].map(event => {return (this.session[event] = (...args) => {this.sessionEmitter.emit(event, ...args);});});// Promise.thenthis.sessionEmitter.once('onLoginSuccess', uid => {this._uid = uid;resolve(uid);});// Promise.catchthis.sessionEmitter.once('onLoginFailed', (...args) => {reject(...args);});});}/*** @description logout agora signaling server* 退出信令服務* @returns {Promise}*/logout() {return new Promise((resolve, reject) => {this.session.logout();this.sessionEmitter.once('onLogout', (...args) => {resolve(...args);});});}/*** @description join channel* 加入某個頻道* @description use channelEmitter to resolve channel's callback* @param {String} channel* @returns {Promise}*/join(channel) {this._channel = channel;return new Promise((resolve, reject) => {if (!this.session) {throw {Message: '"session" must be initialized before joining channel'};}this.channel = this.session.channelJoin(channel);// Proxy callback on channel to channelEmitter// 將回調 都代理到 對應channelEmitter['onChannelJoined','onChannelJoinFailed','onChannelLeaved','onChannelUserJoined','onChannelUserLeaved','onChannelUserList','onChannelAttrUpdated','onMessageChannelReceive'].map(event => {return (this.channel[event] = (...args) => {this.channelEmitter.emit(event, ...args);});});// Promise.thenthis.channelEmitter.once('onChannelJoined', (...args) => {resolve(...args);});// Promise.catchthis.channelEmitter.once('onChannelJoinFailed', (...args) => {this.channelEmitter.removeAllListeners()reject(...args);});});}/*** @description leave channel* 離開當前頻道* @returns {Promise}*/leave() {return new Promise((resolve, reject) => {if (this.channel) {this.channel.channelLeave();this.channelEmitter.once('onChannelLeaved', (...args) => {this.channelEmitter.removeAllListeners()resolve(...args);});} else {resolve();}});}/*** @description send p2p message* 發送點對點消息* @description if you want to send an object, use JSON.stringify* @param {String} peerAccount* @param {String} text*/sendMessage(peerAccount, text) {this.session && this.session.messageInstantSend(peerAccount, text);}/*** @description broadcast message in the channel * 發送頻道消息* @description if you want to send an object, use JSON.stringify* 可以通過JSON.Stringify的方式發送object* @param {String} text*/broadcastMessage(text) {this.channel && this.channel.messageChannelSend(text);} }pages
這里面分別是兩個頁面的實現,默認的index頁面和聊天頁面。大家可以在對webpack有一定了解的情況下,修改這兩個頁面。
pages/index.js
大家注意這里,?點擊Join-meeting,我們獲取id為account-name的DOM值,然后將account的值放到的url中。
$('#join-meeting').click(function(e) {// Join btn clickede.preventDefault();var account = $('#account-name').val() || '';if (checkAccount(account)) {// Account has to be a non empty numeric valuewindow.location.href = `meeting.html?account=${account}`;} else {$('#account-name').removeClass('is-invalid').addClass('is-invalid');} });后續聊天頁面的account值通過url中的account參數來傳值。
pages/meeting.js
metting.js中主要定義了client類和聊天相關的類方法。首先,我們來看看文件尾部:
// 檢測獲取appid 并檢測是否為空 const appid = AGORA_APP_ID || '',appcert = AGORA_CERTIFICATE_ID || ''; if (!appid) {alert('App ID missing!'); } //從url獲取account值 ,Browser 模塊事先在util/index.js中定義好的。 let localAccount = Browser.getParameterByName('account'); let signal = new SignalingClient(appid, appcert); // Let channelName = Math.random() * 10000 + ""; // by default call btn is disabled // 信令登陸 signal.login(localAccount).then(() => {// Once logged in, enable the call btnlet client = new Client(signal, localAccount);$('#localAccount').html(localAccount); });接下來,我們應該關注client類,這里筆者只節選部分代碼。相信很多朋友都發現了,demo中聊天頭像都一樣。其實,生成渲染消息的方法里市可以自定義頭像的,默認被寫為固定圖片,大家其實可以根據accont來拼裝頭像鏈接的,當然你得自己做個用戶頭像接口。
buildMsg(msg, me, ts) {let html = '';let timeStr = this.compareByLastMoment(ts);if (timeStr) {html += `<div>${timeStr}</div>`;}let className = me ? 'message right clearfix' : 'message clearfix';html += '<li class="' + className + '">';// 注意看這里 html += '<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/245657/1_copy.jpg">';html +='<div class="bubble">' +Utils.safe_tags_replace(msg) +'<div class="corner"></div>';html += '<span>' + this.parseTwitterDate(ts) + '</span></div></li>';return html;}這里大家要注意跨域的問題。需要自己對接口url做代理來解決跨域安全問題。開發狀態下,直接配置devServer的proxy即可。又有的朋友說啦,我想保留消息記錄,其實在端上保存消息記錄還是比較容易的。注意看onReceiveMessage()。
onReceiveMessage(account, msg, type) {let client = this;var conversations = this.chats.filter(function(item) {return item.account === account;});if (conversations.length === 0) {// No conversation yet, create oneconversations = [{ id: new Date().getTime(), account: account, type: type }];client.chats.splice(0, 0, conversations[0]);client.updateLocalStorage();client.updateChatList();}// 可以看到下面對消息做了簡單處理,然后丟到msgs中for (let i = 0; i < conversations.length; i++) {let conversation = conversations[i];let msgs = this.messages[conversation.id] || [];let msg_item = { ts: new Date(), text: msg, account: account };msgs.push(msg_item);this.updateMessageMap(conversation, msgs);let chatMsgContainer = $('.chat-messages');if (String(conversation.id) === String(this.current_conversation.id)) {this.showMessage(this.current_conversation.id)chatMsgContainer.scrollTop(chatMsgContainer[0].scrollHeight);}}}我們看一下引用它的位置就會發現,無論是p2p消息還是Chanel消息,都會調用這個onReceiveMessage()方法。因此,大家可以通過修改onReceiveMessage實現自己的聊天記錄功能。具體是通過接口存儲到我們自己的服務器,還是借助localStorage,都可以比較好的實現web端的聊天記錄功能。諸如?window.localStorage.setItem('msglog',msgs)。
既然可以暫時保存在localStorage,那么,想導出聊天數據為json,csv也不會麻煩到哪里去。
可能會遇到的問題
1.npm?install?報錯。
解決方法:?更換倉庫地址;?使用yarn?install;?使用vpn
2.可以發送消息,但是收不到消息。
解決方法:檢查asset目錄和static目錄,是否存在AgoraSig.js?,如果不存在,從sdk的lib目錄中復制并重命名為AgoraSig.js
總結
總體來說,基于Agora信令實現聊天室非常簡單,基于demo,自己擴展一些用戶管理業務就可以實現。大家可以集中精力優化交互體驗,美化UI,專注于端上業務。但是,如果想要更多的可控權,希望在server端實現聊天記錄之類的功能。基于信令當前版本做這類功能,需要自己來開發。信令的優勢在于,方便實現一些消息通知的場景。而且對接非常容易,只要簡單封裝即可直接嵌入端上。另外,對于彈幕的實現,大家可以嘗試在端實發送消息是同時推送消息到保存消息的接口。
總結
以上是生活随笔為你收集整理的聊天室应用开发实践(二):实现基于 Web 的聊天室的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux环境变量PATH
- 下一篇: Kaggle:入门赛Tatanic(泰坦