first commit

main
expressgy 1 year ago
commit 7311f1ed36
  1. 24
      .gitignore
  2. 20
      Client/.eslintrc.cjs
  3. 24
      Client/.gitignore
  4. 8
      Client/README.md
  5. 13
      Client/index.html
  6. 29
      Client/package.json
  7. 2326
      Client/pnpm-lock.yaml
  8. 1
      Client/public/vite.svg
  9. 11
      Client/src/App.jsx
  10. 22
      Client/src/assets/index.scss
  11. 13
      Client/src/main.jsx
  12. 37
      Client/src/route/index.jsx
  13. 45
      Client/src/views/Home/index.jsx
  14. 76
      Client/src/views/Home/index.module.scss
  15. 55
      Client/src/views/MessageWindow/index.jsx
  16. 110
      Client/src/views/MessageWindow/index.module.scss
  17. 57
      Client/src/views/Signin/index.jsx
  18. 45
      Client/src/views/Signin/index.module.scss
  19. 72
      Client/src/views/WebSocket/index.jsx
  20. 9
      Client/vite.config.js
  21. 16
      Server/app.js
  22. 85
      Server/ws.js
  23. 18
      package.json
  24. 25
      pnpm-lock.yaml

24
.gitignore vendored

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
Client/.gitignore vendored

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>实时通信</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

@ -0,0 +1,29 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint client --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"localforage": "^1.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"sass": "^1.66.1"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"vite": "^4.4.5"
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,11 @@
import { useState } from 'react'
import {useRoutes} from "react-router-dom";
import {routerList} from "./route/index.jsx";
function App() {
const [count, setCount] = useState(0)
return useRoutes(routerList)
}
export default App

@ -0,0 +1,22 @@
html,body,#root{
position: relative;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
#root{
display: flex;
overflow: hidden;
& > div{
position: relative;
overflow: hidden;
}
& > div:first-child{
flex-shrink: 0;
width: 240px;
}
& > div:last-child{
flex: 1;
}
}

@ -0,0 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from "./App.jsx";
import './assets/index.scss'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<App/>
</BrowserRouter>
</React.StrictMode>,
)

@ -0,0 +1,37 @@
// | ------------------------------------------------------------
// | @: version 0.1
// | @: Nie-x7129
// | @E-mail: x71291@outlook.com
// | @: index.jsx |
// | @: 2023-08-30 15:43
// | @: 2023-08-30 15:43
// | @:
// | -*-*-*- (----) -*-*-*-
// | =
// | ------------------------------------------------------------
import { Navigate } from 'react-router-dom'
import Home from "../views/Home/index.jsx";
import Signin from "../views/Signin/index.jsx";
import WebSocketClient from "../views/WebSocket/index.jsx";
export const routerList = [
{
path:'/',
element: <Navigate to = '/signin'/>
},
{
path:'/signin',
element: <Signin/>
},
{
path:'/home',
element: <Home/>,
children:[
{
path:'websocket',
element: <WebSocketClient/>
},
]
},
]

@ -0,0 +1,45 @@
// | ------------------------------------------------------------
// | @: version 0.1
// | @: Nie-x7129
// | @E-mail: x71291@outlook.com
// | @: Home.jsx |
// | @: 2023-08-30 15:52
// | @: 2023-08-30 15:52
// | @:
// | -*-*-*- (----) -*-*-*-
// | =
// | ------------------------------------------------------------
import {Outlet, NavLink, useLocation} from "react-router-dom";
import css from './index.module.scss'
import {useEffect, useState} from "react";
export default function Home(){
// @ host - String - ws
const [host, setHost] = useState('');
// @ nickname - String - ws
const [nickname, setNickName] = useState('');
// @ location - String -
const [location, setLocation] = useState(useLocation());
useEffect(() => {
setHost(window.localStorage.getItem('wsHost'));
setNickName(window.localStorage.getItem('wsNickname'))
}, [0]);
return <>
<div className={css.left}>
<div className={css.info}>
<header>个人信息</header>
<div>昵称: {nickname}</div>
<div>链接地址: {host}</div>
</div>
<div className={css.server}>
<header>服务</header>
<NavLink to={'websocket'} className={({ isActive }) => isActive ? css.choose : ''}>WebSocket</NavLink>
</div>
</div>
<div className={css.right}>
<header>{location?.pathname && location?.pathname?.split('/').slice(-1).toString().toUpperCase()}</header>
<div><Outlet/></div>
</div>
</>
}

