├── .babelrc ├── .eslintrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── chat.gif ├── conf └── log.conf.js ├── favicon.ico ├── index.html ├── package-lock.json ├── package.json ├── server.js ├── src ├── components │ ├── ChatInput.tsx │ ├── ChatRoom.tsx │ ├── Messages.tsx │ └── RoomStatus.tsx ├── container │ └── App.tsx ├── context │ └── index.tsx ├── imgs │ ├── bg.jpg │ └── whaletail.jpg ├── index.tsx └── style │ └── index.scss ├── tsconfig.json ├── webpack.config.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "sourceType": "module" 5 | }, 6 | "extends": ["alloy", "alloy/react", "alloy/typescript"], 7 | "plugins": ["react", "@typescript-eslint"], 8 | "settings": { 9 | "react": { 10 | "version": "detect" 11 | } 12 | }, 13 | "rules": { 14 | "no-console": 1, 15 | "react/prop-types": 0, 16 | "no-undef": 0, 17 | "@typescript-eslint/consistent-type-definitions": ["error", "interface"], 18 | "require-jsdoc": 1, 19 | "max-len": [1, { "code": 200 }], 20 | "object-curly-spacing": ["warn", "always"], 21 | "comma-dangle": [1, "never"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | sourceType: 'module' 5 | }, 6 | extends: ['alloy', 'alloy/react', 'alloy/typescript'], 7 | plugins: ['react', '@typescript-eslint'], 8 | settings: { 9 | react: { 10 | version: 'detect' 11 | } 12 | }, 13 | rules: { 14 | 'no-console': 1, 15 | 'react/prop-types': 0, 16 | 'no-undef': 0, 17 | '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], 18 | 'require-jsdoc': 1, 19 | 'max-len': [1, { code: 200 }], 20 | 'object-curly-spacing': ['warn', 'always'], 21 | 'comma-dangle': [1, 'never'] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.* 3 | dist/ 4 | logs/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "printWidth": 200, 6 | "arrowParens": "always" 7 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | os: 4 | - linux 5 | - osx 6 | 7 | node_js: 8 | - 'lts/*' 9 | - '8' 10 | 11 | script: 12 | - npm install 13 | - npm run lint 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ymyqwe/Websocket-React-Chatroom.svg?branch=master)](https://travis-ci.org/ymyqwe/Websocket-React-Chatroom) 2 | 3 | # Websocket-React-Chatroom 4 | 5 | A Chatroom powered by [`React`](https://reactjs.org/), [`Socket.io`](https://socket.io/) 6 | 7 | ## Features 8 | 9 | - Using [React-Hooks](https://reactjs.org/docs/hooks-intro.html) to manage state 10 | - Using [Socket.io](https://socket.io/) to receive real-time message 11 | 12 | ## Getting Started 13 | 14 | 1. Clone this repo 15 | 16 | 2. Install dependencies 17 | 18 | ```bash 19 | npm install 20 | ``` 21 | 22 | 3. Try out 23 | 24 | ```bash 25 | npm start 26 | ``` 27 | 28 | ## For WINDOWS 29 | 30 | `npm install` might fail. 31 | 32 | View detail with this [pull request](https://github.com/ymyqwe/Websocket-React-Chatroom/pull/9) 33 | 34 | - Try Out 35 | 36 | ```bash 37 | npm run start:CE 38 | ``` 39 | 40 | ## Demo 41 | 42 | [Live Demo](http://chat.yumingyuan.me) 43 | 44 | ## Preview 45 | 46 | ![image](https://github.com/ymyqwe/Websocket-React-Chatroom/raw/master/chat.gif) 47 | 48 | ## TODO 49 | 50 | - [x] [Redux](https://github.com/reduxjs/redux)-like style state manage 51 | - [x] [Log4js](https://github.com/log4js-node/log4js-node) 52 | - [ ] [React-router](https://github.com/ReactTraining/react-router) 53 | - [x] [Typescript](https://github.com/Microsoft/TypeScript) 54 | - [x] [Webpack-v4](https://github.com/webpack/webpack) 55 | - [ ] Test 56 | 57 | ## Changelog 58 | 59 | ### [3.1.0] / 2019-12-30 60 | 61 | - Add [cross-env](https://www.npmjs.com/package/cross-env) package to support WINDOWS 62 | 63 | ### [3.0.0] / 2019-11-19 64 | 65 | - Refactor with Typescript 66 | 67 | ### [2.2.1] / 2019-05-10 68 | 69 | - Upgrade tar to 4.4.8 due to github security alerts 70 | - Upgrade [React](https://reactjs.org) and React-Dom to 16.8.6 71 | 72 | ### [2.2.0] / 2019-03-26 73 | 74 | - Upgrade webpack to v4 75 | - Add logs using log4js 76 | 77 | ### [2.1.0] / 2018-12-11 78 | 79 | - Refactor Message.js with `useRef` and `useEffect` 80 | 81 | ### [2.0.0] / 2018-12-03 82 | 83 | - [React Hooks](https://reactjs.org/docs/hooks-intro.html) 84 | - [Eslint](https://github.com/eslint/eslint) 85 | - [Prettier](https://github.com/prettier/prettier) 86 | - [Travis-CI](https://travis-ci.org/) 87 | 88 | ### [1.0.0] / 2017-02-15 89 | 90 | - React state 91 | - [Socket.io](https://socket.io/) 92 | - [Webpack-v2](https://github.com/webpack/webpack) 93 | - [Expressjs](https://github.com/expressjs/express) 94 | -------------------------------------------------------------------------------- /chat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymyqwe/Websocket-React-Chatroom/d91f163ce84f240e7bf8f3fccfe5787bc77aaf12/chat.gif -------------------------------------------------------------------------------- /conf/log.conf.js: -------------------------------------------------------------------------------- 1 | // documents https://log4js-node.github.io/log4js-node/dateFile.html 2 | module.exports = { 3 | appenders: { 4 | dateFile: { 5 | type: 'dateFile', 6 | filename: 'logs/chat.log', 7 | pattern: 'yyyy-MM-dd', /* roll log in days */ 8 | daysToKeep: 30, /* keep log for 30 days */ 9 | layout: { type: 'json', separator: ',' } 10 | } 11 | }, 12 | categories: { 13 | default: { appenders: ['dateFile'], level: 'trace' } 14 | } 15 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymyqwe/Websocket-React-Chatroom/d91f163ce84f240e7bf8f3fccfe5787bc77aaf12/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 在线聊天室 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-react-chatroom", 3 | "version": "1.0.0", 4 | "description": "A chatroom powered by React and Websocket", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "", 8 | "start": "NODE_ENV=dev node server.js", 9 | "prod": "NODE_ENV=production node server.js", 10 | "start:CE": "cross-env NODE_ENV=dev node server.js", 11 | "prod:CE": "cross-env NODE_ENV=production node server.js", 12 | "build": "webpack -p --config webpack.config.prod.js", 13 | "server": "run-s build prod", 14 | "lint": "eslint src --fix --ext .tsx,.ts" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/ymyqwe/Websocket-React-Chatroom.git" 19 | }, 20 | "keywords": [ 21 | "React", 22 | "Websocket", 23 | "Chatroom" 24 | ], 25 | "author": "ManiaU", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/ymyqwe/Websocket-React-Chatroom/issues" 29 | }, 30 | "homepage": "https://github.com/ymyqwe/Websocket-React-Chatroom#readme", 31 | "dependencies": { 32 | "@babel/core": "^7.0.0", 33 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 34 | "@babel/preset-env": "^7.0.0", 35 | "@babel/preset-react": "^7.0.0", 36 | "@types/react": "^16.9.11", 37 | "@types/react-dom": "^16.9.3", 38 | "awesome-typescript-loader": "^5.2.1", 39 | "babel-core": "7.0.0-bridge.0", 40 | "babel-eslint": "^10.0.1", 41 | "babel-loader": "^8.0.5", 42 | "babel-preset-env": "^1.7.0", 43 | "babel-preset-react": "^6.22.0", 44 | "clean-webpack-plugin": "^2.0.1", 45 | "css-loader": "^3.2.0", 46 | "eslint-config-alloy": "^3.5.0", 47 | "eslint-plugin-react": "^7.11.1", 48 | "express": "^4.14.1", 49 | "file-loader": "^3.0.1", 50 | "html-webpack-plugin": "^3.2.0", 51 | "log4js": "^4.1.0", 52 | "micromatch": "^4.0.2", 53 | "node-sass": "^4.5.0", 54 | "npm-run-all": "^4.1.5", 55 | "react": "^16.8.6", 56 | "react-dom": "^16.8.6", 57 | "sass-loader": "^5.0.1", 58 | "socket.io": "^2.2.0", 59 | "source-map-loader": "^0.2.4", 60 | "style-loader": "^0.13.1", 61 | "typescript": "^3.6.4", 62 | "url-loader": "^1.1.2", 63 | "webpack": "^4.29.6", 64 | "webpack-hot-middleware": "^2.16.1" 65 | }, 66 | "devDependencies": { 67 | "@typescript-eslint/eslint-plugin": "^2.7.0", 68 | "@typescript-eslint/parser": "^2.7.0", 69 | "cross-env": "^6.0.3", 70 | "eslint": "^6.8.0", 71 | "eslint-config-google": "^0.11.0", 72 | "open-browsers": "^1.1.1", 73 | "webpack-cli": "^3.3.0", 74 | "webpack-dev-middleware": "^1.12.2", 75 | "webpack-dev-server": "^3.9.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var path = require('path'); 3 | var express = require('express'); 4 | var app = express(); 5 | var openBrowsers = require('open-browsers'); 6 | 7 | // log 8 | const log4js = require('log4js'); 9 | log4js.addLayout( 10 | 'json', 11 | (config) => 12 | function(logEvent) { 13 | logEvent.data = logEvent.data[0]; 14 | return JSON.stringify(logEvent) + config.separator; 15 | } 16 | ); 17 | const logConf = require('./conf/log.conf'); 18 | log4js.configure(logConf); 19 | const logger = log4js.getLogger('chatLog'); 20 | 21 | // 开发模式热更新 22 | if (process.env.NODE_ENV !== 'production') { 23 | var webpack = require('webpack'); 24 | var config = require('./webpack.config'); 25 | var compiler = webpack(config); 26 | // use in develope mode 27 | app.use( 28 | require('webpack-dev-middleware')(compiler, { 29 | publicPath: config.output.publicPath 30 | }) 31 | ); 32 | app.use(require('webpack-hot-middleware')(compiler)); 33 | 34 | app.get('/', function(req, res) { 35 | const filename = path.join(compiler.outputPath, 'index.html'); 36 | compiler.outputFileSystem.readFile(filename, function(err, result) { 37 | res.set('content-type', 'text/html'); 38 | res.send(result); 39 | res.end(); 40 | }); 41 | }); 42 | } else { 43 | app.get('/', function(req, res) { 44 | res.sendFile(path.join(__dirname, 'dist/index.html')); 45 | }); 46 | } 47 | 48 | var server = require('http').createServer(app); 49 | var io = require('socket.io')(server); 50 | 51 | app.use(express.static(path.join(__dirname, '/'))); 52 | 53 | // 在线用户 54 | var onlineUsers = {}; 55 | // 在线用户人数 56 | var onlineCount = 0; 57 | 58 | io.on('connection', function(socket) { 59 | // 监听客户端的登陆 60 | socket.on('login', function(obj) { 61 | // 用户id设为socketid 62 | socket.id = obj.uid; 63 | 64 | // 如果没有这个用户,那么在线人数+1,将其添加进在线用户 65 | if (!onlineUsers[obj.uid]) { 66 | onlineUsers[obj.uid] = obj.username; 67 | onlineCount++; 68 | } 69 | 70 | // 向客户端发送登陆事件,同时发送在线用户、在线人数以及登陆用户 71 | io.emit('login', { onlineUsers: onlineUsers, onlineCount: onlineCount, user: obj }); 72 | logger.info({ socketId: socket.id, ip: socket.request.connection.remoteAddress, user: obj.username, event: 'in', message: obj.username + '加入了群聊' }); 73 | console.log(obj.username + '加入了群聊'); 74 | }); 75 | 76 | // 监听客户端的断开连接 77 | socket.on('disconnect', function() { 78 | // 如果有这个用户 79 | if (onlineUsers[socket.id]) { 80 | var obj = { uid: socket.id, username: onlineUsers[socket.id] }; 81 | 82 | // 删掉这个用户,在线人数-1 83 | delete onlineUsers[socket.id]; 84 | onlineCount--; 85 | 86 | // 向客户端发送登出事件,同时发送在线用户、在线人数以及登出用户 87 | io.emit('logout', { onlineUsers: onlineUsers, onlineCount: onlineCount, user: obj }); 88 | logger.info({ socketId: socket.id, ip: socket.request.connection.remoteAddress, user: obj.username, event: 'out', message: obj.username + '退出了群聊' }); 89 | console.log(obj.username + '退出了群聊'); 90 | } 91 | }); 92 | 93 | // 监听客户端发送的信息 94 | socket.on('message', function(obj) { 95 | io.emit('message', obj); 96 | logger.info({ socketId: socket.id, ip: socket.request.connection.remoteAddress, user: obj.username, event: 'chat', message: obj.username + '说:' + obj.message }); 97 | console.log(obj.username + '说:' + obj.message); 98 | }); 99 | }); 100 | 101 | server.listen(3300, function(err) { 102 | if (process.env.NODE_ENV !== 'production') { 103 | openBrowsers('http://localhost:3300'); 104 | } 105 | console.log('Listening at *:3300'); 106 | }); 107 | -------------------------------------------------------------------------------- /src/components/ChatInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | interface MyProps { 4 | socket: any; 5 | myId: string; 6 | myName: string; 7 | } 8 | interface MyState { 9 | socket: any; 10 | message: string; 11 | myId: string; 12 | myName: string; 13 | } 14 | export default class ChatInput extends Component { 15 | public constructor(props: MyProps) { 16 | super(props); 17 | this.state = { 18 | socket: props.socket, 19 | message: '', 20 | myId: props.myId, 21 | myName: props.myName 22 | }; 23 | } 24 | 25 | // 监控input变化 26 | public handleChange(e: React.ChangeEvent) { 27 | this.setState({ message: e.target.value }); 28 | } 29 | 30 | // 点击提交或按回车 31 | 32 | public handleClick(e: MouseEvent) { 33 | e.preventDefault(); 34 | this.sendMessage(); 35 | } 36 | 37 | public handleKeyPress(e: KeyboardEvent): boolean { 38 | if (e.key === 'Enter') { 39 | this.sendMessage(); 40 | } 41 | return false; 42 | } 43 | 44 | // 发送聊天信息 45 | public sendMessage(): boolean { 46 | const message = this.state.message; 47 | const socket = this.state.socket; 48 | if (message) { 49 | const obj = { 50 | uid: this.state.myId, 51 | username: this.state.myName, 52 | message: message 53 | }; 54 | socket.emit('message', obj); 55 | this.setState({ message: '' }); 56 | } 57 | return false; 58 | } 59 | public render(): JSX.Element { 60 | return ( 61 |
62 |
63 |
64 | 65 |
66 |
67 | 70 |
71 |
72 |
73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/ChatRoom.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import Messages from './Messages'; 3 | import ChatInput from './ChatInput'; 4 | import { Context } from '../context'; 5 | 6 | // 生成消息id 7 | const generateMsgId = () => { 8 | return String(new Date().getTime()) + Math.floor(Math.random() * 899 + 100); 9 | }; 10 | 11 | // 时间格式 12 | const generateTime = () => { 13 | const hour = new Date().getHours(); 14 | const minute = new Date().getMinutes(); 15 | const hourText = hour === 0 ? '00' : String(hour); 16 | const minuteText = minute < 10 ? '0' + minute : String(minute); 17 | return hourText + ':' + minuteText; 18 | }; 19 | 20 | const ChatRoom = (props) => { 21 | const { state, dispatch } = useContext(Context); 22 | const [init, setInit] = useState(false); 23 | // 更新系统消息 24 | const updateSysMsg = (o, action) => { 25 | const newMsg = { type: 'system', username: o.user.username, uid: o.user.uid, action: action, msgId: generateMsgId(), time: generateTime() }; 26 | dispatch({ 27 | type: 'UPDATE_SYSTEM_MESSAGE', 28 | payload: { 29 | onlineCount: o.onlineCount, 30 | onlineUsers: o.onlineUsers, 31 | message: newMsg 32 | } 33 | }); 34 | }; 35 | 36 | // 发送新消息 37 | const updateMsg = (obj) => { 38 | const newMsg = { type: 'chat', username: obj.username, uid: obj.uid, action: obj.message, msgId: generateMsgId(), time: generateTime() }; 39 | dispatch({ 40 | type: 'UPDATE_USER_MESSAGE', 41 | payload: { 42 | message: newMsg 43 | } 44 | }); 45 | }; 46 | // 监听消息发送 47 | const ready = () => { 48 | const { socket } = props; 49 | setInit(true); 50 | socket.on('login', (o) => { 51 | updateSysMsg(o, 'login'); 52 | }); 53 | socket.on('logout', (o) => { 54 | updateSysMsg(o, 'logout'); 55 | }); 56 | socket.on('message', (obj) => { 57 | updateMsg(obj); 58 | }); 59 | }; 60 | if (!init) { 61 | ready(); 62 | } 63 | const renderUserList = () => { 64 | const users = state.onlineUsers; 65 | let userhtml = ''; 66 | let separator = ''; 67 | for (const key in users) { 68 | if (users.key) { 69 | userhtml += separator + users[key]; 70 | separator = '、'; 71 | } 72 | } 73 | return userhtml; 74 | }; 75 | return ( 76 |
77 |
78 |
79 |
鱼头的聊天室 | {props.username}
80 |
81 | 82 |
83 |
84 |
85 |
86 | 在线人数: {state.onlineCount}, 在线列表: {renderUserList()} 87 |
88 |
89 | 90 | 91 |
92 |
93 | ); 94 | }; 95 | export default ChatRoom; 96 | -------------------------------------------------------------------------------- /src/components/Messages.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useRef } from 'react'; 2 | import { Context } from '../context'; 3 | 4 | const Message = (props) => { 5 | if (props.msgType === 'system') { 6 | return ( 7 |
8 | {props.msgUser} {props.action === 'login' ? '进入了聊天室' : '离开了聊天室'}  {props.time} 9 |
10 | ); 11 | } else { 12 | return ( 13 |
14 |

15 | {props.msgUser} {props.time} 16 |

17 |
{props.action}
18 |
19 | ); 20 | } 21 | }; 22 | 23 | const Messages = (props) => { 24 | const messageList = useRef(null); 25 | // 使用context中的状态,而不是props传值 26 | const { state } = useContext(Context); 27 | 28 | // 使用useFffect取代componentDidUpdate 29 | useEffect(() => { 30 | window.scrollTo(0, messageList.current.clientHeight + 50); 31 | }); 32 | const { uid, messages } = state; 33 | return ( 34 |
35 | {messages.map((message) => ( 36 | 37 | ))} 38 |
39 | ); 40 | }; 41 | 42 | export default Messages; 43 | -------------------------------------------------------------------------------- /src/components/RoomStatus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const RoomStatus = (props) => { 4 | return ( 5 |
6 | 在线人数: {props.onlineCount}, 在线列表: {props.userhtml} 7 |
8 | ); 9 | }; 10 | export default RoomStatus; 11 | -------------------------------------------------------------------------------- /src/container/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react'; 2 | import ChatRoom from '../components/ChatRoom'; 3 | import '../style/index.scss'; 4 | import { Context } from '../context/index'; 5 | 6 | const userState = (username) => { 7 | const [user, setUsername] = useState(username); 8 | return [user, setUsername]; 9 | }; 10 | 11 | const generateUid = () => { 12 | return String(new Date().getTime()) + Math.floor(Math.random() * 999 + 1); 13 | }; 14 | 15 | const App = (props) => { 16 | // 获取context中的数据 17 | const { state, dispatch } = useContext(Context); 18 | // 输入输出用户名 19 | const [user, setUsername] = userState(null); 20 | const handleLogin = () => { 21 | const uid = generateUid(); 22 | const username = user ? user : `游客${uid}`; 23 | dispatch({ type: 'login', payload: { uid, username } }); 24 | state.socket.emit('login', { uid, username }); 25 | }; 26 | const handleKeyPress = (e) => { 27 | if (e.key === 'Enter') { 28 | handleLogin(); 29 | } 30 | return false; 31 | }; 32 | return ( 33 |
34 | {state.uid ? ( 35 | // 已登录 36 | 37 | ) : ( 38 | // 登录界面 39 |
40 |

登 陆

41 |
42 | setUsername(e.target.value)} onKeyPress={handleKeyPress} /> 43 |
44 |
45 | 48 |
49 |
50 | )} 51 |
52 | ); 53 | }; 54 | export default App; 55 | -------------------------------------------------------------------------------- /src/context/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useReducer } from 'react'; 2 | import io from 'socket.io-client'; 3 | 4 | const Context = createContext(null); 5 | 6 | interface StateType { 7 | username: string; 8 | uid: string; 9 | socket: any; 10 | messages: []; 11 | onlineUsers: { [key: string]: string }; 12 | onlineCount: number; 13 | userhtml: string; 14 | } 15 | 16 | const initValue: StateType = { 17 | username: '', 18 | uid: '', 19 | socket: io(), 20 | messages: [], 21 | onlineUsers: {}, 22 | onlineCount: 0, 23 | userhtml: '' 24 | }; 25 | interface Login { 26 | uid: string; 27 | username: string; 28 | } 29 | 30 | const login = (info: Login): object => { 31 | return info; 32 | }; 33 | 34 | interface SystemMessage { 35 | onlineCount: number; 36 | onlineUsers: object; 37 | message: Message; 38 | } 39 | 40 | const systemMessage = (sysMsg: SystemMessage, state): object => { 41 | return { 42 | messages: state.messages.concat(sysMsg.message), 43 | onlineUsers: sysMsg.onlineUsers, 44 | onlineCount: sysMsg.onlineCount 45 | }; 46 | }; 47 | 48 | interface Message { 49 | type: number; 50 | username: string; 51 | uid: string; 52 | action: string; 53 | msgId: string; 54 | time: string; 55 | } 56 | interface UserMessage { 57 | message: Message; 58 | } 59 | const userMessage = (usrMsg: UserMessage, state): object => { 60 | return { 61 | messages: state.messages.concat(usrMsg.message) 62 | }; 63 | }; 64 | interface Payload extends UserMessage, SystemMessage, Login {} 65 | interface ActionType { 66 | type: string; 67 | payload: Payload; 68 | } 69 | 70 | const reducer = (state: StateType, action: ActionType): StateType => { 71 | // console.log(state, action); 72 | switch (action.type) { 73 | case 'login': 74 | return { ...state, ...login(action.payload) }; 75 | case 'UPDATE_SYSTEM_MESSAGE': 76 | return { ...state, ...systemMessage(action.payload, state) }; 77 | case 'UPDATE_USER_MESSAGE': 78 | return { ...state, ...userMessage(action.payload, state) }; 79 | default: 80 | return state; 81 | } 82 | }; 83 | 84 | const ContextProvider = (props) => { 85 | const [state, dispatch] = useReducer(reducer, initValue); 86 | return {props.children}; 87 | }; 88 | 89 | const ContextConsumer = Context.Consumer; 90 | 91 | export { Context, ContextProvider, ContextConsumer }; 92 | -------------------------------------------------------------------------------- /src/imgs/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymyqwe/Websocket-React-Chatroom/d91f163ce84f240e7bf8f3fccfe5787bc77aaf12/src/imgs/bg.jpg -------------------------------------------------------------------------------- /src/imgs/whaletail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymyqwe/Websocket-React-Chatroom/d91f163ce84f240e7bf8f3fccfe5787bc77aaf12/src/imgs/whaletail.jpg -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './container/App'; 4 | import { ContextProvider } from './context/index'; 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('app') 10 | ); 11 | -------------------------------------------------------------------------------- /src/style/index.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ''; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | $breakpoint-tablet: 768px; 9 | * { 10 | margin: 0; 11 | padding: 0; 12 | border: none; 13 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 14 | box-sizing: border-box; 15 | } 16 | body { 17 | background: url('../imgs/whaletail.jpg') center center; 18 | background-size: cover; 19 | background-attachment: fixed; 20 | font-size: 15px; 21 | } 22 | @media (max-width: $breakpoint-tablet) { 23 | body { 24 | background: url('../imgs/bg.jpg') center center; 25 | background-size: cover; 26 | background-attachment: fixed; 27 | } 28 | } 29 | .login-box { 30 | position: absolute; 31 | display: block; 32 | top: 50%; 33 | left: 50%; 34 | width: 100%; 35 | text-align: center; 36 | height: 200px; 37 | width: 300px; 38 | margin-left: -150px; 39 | margin-top: -100px; 40 | background: rgba(255, 255, 255, 0.8); 41 | border-radius: 10px; 42 | padding: 20px; 43 | color: rgb(51, 51, 51); 44 | input { 45 | height: 30px; 46 | font-size: 12px; 47 | border: none; 48 | border-radius: 2px; 49 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08); 50 | transition: box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1); 51 | margin-top: 30px; 52 | padding: 10px; 53 | outline: none; 54 | &:hover, 55 | &:focus { 56 | box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08); 57 | } 58 | } 59 | button { 60 | border-radius: 2px; 61 | color: #757575; 62 | cursor: default; 63 | font-family: arial, sans-serif; 64 | font-size: 13px; 65 | font-weight: bold; 66 | margin: 11px 4px; 67 | min-width: 54px; 68 | padding: 0 16px; 69 | text-align: center; 70 | height: 27px; 71 | line-height: 18px; 72 | &:hover { 73 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f8f8f8), to(#f1f1f1)); 74 | background-image: -webkit-linear-gradient(top, #f8f8f8, #f1f1f1); 75 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); 76 | background-color: #f8f8f8; 77 | background-image: linear-gradient(top, #f8f8f8, #f1f1f1); 78 | background-image: -o-linear-gradient(top, #f8f8f8, #f1f1f1); 79 | border: 1px solid #c6c6c6; 80 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); 81 | color: #222; 82 | } 83 | } 84 | } 85 | .chat-room { 86 | color: #fff; 87 | max-width: 1200px; 88 | margin: 0 auto; 89 | .welcome { 90 | @include clearfix; 91 | width: 100%; 92 | color: rgba(0, 0, 0, 0.870588); 93 | background: #fff; 94 | padding: 10px 0; 95 | position: fixed; 96 | left: 0; 97 | right: 0; 98 | top: 0; 99 | line-height: 24px; 100 | .room-action { 101 | max-width: 1200px; 102 | margin: 0 auto; 103 | } 104 | .room-name { 105 | float: left; 106 | margin-left: 10px; 107 | } 108 | } 109 | } 110 | .room-status { 111 | margin-top: 50px; 112 | text-align: center; 113 | font-size: 13px; 114 | } 115 | .messages { 116 | padding: 5px 10px 60px; 117 | font-size: 15px; 118 | .one-message.system-message { 119 | font-size: 13px; 120 | color: #e0e0e0; 121 | flex-direction: row; 122 | justify-content: center; 123 | align-items: baseline; 124 | } 125 | .one-message { 126 | padding-bottom: 10px; 127 | display: flex; 128 | flex-direction: column; 129 | justify-content: flex-start; 130 | align-items: flex-start; 131 | p { 132 | margin-bottom: 5px; 133 | } 134 | .time { 135 | color: #e0e0e0; 136 | } 137 | .message-content { 138 | color: rgba(0, 0, 0, 0.870588); 139 | padding: 5px 10px; 140 | margin: 0; 141 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.117647); 142 | word-break: break-all; 143 | border-radius: 10px; 144 | display: inline-block; 145 | background-color: #f1f1f1; 146 | } 147 | } 148 | .one-message.me { 149 | flex-direction: column; 150 | justify-content: flex-start; 151 | align-items: flex-end; 152 | .message-content { 153 | background: #a2e563; 154 | } 155 | } 156 | } 157 | .bottom-area { 158 | position: fixed; 159 | bottom: 0; 160 | left: 0; 161 | right: 0; 162 | width: 100%; 163 | background: #fff; 164 | padding: 10px 0; 165 | font-size: 14px; 166 | .input-box { 167 | display: flex; 168 | max-width: 1200px; 169 | margin: 0 auto; 170 | .input { 171 | flex: 1; 172 | margin: 0 10px; 173 | input { 174 | width: 100%; 175 | outline: none; 176 | color: rgba(0, 0, 0, 0.870588); 177 | font-size: 100%; 178 | line-height: 24px; 179 | border-bottom-width: 2px; 180 | transition: border-color 0.2s ease; 181 | -webkit-transition: border-color 0.2s ease; 182 | background: 0; 183 | border: 0; 184 | border-bottom: 2px solid rgb(224, 224, 224); 185 | } 186 | } 187 | } 188 | } 189 | 190 | .button { 191 | float: right; 192 | margin-right: 10px; 193 | } 194 | button { 195 | background-color: rgb(255, 255, 255); 196 | transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; 197 | box-sizing: border-box; 198 | font-family: Roboto, sans-serif; 199 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 200 | box-shadow: rgba(0, 0, 0, 0.117647) 0px 1px 6px, rgba(0, 0, 0, 0.117647) 0px 1px 4px; 201 | border-radius: 2px; 202 | display: inline-block; 203 | padding: 3px 15px; 204 | font-size: 13px; 205 | letter-spacing: 0px; 206 | font-weight: 500; 207 | margin: 0px; 208 | color: rgba(0, 0, 0, 0.870588); 209 | min-width: 65px; 210 | outline: 0; 211 | } 212 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./built", 4 | "sourceMap": true, 5 | "allowJs": true, 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "jsx": "react", 9 | "target": "es5" 10 | }, 11 | "include": ["./src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | entry: ['./src/index', 'webpack-hot-middleware/client?reload=true'], 8 | devtool: 'source-map', 9 | resolve: { 10 | extensions: ['.ts', '.js', '.tsx'] 11 | }, 12 | output: { 13 | path: path.join(__dirname), 14 | filename: 'bundle.js', 15 | publicPath: '/' 16 | }, 17 | plugins: [ 18 | new HtmlWebpackPlugin({ 19 | inject: true, 20 | template: 'index.html' 21 | }), 22 | new webpack.HotModuleReplacementPlugin(), 23 | // Use NoErrorsPlugin for webpack 1.x 24 | new webpack.NoEmitOnErrorsPlugin() 25 | ], 26 | module: { 27 | rules: [ 28 | { test: /\.tsx?$/, loader: 'awesome-typescript-loader' }, 29 | { 30 | test: /\.js$/, 31 | exclude: /node_modules/, 32 | use: ['babel-loader'] 33 | }, 34 | { 35 | test: /\.(png|jpg)$/, 36 | use: [ 37 | { 38 | loader: 'url-loader', 39 | options: { 40 | limit: 8192 41 | } 42 | } 43 | ] 44 | }, 45 | { 46 | test: /\.scss$/, 47 | use: ['style-loader', 'css-loader', 'sass-loader'] 48 | } 49 | ] 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | entry: ['./src/index'], 7 | devtool: false, 8 | output: { 9 | path: path.join(__dirname, 'dist'), 10 | filename: 'bundle.[hash].js', 11 | publicPath: '/dist/' 12 | }, 13 | resolve: { 14 | extensions: ['.ts', '.js', '.tsx'] 15 | }, 16 | plugins: [ 17 | new HtmlWebpackPlugin({ 18 | inject: true, 19 | template: 'index.html' 20 | }) 21 | ], 22 | module: { 23 | rules: [ 24 | { test: /\.tsx?$/, loader: 'awesome-typescript-loader' }, 25 | { 26 | test: /\.js$/, 27 | exclude: /node_modules/, 28 | use: ['babel-loader'] 29 | }, 30 | { 31 | test: /\.(png|jpg)$/, 32 | use: [ 33 | { 34 | loader: 'url-loader', 35 | options: { 36 | limit: 8192 37 | } 38 | } 39 | ] 40 | }, 41 | { 42 | test: /\.scss$/, 43 | use: ['style-loader', 'css-loader', 'sass-loader'] 44 | } 45 | ] 46 | } 47 | }; 48 | --------------------------------------------------------------------------------