http://nothing.orion-c.top/
一个无法扯闲篇的网站是没有灵魂的,一个无聊的网站,当然需要一个扯蛋的聊天功能。
初步的架构设计如下:
实现效果如下:
服务端核心代码
我的服务端使用的python,数据存入了sqllite轻量的db文件中,flask_socketio
,结合Blueprint
使用起来还是比较简单的:
初始化
'''
数据库对象创建
'''
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
socketio = SocketIO(app)
'''
注册蓝图
'''
from .devices.websocketCilent import websocketClient
app.register_blueprint(websocketClient)
处理用户消息并广播
@socketio.on('send_message', namespace='/ws')
def send_message(data):
user_sid = data['user_sid']
message = data['message']
if request.headers.getlist("X-Forwarded-For"):
ips = request.headers.getlist("X-Forwarded-For")
ip = ips[0]
elif request.headers.getlist("X-Real-IP"):
ip = request.headers.getlist("X-Real-IP")[0]
else:
ip = request.remote_addr
user_avatar = getUserAvatar(user_sid)
data = Messages(user_sid,message,ip)
db.session.add(data)
db.session.commit()
content = {
'id': data.id,
'message': message,
'avatar': user_avatar,
}
if data.id:
socketio.emit('server_response', json.dumps({'code': 0, 'command': 'push_message', 'content': content}),
namespace='/ws')
这里有个小插曲,既然是聊天,头像少不了,但让用户登录绑定对于我这个无聊的网站来说,操作流程有点长,借助一个免费的随机头像apihttps://api.uomg.com/api/rand.avatar?format=json
,给用户第一次访问的session id进行绑定,并写入数据库,除非用户清理浏览器缓存,不然用户头像就是绑定了。
方法如下:
@socketio.on('add_user', namespace='/ws')
def add_user(data):
user_id = data['user_id']
res = requests.get('https://api.uomg.com/api/rand.avatar?format=json')
resp = res.json()
content = {
'user_sid': user_id,
'imgurl': ''
}
if resp['code'] == 1:
rowData = Users.query.filter_by(user_sid = user_id).first()
if not rowData:
data = Users(user_id, resp['imgurl'])
db.session.add(data)
db.session.commit()
content = {
'user_sid': user_id,
'imgurl': resp['imgurl']
}
return content
客户端核心代码
客户端我是用reactjs写的,socket使用的是socket.io-client
库,结合dva
框架来使用。
// wsApi.js
import io from 'socket.io-client';
let socket = '';
export function initSocket(action) {
const urlHost = window.location.host;
if (socket && socket.connected) {
return
}
socket = io(`ws://${urlHost}/ws`);
socket.on('connect', () => {
console.log('<= 连接服务器成功!');
});
socket.on('disconnect', () => {
console.log('=> 断开服务器成功!');
});
socket.on('server_response', (data) => {
const jsonData = JSON.parse(data)
action({
type: 'server_response',
...jsonData,
});
});
}
export function querySendMessage(payload) {
socket.emit('send_message', payload);
}
export function queryAddUser(payload) {
return new Promise((resolve) => {
socket.emit('add_user', payload, resolve);
});
}
models的核心代码如下:
// wsScoket.js
import {
initSocket,
querySendMessage,
queryAddUser,
} from '@/services/wsApi';
const Model = {
namespace: 'wsSocket',
state: {
avatar: null,
configData: {},
messageData: {id: null, message: null},
},
subscriptions: {
socket({ dispatch }) { // socket相关
return initSocket(data => {
switch (data.command) {
case 'push_message':
dispatch({
type: 'setServerMessage',
payload: data.content,
})
break;
case 'set_config':
dispatch({
type: 'setConfig',
payload: data.content,
})
break;
}
})
},
},
effects: {
*setConfig({ payload }, { put }) {
console.log('payload', payload)
yield put({ type: 'updateState', payload: { configData: payload } });
},
*setServerMessage({ payload }, { put }) {
yield put({ type: 'updateState', payload: { messageData: payload } });
},
*querySendMessage({ payload }, { call }) {
yield call(querySendMessage, payload);
},
*queryAddUser({ payload }, { call, put }) {
const response = yield call(queryAddUser, payload);
yield put({ type: 'updateState', payload: { avatar: response.imgurl } });
},
},
reducers: {
updateState(state, { payload }) {
return { ...state, ...payload };
},
},
};
export default Model;
在用户初次进入网站时,检查本地是否有sid,没有就做一次绑定:
componentWillReceiveProps(nextProps) {
if (nextProps.wsSocket.configData !== this.props.wsSocket.configData) {
if (nextProps.wsSocket.configData.musicUrl !== this.state.musicUrl){
this.setState({
musicUrl: nextProps.wsSocket.configData.musicUrl,
},() => {
if (!localStorage.getItem('user_sid')){
localStorage.setItem('user_sid',nextProps.wsSocket.configData.sid)
const { dispatch } = this.props;
dispatch({
type: 'wsSocket/queryAddUser',
payload: {
user_id: nextProps.wsSocket.configData.sid
}
})
.then(() => {
const { avatar } = this.props.wsSocket
if (avatar){
this.setState({
avatar
})
}
})
}
})
}
}
}
后面发送消息时,都会带上第一次的sid,用以查询发送用户的头像
handleSendMessage = value => {
const user_sid = localStorage.getItem('user_sid')
if (value && user_sid) {
this.querySendMessage(user_sid, value);
}
};
querySendMessage = (user_sid,message) => {
const { dispatch } = this.props;
this.setState({inputMessage: null},()=>{
dispatch({
type: 'wsSocket/querySendMessage',
payload: {
user_sid,
message,
},
})
})
};
消息机制都有了,就差展示了,我使用了rc-bullets库,组件的基本使用按readme配置即可,我这边用antd自定了消息发送框。
this.setState({inputMessage: e.target.value})}
className={styles.input_wrapper}
placeholder="说点啥吧"
onSearch={value => this.handleSendMessage(value)}
onPressEnter={e => this.handleSendMessage(e.target.value)}
enterButton='发送'
size="large"
style={{ minWidth: 300, maxWidth: 500 }}
/>
ok,需求基本就实现了,希望对你有所启发。