@ -0,0 +1,76 @@
.left{
position: relative;
overflow: hidden;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 10px;
& > div.info{
position: relative;
flex-shrink: 0;
overflow: hidden;
border-radius: 10px;
background: #00033322;
box-sizing: border-box;
padding: 10px;
&>header{
position: relative;
line-height: 3em;
text-align: center;
font-size: 18px;
}
& > div{
position: relative;
line-height: 2em;
font-size: 14px;
}
}
& > div.server{
position: relative;
flex: 1;
overflow: hidden;
background: aquamarine;
margin-top: 10px;
border-radius: 10px;
padding: 10px;
&>header{
position: relative;
line-height: 3em;
text-align: center;
font-size: 18px;
}
}
.choose{
all: unset;
cursor: pointer;
background: darkmagenta;
color: #fefefe;
}
}
.right{
position: relative;
overflow: hidden;
height: 100%;
padding: 10px;
box-sizing: border-box;
margin: 10px 10px 10px 0;
border-radius: 10px;
background: #eeeeee99;
display: flex;
flex-direction: column;
& > header{
position: relative;
flex-shrink: 0;
line-height: 3em;
font-size: 18px;
font-weight: bold;
}
& > div{
position: relative;
flex: 1;
}
}

@ -0,0 +1,55 @@
import css from './index.module.scss'
import {useEffect, useState} from "react";
export default function MessageWindow(props){
// @ inputMessage - String -
const [inputMessage, setInputMessage] = useState('');
// @ messageList - Object[] -
const [messageList, setMessageList] = useState([]);
useEffect(() => {
setMessageList(props.messageList)
}, [props.messageList])
// = : handleInputMessageChange
// = :
// = : event
// = : undefined
// = : Nier
// = : 2023-08-30 19:41:26 -
function handleInputMessageChange(event) {
setInputMessage(event.target.value)
}
// = : handleSendMessage
// = :
// = : null
// = : undefined
// = : Nier
// = : 2023-08-30 19:42:04 -
function handleSendMessage() {
props?.sendMessage(inputMessage)
setInputMessage('')
}
return <div className={css.messageWindow}>
<div className={css.container}>
<div className={css.body}>
<div className={css.container}>
{
messageList.map(item => <div key={item.timeId} className={item.isMe ? [css.messageBox,css.myMessageBox].join(' ') : css.messageBox}>
<div>
<div>
<div>{item.nickname}</div>&nbsp;&nbsp;|&nbsp;&nbsp;
<div>{item.timeId}</div>
</div>
<div>{item.message}</div>
</div>
</div>)
}
</div>
</div>
<div className={css.footer}>
<textarea value={inputMessage} onChange={handleInputMessageChange}></textarea>
<div className={css.sendButton} onClick={handleSendMessage}>发送</div>
</div>
</div>
</div>
}

@ -0,0 +1,110 @@
.messageWindow{
position: relative;
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
& > div.container{
position: relative;
min-width: 800px;
max-width: 1200px;
width: auto;
overflow: hidden;
height: 80%;
min-height: 700px;
max-height: 900px;
display: flex;
flex-direction: column;
background: #9D44C099;
border-radius: 10px;
box-sizing: border-box;
padding: 15px;
box-shadow: 0 0 20px -5px #33333333;
& > div.body{
position: relative;
flex: 1;
background: #F7E987;
border-radius: 10px;
box-shadow: 0 0 10px -5px #33333399 inset;
margin-bottom: 15px;
padding: 15px;
overflow: hidden;
& > div.container{
position: relative;
max-height: 100%;
box-sizing: border-box;
overflow: auto;
&>div.messageBox{
position: relative;
& > div{
position: relative;
border-radius: 10px 10px 10px 0;
padding: 10px;
box-sizing: border-box;
box-shadow: 0 0 10px 2px #33333333;
background: #1BB74Bcc;
width:fit-content;
width:-webkit-fit-content;
width:-moz-fit-content;
cursor: pointer;
color: #fefefe;
margin: 10px 0;
& > div:first-child{
position: relative;
display: flex;
}
}
}
&>div.myMessageBox{
display: flex;
justify-content: end;
& > div{
border-radius: 10px 10px 0 10px ;
background: darkmagenta;
}
}
}
}
& > div.footer{
position: relative;
height: 140px;
flex-shrink: 0;
background: #fcfcfc;
border-radius: 10px;
box-sizing: border-box;
box-shadow: 0 6px 20px -2px #33333333;
padding: 15px;
& > textarea{
border:unset;
outline: unset;
resize: unset;
position: relative;
display: block;
width: 100%;
background: #fcfcfc;
height: 100%;
overflow: auto;
}
& > div.sendButton{
position: absolute;
padding: 5px 15px;
bottom: 15px;
right: 15px;
background: #1BB74B;
border-radius: 5px;
cursor: pointer;
user-select: none;
transition: all ease-in-out 200ms;
color: #fefefe;
//font-weight: 600;
font-size: 16px;
&:hover{
box-shadow: 0 0 10px 1px #33333333;
}
}
}
}
}

