Kliensoldali webprogramozás
Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu
useState
const middleware1 = store => next => action => {
return next(action)
}
Függvény akciók
npm install --save redux-thunk
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(rootReducer, applyMiddleware(thunk));
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
// szinkron
const fetchItemsRequest = () => ({
type: FETCH_ITEMS_REQUEST
})
// aszinkron
const getAll = () => async dispatch => {
dispatch(fetchItemsRequest())
const items = await api.getItems()
dispatch(fetchItemsSuccess(items))
}
// aszinkron
const getAll = () => dispatch => {
dispatch(fetchItemsRequest())
api.getItems().then(items => dispatch(fetchItemsSuccess(items)))
}
XMLHttpRequest
, fetch
, + egyéb függvénykönyvtárak (pl. axios)
Erőforrások elérése és kezelése HTTP protokoll fölött
const source = new EventSource('updates.php');
source.onmessage = function (event) {
console.log(event.data);
};
<?php
header("Content-Type: text/event-stream\n\n");
echo 'data: ' . time() . "\n";
data: TROLOLOLOLOLOLOLOLOLOLO
data: Lorem ipsum dolor sit amet
data: consectetur adipiscing elit.
data: Vestibulum rutrum nisi in diam dapibus eget tempor mauris sollicitudin
Backend-as-a-Service (BaaS), pl. Google Firebase
class StorageApi {
async getItems() {
const response = await fetch(url, {
headers: {
'Accept': 'application/json',
},
})
const items = await response.json()
return items
}
async addItem(item) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(item)
})
const newItem = await response.json()
return newItem
}
}
--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}
--> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
<-- {"jsonrpc": "2.0", "result": -19, "id": 2}
declare(strict_types=1);
namespace App\Http\Procedures;
use Sajya\Server\Procedure;
class TennisProcedure extends Procedure {
public static string $name = 'tennis';
public function ping() {
return 'pong';
}
}
Route::rpc('/v1/endpoint', [TennisProcedure::class])->name('rpc.endpoint');
curl 'http://localhost:8000/api/v1/endpoint' \
--data-binary '{"jsonrpc":"2.0","method":"tennis@ping","params":[],"id" : 1}'
[{"id":"1","result":"pong","jsonrpc":"2.0"}]
fetch('http://localhost:8000/api/v1/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({jsonrpc:"2.0", method:"tennis@ping", params:[], id: 1})
})
.then(response => response.json())
.then(data => console.log(data));
[{"id":"1","result":"pong","jsonrpc":"2.0"}]
http://webprogramozas.inf.elte.hu:3030/tracks
fetch('http://webprogramozas.inf.elte.hu:3030/tracks', {
headers: { 'Accept': 'application/json' },
})
.then(response => response.json())
.then(data => console.log(data));
{
"total": 3,
"limit": 10,
"skip": 0,
"data": [
{
"id": 1,
"artist": "Bon Jovi",
"title": "It's my life",
"length": "3:44",
"thumbnailURL": "",
"spotifyURL": "https://open.spotify.com/track/0v1XpBHnsbkCn7iJ9Ucr1l",
"chordsURL": "https://tabs.ultimate-guitar.com/tab/bon-jovi/its-my-life-chords-951538",
"lyricsURL": "https://www.azlyrics.com/lyrics/bonjovi/itsmylife.html",
"createdAt": "2020-04-24 13:15:42.228 +00:00",
"updatedAt": "2020-04-24 13:15:42.228 +00:00",
"userId": null
},
{
"id": 2,
"artist": "Bon Jovi",
"title": "Livin' on a prayer",
"length": "4:11",
"thumbnailURL": null,
"spotifyURL": null,
"chordsURL": null,
"lyricsURL": null,
"createdAt": "2020-04-24 13:15:52.272 +00:00",
"updatedAt": "2020-04-24 13:15:52.272 +00:00",
"userId": null
},
{
"id": 3,
"artist": "AC/DC",
"title": "Thunderstruck",
"length": "4:52",
"thumbnailURL": null,
"spotifyURL": null,
"chordsURL": null,
"lyricsURL": null,
"createdAt": "2020-04-24 13:16:37.675 +00:00",
"updatedAt": "2020-04-24 13:16:37.675 +00:00",
"userId": null
}
]
}
[
{
"title": "más",
"description": "desc",
"user": {
"username": "q"
}
},
{
"title": "issue2",
"description": "desc2",
"user": {
"username": "q"
}
},
{
"title": "issue3",
"description": "desc3",
"user": {
"username": "w"
}
},
{
"title": "issue4",
"description": "desc",
"user": null
}
]
Mi van, ha csak a szerzőre és a címre vagyunk kíváncsiak?
{
"id": 1,
"artist": "Bon Jovi",
"title": "It's my life",
"length": "3:44",
"thumbnailURL": "",
"spotifyURL": "https://open.spotify.com/track/0v1XpBHnsbkCn7iJ9Ucr1l",
"chordsURL": "https://tabs.ultimate-guitar.com/tab/bon-jovi/its-my-life-chords-951538",
"lyricsURL": "https://www.azlyrics.com/lyrics/bonjovi/itsmylife.html",
"createdAt": "2020-04-24 13:15:42.228 +00:00",
"updatedAt": "2020-04-24 13:15:42.228 +00:00",
"userId": null
}
[
{
"id": 2,
"title": "playlist1",
"createdAt": null,
"updatedAt": null
},
{
"id": 7,
"title": "playlist2",
"createdAt": "2020-04-25T08:22:50.183Z",
"updatedAt": "2020-04-25T08:22:50.183Z"
}
]
type Query {
me: User
}
type User {
id: ID
name: String
}
{
me {
name
}
}
{
"me": {
"name": "Luke Skywalker"
}
}
A lekérdezés struktúrája megegyezik a kívánt adat struktúrájával
query {
issues {
title
description
user {
username
}
}
}
{
"data": {
"issues": [
{
"title": "más",
"description": "desc",
"user": {
"username": "q"
}
},
{
"title": "issue2",
"description": "desc2",
"user": {
"username": "q"
}
},
{
"title": "issue3",
"description": "desc3",
"user": {
"username": "w"
}
},
{
"title": "issue4",
"description": "desc",
"user": null
}
]
}
}
Int
, String
, Boolean
, Float
, ID
, enum[Int]
type Query {
issues: [Issue]
issueById(id: Int): Issue
}
type Issue {
id: Int
title: String
description: String
place: String
status: String
created_at: String
user: User
}
type User {
id: Int
username: String
role: String
issues: [Issue]
}
type Mutation {
createIssue(issue: IssueInput): Issue
updateIssue(id: Int, issue: IssueInput): Issue
}
input IssueInput {
title: String
description: String
place: String
status: String
}
Mező
query HeroName {
hero {
name
}
}
{
"hero": {
"name": "R2-D2"
}
}
Argumentumok
query HumanWithHeight {
human(id: "1000") {
name
height
}
}
{
"human": {
"name": "Luke Skywalker",
"height": 5.6430448
}
}
Változók
query HeroNameAndFriends($episode: Episode = JEDI) {
hero(episode: $episode) {
name
friends {
name
}
}
}
{
"episode": "JEDI"
}
{
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
Direktívák
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
{
"episode": "JEDI",
"withFriends": false
}
{
"hero": {
"name": "R2-D2"
}
}
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
type ReviewInput {
stars: Int!
commentary: String!
}
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
{
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
type Query {
issues: [Issue]
issueById(id: Int): Issue
}
type Issue {
id: Int
title: String
description: String
place: String
status: String
created_at: String
user: User
}
type User {
id: Int
username: String
role: String
issues: [Issue]
}
type Mutation {
createIssue(issue: IssueInput): Issue
updateIssue(id: Int, issue: IssueInput): Issue
}
input IssueInput {
title: String
description: String
place: String
status: String
}
curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "{ hello }"}' \
http://localhost:4000/graphql
{"data":{"hello":"Hello world!"}}
fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({query: "{ hello }"})
})
.then(response => response.json())
.then(data => console.log('data returned:', data));
data returned: Object { hello: "Hello world!" }
type Query {
rollDice(numDice: Int!, numSides: Int): [Int]
}
const dice = 3;
const sides = 6;
const query = `query RollDice($dice: Int!, $sides: Int) {
rollDice(numDice: $dice, numSides: $sides)
}`;
fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
query,
variables: { dice, sides },
})
})
.then(response => response.json())
.then(data => console.log('data returned:', data));
Ne szövegösszefűzéssel adjuk meg a paramétereket!
npm install --save apollo-boost @apollo/react-hooks graphql
import ApolloClient, { gql } from 'apollo-boost';
import { ApolloProvider, useQuery, useMutation } from '@apollo/react-hooks';
const client = new ApolloClient({
uri: 'http://localhost:4000',
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
const GET_ISSUES = gql`
query IssuesAndIssueById($id: Int) {
issues { id title description }
issueById(id: $id) { title description }
}
`;
function App() {
const [value, setValue] = useState('1')
const { loading, error, data, refetch } = useQuery(GET_ISSUES, {
variables: {
id: parseInt(value)
}
});
if (error) return <p>Error :(</p>;
return (
<div style={{backgroundColor: randomColor(), padding: '10px', border: `${loading ? 5 : 0}px solid red`}}>
<form>
<input type="number" value={value} onChange={e => setValue(e.target.value)} />
<button>Get issue by ID</button>
</form>
{data && data.issueById
? <p>{data.issueById.title} ({data.issueById.description})</p>
: <p>No data</p>
}
<ul>
{data && data.issues && data.issues.map(({id, title, description}) =>
<li key={id}>{title} ({description})</li>
)}
</ul>
<button onClick={() => refetch()}>Refresh</button><br></br>
</div>
)
}
const CHANGE_ISSUE = gql`
mutation UpdateIssue($id: Int, $issue: IssueInput) {
updateIssue(id: $id, issue: $issue) { id title }
}
`
function App() {
const [value, setValue] = useState('1')
const [title, setTitle] = useState('körte')
const { loading, error, data, refetch } = useQuery(GET_ISSUES, {
variables: {
id: parseInt(value)
}
});
✒>const [changeIssue, {data: mutationData}] = useMutation(CHANGE_ISSUE)<✒
✒>const handleClick = () => changeIssue({
variables: {
id: parseInt(value),
issue: {
title
}
}
})<✒
if (error) return <p>Error :(</p>;
return (
<div style={{backgroundColor: randomColor(), padding: '10px', border: `${loading ? 5 : 0}px solid red`}}>
<form>
<input type="number" value={value} onChange={e => setValue(e.target.value)} />
<button>Get issue by ID</button>
</form>
{data && data.issueById
? <p>{data.issueById.title} ({data.issueById.description})</p>
: <p>No data</p>
}
<ul>
{!loading && data.issues.map(({id, title, description}) =>
<li key={id}>{title} ({description})</li>
)}
</ul>
<button onClick={() => refetch()}>Refresh</button><br></br>
<input type="text" value={title} onChange={e => setTitle(e.target.value)} />
<button onClick={handleClick}>Change title</button>
</div>
)
}
// Client
var socket = new WebSocket("ws://localhost:8080");
socket.onopen = function (event) {
socket.send("Here's some text that the server is urgently awaiting!");
socket.onmessage = function (event) {
console.log(event.data);
}
};
socket.close();
// Server
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: ', message);
ws.send(message);
});
ws.send('something');
});
npm install --save socket.io-client
import io from 'socket.io-client';
const socket = io('http://localhost:8080');
socket.on('news', (data) => {
console.log(data);
socket.emit('hello', { my: data });
});
const app = require('http').createServer()
const io = require('socket.io')(app);
app.listen(8080);
io.on('connection', (socket) => {
socket.emit('news', { hello: 'world' });
socket.on('hello', (data) => {
console.log(data);
});
});
const app = require('http').createServer()
const io = require('socket.io')(app);
app.listen(8080);
io.on('connection', function(socket){
socket.on('chat message', function(msg){
io.emit('chat message', msg);
});
});
const socket = io('http://localhost:8080');
function App() {
const [value, setValue] = useState('')
const [messages, setMessages] = useState([])
useEffect(() => {
socket.on('chat message', msg => setMessages(messages => messages.concat(msg)))
return () => {
socket.off('chat message')
}
}, [])
const handleSubmit = e => {
e.preventDefault()
socket.emit('chat message', value);
}
return (
<div style={{backgroundColor: randomColor(), padding: '10px'}}>
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
<button>Send message</button>
</form>
<ul>
{messages.map((msg, i) =>
<li key={i}>{msg}</li>
)}
</ul>
</div>
)
}
import { socket } from 'socketUtil.js';
import { useDispatch } from 'react-redux';
function ChatRoomComponent(){
const dispatch = useDispatch();
useEffect(() => {
socket.on('get-message', payload => {
// update messages
dispatch({ type: UPDATE_CHAT_LOG }, payload)
});
}, [dispatch]);
const handleClick = () => {
socket.emit('send-message', 'message')
}
}
// socketUtil.js
export const socket = io.connect(WS_BASE)
import React, { createContext } from 'react'
import io from 'socket.io-client';
import { WS_BASE } from './config';
import { useDispatch } from 'react-redux';
import { updateChatLog } from './actions';
const WebSocketContext = createContext(null)
export { WebSocketContext }
export default ({ children }) => {
let socket;
let ws;
const dispatch = useDispatch();
const sendMessage = (roomId, message) => {
const payload = {
roomId: roomId,
data: message
}
socket.emit("send-message", JSON.stringify(payload));
dispatch(updateChatLog(payload));
}
if (!socket) {
socket = io.connect(WS_BASE)
socket.on("get-message", (msg) => {
const payload = JSON.parse(msg);
dispatch(updateChatLog(payload));
})
ws = {
socket: socket,
sendMessage
}
}
return (
<WebSocketContext.Provider value={ws}>
{children}
</WebSocketContext.Provider>
)
}
export const wsConnect = host => ({ type: 'WS_CONNECT', host });
export const wsConnecting = host => ({ type: 'WS_CONNECTING', host });
export const wsConnected = host => ({ type: 'WS_CONNECTED', host });
export const wsDisconnect = host => ({ type: 'WS_DISCONNECT', host });
export const wsDisconnected = host => ({ type: 'WS_DISCONNECTED', host });
function App() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(wsConnect(host));
}, [dispatch]);
}
import * as actions from '../modules/websocket';
import { updateGame, } from '../modules/game';
let socket = null;
const onOpen = store => (event) => {
console.log('websocket open', event.target.url);
store.dispatch(actions.wsConnected(event.target.url));
};
const onClose = store => () => {
store.dispatch(actions.wsDisconnected());
};
const onMessage = store => (event) => {
const payload = JSON.parse(event.data);
console.log('receiving server message');
switch (payload.type) {
case 'update_game_players':
store.dispatch(updateGame(payload.game, payload.current_player));
break;
default:
break;
}
};
// the middleware part of this function
export const socketMiddleware = store => next => action => {
switch (action.type) {
case 'WS_CONNECT':
if (socket !== null) {
socket.close();
}
// connect to the remote host
socket = new WebSocket(action.host);
// websocket handlers
socket.onmessage = onMessage(store);
socket.onclose = onClose(store);
socket.onopen = onOpen(store);
break;
case 'WS_DISCONNECT':
if (socket !== null) {
socket.close();
}
socket = null;
console.log('websocket closed');
break;
case 'NEW_MESSAGE':
console.log('sending a message', action.msg);
socket.send(JSON.stringify({ command: 'NEW_MESSAGE', message: action.msg }));
break;
default:
console.log('the next action:', action);
return next(action);
}
};