Клонуємо шаблон.
git clone git@github.com:zdimon/marafon-js-quiz-template.git
Створюємо програму.
npx create-react-app my-app --template typescript
Ця команда створить найпростіший додаток та встановить у нього всі залежності.
Запуск веб-сервера програми.
npm run start
Копіюємо всі шаблони в папку public c заміною index.html.
Помилка з’являється через те, що тепер в index.html відсутній елемент, до якого прив’язується React.
Закоментуємо запуск програми за промовчанням.
// ReactDOM.render(
// <React.StrictMode>
// <App />
// </React.StrictMode>,
// document.getElementById('root')
// );
Запускаємо сервер
npm run startу
У React все складається з програмних компонентів, які прив’язуються до тегів сторінки.
Створюємо компонент Question у новій папці components.
import React from 'react';
export function Question() {
return (
<div className="question">
Test question
</div>
);
}
Прив’яжемо компонент до дива в index.tsx.
import {Question} from './components/Question';
ReactDOM.render(
<React.StrictMode>
<Question />
</React.StrictMode>,
document.getElementById('#currentQuestionBlock')
);
Створимо компонент форми.
import React from 'react';
export function LoginForm() {
return (
<div className="chat-start">
Как вас зовут?
<input type="text" className="round" />
<button id="chat-start" className="btn bg-white mt-3">Начать!</button>
</div>
);
}
Прив’яжемо до елемента.
...
import {LoginForm} from './components/LoginForm';
...
ReactDOM.render(
<React.StrictMode>
<LoginForm />
</React.StrictMode>,
document.getElementById('#loginForm')
);
Створимо умову, де перевіримо залогіненість.
if(sessionStorage.getItem('username')) {
ReactDOM.render(
<React.StrictMode>
<Question />
</React.StrictMode>,
document.getElementById('#currentQuestionBlock')
);
} else {
ReactDOM.render(
<React.StrictMode>
<LoginForm />
</React.StrictMode>,
document.getElementById('#loginForm')
);
}
Створимо сервіс, що генерує запити на сервер.
Встановимо бібліотеку для http запитів.
npm install axios --save
Створюємо клас у Request.ts
import axios from "axios";
const serverUrl = 'http://localhost:7777/v1/quiz/';
export class Request {
async get(url: string) {
let response = await axios.get(`${serverUrl}${url}`)
return response.data;
}
}
axios у своїй роботі використовує проміси.
Існує спеціальний синтаксис для роботи з промісами, який називається async/await.
Слово async має один простий зміст: ця функція завжди повертає проміс. Значення інших типів обертаються в успішний проміс автоматично.
await axios.get() - запустить запит і не піде далі за кодом, поки він не завершиться.
Зробимо AJAX-запит у події компонента componentDidMount.
У компоненті можуть бути стани - це дані, які змінюються в міру подій додатка.
Для того, щоб ці дані визначити і міняти, є функція використаннядержави.
Ця функція повертає змінну стану та функцію для його зміни (бо стан ніколи не змінюється безпосередньо через змінну присвоєнням).
Тобто. Коли отримаємо дані з сервера, ми повинні викликати setState, щоб передати їх компоненту.
При отриманні даних із сервера ми повинні переконатися, що на даний момент компонент існує в DOM.
Для того, щоб підв’язатися до моменту монтування компонента використовуємо хук useEffect
Хук useEffect являє собою сукупність методів componentDidMount, componentDidUpdate, і componentWillUnmount.
Так як ми плануємо виводити стікери в циклі зручніше під них створити окремий компонент і передати стикер властивістю (props)
import React from 'react';
import { useState } from 'react';
export function Sticker(props: any) {
const [current, setCurrent] = useState(0);
return (
<img
width="50"
alt=""
src={props.item.get_url}
/>
)
}
** Sticker(props: any)** - Тут ми говоримо про те, що в компонент зайдуть дані.
Відпрацюємо клік на стікері та встановимо поточний стікер у вигляді змінної стану current.
import React from 'react';
import { useState } from 'react';
export function Sticker(props: any) {
const [current, setCurrent] = useState(0);
const select = (id: number) => {
setCurrent(id);
}
return (
<img
width="50"
className={` ${props.item.id===current ? 'sticker-active': ''} `}
alt=""
onClick={() => select(props.item.id)}
src={props.item.get_url}
/>
)
}
import React, { useState, useEffect } from 'react';
import { Request } from '../Request';
import {Sticker} from './Sticker';
export function LoginForm() {
const [stickers, setStickers] = useState([]);
useEffect(() => {
let req = new Request();
req.get('sticker/list').then((data) => {
setStickers(data);
})
},[]);
return (
<div className="login-form">
<p>Как вас зовут?</p>
<p><input type="text" className="round" /></p>
<div className="stickers" >
{ stickers.map((el,index) => <Sticker key={index} item={el} />) }
</div>
<button id="chat-start" className="btn bg-white mt-3">Начать!</button>
</div>
);
}
При використанні useEffect потрібно передати порожній масив другим параметром для запобігання рекурсії т.к. інакше це спрацьовуватиме при кожних змінах стейту.
useEffect(() => {},[]);
Додамо запит на сервер для отримання поточного питання у компоненті Question.tsx
import React from 'react';
import { Request } from '../Request';
import { useState, useEffect } from 'react';
export function Question() {
const req = new Request();
const [question, setQuestion] = useState({
question: '',
answers: ''
});
useEffect(() => {
req.get('get_current_question').then((data) => {
setQuestion(data);
})
},[]);
return (
<div className="question">
{question.question} ({question.answers})
</div>
);
}
TODO: Формат повідомлення тут добре оформити у вигляді інтерфейсу.
useState({
question: '',
answers: ''
});
Далі нам потрібно відпрацювати клік по стікеру і передати його з компонента Sticker компонент LoginForm.
Для цього визначимо обробник кліку у Sticker.ts
import React from 'react';
export function Sticker(props:any) {
var select = (id: number) => {
props.onSelectSticker(id);
}
return (
<img width="50"
onClick={() => select(props.item.id)}
src={props.item.get_url} />
);
}
Та прокинемо обробник через props з батьківського компоненту у Sticker.
...
export function LoginForm() {
...
const [sticker, setSticker] = useState(0);
...
var handleSelectSticker = (id: number) => {
setSticker(id);
}
return (
...
<div className="stickers">
{
stickers.map((el, key) =>
<Sticker
onSelectSticker={handleSelectSticker}
item={el}
key={key}
/>)
}
...
Додамо state для імені користувача та прив’яжемо його до зміни input-а за допомогою оброблювача handleChangeUsername.
export function LoginForm() {
...
const [username, setUsername] = useState('');
var handleChangeUsername = (evt:any) => {
setUsername(evt.target.value);
}
...
<input onChange={handleChangeUsername} type="text" className="round" />
Створимо компонент форми надсилання MessageForm.ts.
import React, {useState} from 'react';
import { Request } from '../Request';
export function MessageForm() {
const req = new Request();
const [message, setMessage] = useState('');
var handleChangeMessage = (evt:any) => {
setMessage(evt.target.value);
}
var submit = () => {
let data = {
message: message,
playername: localStorage.getItem('username')
}
req.post('save_message', data).then((data) => {
setMessage('');
})
}
return (
<>
<input
type="text"
className="form-control mr-3"
placeholder="Введите ответ на вопрос"
defaultValue={message}
onChange={handleChangeMessage}
/>
<button onClick={submit} type="submit" className="btn btn-primary d-flex align-items-center p-2"><i className="fa fa-paper-plane-o" aria-hidden="true"></i><span className="d-none d-lg-block ml-1">Send</span></button>
);
}
Нам тепер потрібно встановити сокет-з’єднання.
Для цього створимо клас з’єднання SocketConnection.ts та визначимо обробники основних подій у його конструкторі.
export class SocketConnection {
timer:any = null;
websocket:any = null;
constructor() {
this.wsConnect();
}
wsConnect() {
clearInterval(this.timer);
this.websocket = new WebSocket('ws://quizapi.webmonstr.com:7777/quiz/');
this.websocket.onerror = (evt: any) => {
this.timer = setTimeout(() => this.wsConnect(),2000);
}
this.websocket.onmessage = (message: any) => {
var message = JSON.parse(message.data)
console.log(message);
}
this.websocket.onclose = (event: any) => {
console.log('Close connection');
this.timer = setTimeout(() => this.wsConnect(),2000);
};
}
}
Нам необхідно відстежувати надходження повідомлень по сокетах у різних компонентах.
Для цього найзручніше генерувати події та підписуватися на них у компонентах.
У цьому класі ми будемо використовувати бібліотеку RxJS і її пострумене Subject.
[посилання на документацію] (https://www.learnrxjs.io/learn-rxjs/subjects/subject)
Встановимо
npm install rxjs @types/rx --save
Застосуємо та запустимо подію при надходженні повідомлення.
...
import { Subject } from 'rxjs';
...
export class SocketConnection {
...
newMessage$ = new Subject();
...
this.websocket.onmessage = (message: any) => {
var message = JSON.parse(message.data)
this.newMessage$.next(message);
}
Однак тепер потрібно вирішити проблему багатьох екземплярів цього класу.
Застосуємо патерн singletone.
export class SocketConnection {
private static instance: SocketConnection;
...
public static getInstance(): SocketConnection {
if (!SocketConnection.instance) {
SocketConnection.instance = new SocketConnection();
}
return SocketConnection.instance;
}
Тепер будемо створювати цей об’єкт через метод getInstance і він буде завжди в єдиному екземплярі.
import { SocketConnection } from '../SocketConnection';
const socket = SocketConnection.getInstance();
Підпишемося на цю подію у компоненті Question.
import { SocketConnection } from '../SocketConnection';
export function Question() {
const req = new Request();
const socket = SocketConnection.getInstance();
socket.newMessage$.subscribe((data: any) => {
console.log(data);
});
Однак при цьому ми спостерігаємо 4 реакції на одну подію.
Проблема в тому, що функція реакт компонента запускається багато разів.
Щоб щось вистрілило один раз, потрібно загорнути це в хук, використовуючиефект.
Зробимо це та запитаємо нове питання із сервера.
...
export function Question(props: any) {
useEffect(() => {
socket.sendMessage$.subscribe((data: any) => {
if(data.is_right) {
let req = new Request();
req.get('get_current_question').then((data) => {
setQuestion(data);
console.log(data);
});
}
});
},[])
...
Створимо компонент повідомлення Message.tsx.
import React from 'react';
export function Message(props: any) {
return (
<div className={`chat ${props.message.is_right ? "chat-left" : ""}`}>
<div className="chat-user">
<a className="avatar m-0">
<img src="images/user/1.jpg" alt="avatar" className="avatar-35 " />
</a>
<span className="chat-time mt-1">{props.message.playername}</span>
</div>
<div className="chat-detail">
<div className="chat-message">
<p>{ props.message.text } {props.message.is_right}</p>
</div>
</div>
</div>
);
}
Створимо компонент списку повідомлень MessageBox.tsx.
Відпрацюємо повідомлення приходу нової відповіді.
import React from 'react';
import { Request } from '../Request';
import { useState, useEffect } from 'react';
import { SocketConnection } from '../SocketConnection';
import { Message } from './Message';
const socket = SocketConnection.getInstance();
export function MessageBox(props: any) {
useEffect(() => {
socket.newMessage$.subscribe((payload: any) => {
// const msg = [...messages] as any;
// msg.push(payload);
// setMessages(msg);
const req = new Request();
req.get('message/list').then((data) => {
setMessages(data);
})
});
},[])
const [messages, setMessages] = useState([]);
useEffect(() => {
const req = new Request();
req.get('message/list').then((data) => {
setMessages(data);
console.log(data);
})
},[]);
return (
<div className="messages">
{ messages.map((el, key) => <Message key={key} message={el} />) }
</div>
);
}
Компонент користувача CurrentPlayer.tsx.
import React from 'react';
import { useState, useEffect } from 'react';
export function CurrentPlayer(props: any) {
const [image, setImage] = useState('');
useEffect(() => {
setImage(localStorage.getItem('image') as string);
},[]);
return (
<>
<div className="avatar chat-user-profile m-0 mr-3">
<img
src={image}
alt="avatar"
className="avatar-50 " />
</div>
<h5 className="mb-0">{localStorage.getItem('username')}</h5>
<h5 className="player-account"> Правильных ответов: {localStorage.getItem('account')}</h5>
);
}
Компонент у списку учасників PlayerList.tsx.