@ -0,0 +1,57 @@
import css from './index.module.scss'
import {useEffect, useState} from "react";
import { useNavigate} from 'react-router-dom'
export default function Signin(){
const navigate = useNavigate()
// @ host - String - webSocket
const [host, setHost] = useState('')
// @ nickname - String -
const [nickname, setNickname] = useState('');
useEffect(() => {
// @ storageHost - String - localStoragews
const storageHost = window.localStorage.getItem('wsHost');
// @ storageNickname - String - localStoragews
const storageNickname =window.localStorage.getItem('wsNickname');
storageHost && setHost(storageHost)
storageNickname && setNickname(storageNickname)
}, [0]);
// = : handleHostChange
// = : host
// = : None
// = : undefined
// = : Nier
// = : 2023-08-30 16:42:16 -
function handleHostChange(event) {
setHost(event.target.value)
}
// = : handleChangeNickname
// = : nickName
// = : None
// = : undefined
// = : Nier
// = : 2023-08-30 16:42:51 -
function handleChangeNickname(event) {
setNickname(event.target.value)
}
// = : handleConfirmHost
// = : host
// = : None
// = : undefined
// = : Nier
// = : 2023-08-30 16:42:19 -
function handleConfirmHost() {
window.localStorage.setItem('wsHost', host)
window.localStorage.setItem('wsNickname', nickname)
navigate('/home')
}
return <div className={css.signin}>
<div>
<div className={css.input}><input type="text" value={host} onChange={handleHostChange} placeholder="请输入连接地址"/></div>
<div className={css.input}><input type="text" value={nickname} onChange={handleChangeNickname} placeholder="请输入昵称"/></div>
<div className={css.button} onClick={handleConfirmHost}>确认连接</div>
</div>
</div>
}

@ -0,0 +1,45 @@
.signin{
position: relative;
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
& > div{
position: relative;
input{
all: unset;
width: 300px;
background: #00033322;
padding: 10px 10px;
font-size: 20px;
font-weight: bold;
border-radius: 5px;
text-align: center;
}
&>.input{
position: relative;
margin: 20px 0;
}
& > .button{
position: relative;
margin: 0 auto;
/*div宽度适应文字*/
width:fit-content;
width:-webkit-fit-content;
width:-moz-fit-content;
margin-top: 40px;
padding: 10px 20px;
border-radius: 5px;
background: darkmagenta;
font-weight: bold;
color: #fefefe;
cursor: pointer;
transition: all ease-in-out 300ms;
&:hover{
box-shadow: 0 10px 20px -8px #33333366;
}
}
}
}

@ -0,0 +1,72 @@
import {useEffect, useLayoutEffect, useState, useRef} from "react";
import localforage from 'localforage';
import MessageWindow from "../MessageWindow/index.jsx";
export default function WebSocketClient(){
// @ nickname - String - WebSocket
const [nickname, setNickname] = useState(window.localStorage.getItem('wsNickname'))
// @ ws - WebSocket - WebSocket
const [ws, setWs] = useState(null)
// @ wsState - Boolean - websocket
const [wsState, setWsState] = useState(false);
// @ webSocketDB - LocalForage - WebSocket
const [webSocketDB, setWebSocketDB] = useState(null);
// @ messageList - Object[] -
const [messageList, setMessageList] = useState([]);
useEffect(() => {
const socket = ws === null && new WebSocket(`ws://${window.localStorage.getItem('wsHost')}:10001`);
const db = localforage.createInstance({
name: "websocket"
});
const msgList = []
db.iterate((value, key, iterationNumber) => {
msgList.push({
timeId: key,
...value
})
}).then(resd => {
setMessageList(msgList)
})
socket.onopen = event => {
setWsState(true)
}
socket.onerror = event =>{
console.error('Error',event)
}
socket.onclose = event => {
setWsState(false)
setWs(null)
}
setWs(socket)
setWebSocketDB(db)
return () => {
socket.readyState === 1 &&socket.close()
}
}, [0])
if(ws){
ws.onmessage = event => {
const msg = JSON.parse(event.data)
msg.nickname == nickname ? msg.isMe = true : msg.isMe = false;
setMessageList([...messageList,msg])
webSocketDB.setItem(msg.timeId, msg)
}
}
// = : handleSendMessage
// = : WebSocket
// = : message | String
// = : undefined
// = : Nier
// = : 2023-08-30 19:58:30 -
function handleSendMessage(message) {
ws.send(JSON.stringify({
message,
timeId: new Date().getTime().toString() + parseInt(Math.random() * 10000000),
nickname
}))
}
return <MessageWindow sendMessage={handleSendMessage} messageList={messageList}/>
}

@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: "0.0.0.0",}
})

@ -0,0 +1,16 @@
// | ------------------------------------------------------------
// | @版本: version 0.1
// | @创建人: 【Nie-x7129】
// | @E-mail: x71291@outlook.com
// | @文件描述: app.js | 实时通信服务端
// | @创建时间: 2023-08-30 14:57
// | @更新时间: 2023-08-30 14:57
// | @修改记录:
// | -*-*-*- (时间--修改人--修改说明) -*-*-*-
// | =
// | ------------------------------------------------------------
import {httpWebSocket} from "./ws.js";
// http WebSocket
httpWebSocket()

@ -0,0 +1,85 @@
// | ------------------------------------------------------------
// | @版本: version 0.1
// | @创建人: 【Nie-x7129】
// | @E-mail: x71291@outlook.com
// | @文件描述: ws.js | 通过WebSocket建立服务
// | @创建时间: 2023-08-30 15:08
// | @更新时间: 2023-08-30 15:08
// | @修改记录:
// | -*-*-*- (时间--修改人--修改说明) -*-*-*-
// | =
// | ------------------------------------------------------------
// 引入WebSocketServer
import WebSocket, { WebSocketServer } from 'ws';
// 文件操作
import { createServer } from 'https';
import { readFileSync } from 'fs';
// = 函数名: httpWebSocket
// = 描述: 使用http模式建立WebSocket服务
// = 参数: None
// = 返回值: undefined
// = 创建人: Nier
// = 创建时间: 2023-08-30 15:12:57 -
async function httpWebSocket() {
// @ option - Object - 描述:webSocket配置项
const option = {
// 创建服务器 必须提供 port、server、noServer其中任意一个 否则报错
port:10001, // 类型:Number 服务器端口号
host:'10.10.10.10', // 类型:String 服务器ip(主机名)
backlog:50, // 类型:Number 最大等待连接队列的长度
// server // 类型:http.Server|https.Server 预先创建的http或https的服务器
// path:'message', // 只接受与此路径匹配的连接。
// noServer:false, // 类型:Boolean 不启用服务器模式
//clientTracking // 指定是否跟踪客户端
maxPayload:4096, // 允许的最大消息大小(以字节为单位),默认为 100 MiB(104857600 字节)。
}
const wss = new WebSocketServer(option,() => {
console.log(`HTTP WebSocket 已启动: ws://${option.host}:${option.port}`)
});
wss.on('connection', function connection(ws) {
console.log('Conn')
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
ws.onclose = event => {
console.log('Close')
}
});
}
// = 函数名: httpsWebSocket
// = 描述: 使用https模式建立WebSocket服务
// = 参数: None
// = 返回值: undefined
// = 创建人: Nier
// = 创建时间: 2023-08-30 15:13:30 -
function httpsWebSocket() {
const server = createServer({
cert: readFileSync('/path/to/cert.pem'),
key: readFileSync('/path/to/key.pem')
});
const wss = new WebSocketServer({ server, port: 10002 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log('received: %s', data);
});
ws.send('something');
});
}
export {
httpWebSocket,
httpsWebSocket
}

@ -0,0 +1,18 @@
{
"name": "real-time-communication",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start:server": "node Server/app.js",
"start:client": "cd Client && npm run dev"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ws": "^8.13.0"
}
}

@ -0,0 +1,25 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
ws:
specifier: ^8.13.0
version: 8.13.0
packages:
/ws@8.13.0:
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false
Loading…
Cancel
Save