Transcript
The 3R Stack
Scott Persinger
Senior Director, Heroku
scottp@heroku.com@persingerscott
Meet the team
Howard Burrows Jeremy West Scott Persinger
3R Stack
Real-time, RESTful apps with React
Traditional Web Apps
BROWSERServer Generated
HTML
HTTP Request / Response
how did we get here?
Remember the good ol’ days?
<?php $db = mysql_connect("localhost", "root"); mysql_select_db("mydb",$db); $result = $mysqli->query("SELECT title FROM City LIMIT 1"); $title = $result->rows[0].title;?><html> <head> <title><?php echo $title ?></title> </head> <body> <center> <h2><?php echo $title ?></h2> </center> </body></html>
Dynamic Web Apps
BROWSER
DOM updates
Server Generated HTML
REST API
HTTP Request / Response
how did we get here?
AJAX
Single-Page Apps (“rich client”)
Javascript App
REST API
new state
how did we get here?
AJAX
AJAX check for updates
Why real-time?
● Collaboration apps○ See changes made by collaborators
immediately○ Avoid change conflicts
● Real-time monitoring○ See changing status immediately
● Eliminate polling○ Incurs a heavy system load
Real-time Apps
Javascript App
REST API
new state
how did we get here?
AJAX
Websocket PUSH
Websocket “SUBSCRIBE”
Rich-client Architecture
Business logic and
persistent state
RE
ST
AP
I
Rich Web client
Mobile client
Partner apps
Application architecture
Redis
server
Express
Mongoose
Node.js
socket.io
save hooks
Mongo DB
browser
React
Redux
socket.iofetch
REST API
Websocket
Server side code tour
Swagger.io
var express = require('express');
var app = express();
var router = express.Router();
router.route('/users')
.get(function(req, res) {
User.find({name: {$ne: 'admin'}}, function(err, users) {
res.json(users.map(function(q) {return q.toObject()}));
});
})
.post(function(req, res) {
var user = new User(req.body);
user.save(function(err) {
res.status(201).json(user.toObject({virtuals:true}));
});
});
app.use('/api', router);
index.js
var request = require('supertest')
request = request(app);
describe("Users CRUD...", function() {
it('can POST a new user', function(done) {
request
.post('/api/users')
.send({name:"admin"})
.expect(200)
.end(function(err, res) {
res.body.token.should.be.ok;
models.User.count({name:"admin"}, function(err, c) {
c.should.equal(1);
done();
});
});
});
});
test/users.js
browser
Reactcomponents
Front-end architecture
REST APIReact
components
Reactcomponents Action creators
“dispatch”
Actions
Store reducers
user actions
The Flux architecture
● How do I manage state in my app?● One way flow:
Action creators -> store -> components -> DOM● Encapsulate state changes as “actions”
const GameTabs = React.createClass({
componentWillMount() {
this.props.dispatch(list_users());
},
});
export function list_users() {
return function(dispatch) {
fetch(API_BASE_URL + 'api/users')
.then(response => response.json())
.then(json => {
dispatch({
type: USERS_LIST,
payload: json,
});
})
};
}
JSX component
Action creator action
components/game_tabs.jsx actions/users.js
function orderByPoints(users) {
return _.sortBy(users, user => -user.points);
}
export default function users(state=[], action) {
switch (action.type) {
case USERS_LIST:
return orderByPoints(action.payload);
case USERS_CREATE:
return orderByPoints([...state, action.payload]);
…
});
reducers/users.list
function mapStateToProps(state) {
return {
users: state.users,
game: state.game,
guesses: state.guesses,
};
}
const GameTabs = React.createClass({
render() {
return (
<Tab label="Leaderboard" >
<LeaderboardTable users=
{this.props.users} /> : ''}
</Tab>
);
}
});
game_tabs.jsx
export default React.createClass({
render: function () {
return (
<Table selectable={false}>
{this.props.users.map(user =>
<TableRow key={user.name}>
<TableRowColumn>
{user.name}
</TableRowColumn>
<TableRowColumn>
{user.points || 0}
</TableRowColumn>
</TableRow>
)}
</Table>
);
},
});
leaderboard.jsx
The reducer pattern
● Reducers interpret actions to change the application state
● Just a big switch statement● Actions are like the command pattern● Encapsulate state changes in a single place● The store is the state
Now let’s add real-time...
Key insight - add “pub/sub” to REST
Create - POSTRead - GETUpdate - PATCHDelete - DELETE
Listen - SUBSCRIBE [event]Event - PUBLISH
HTTP
Websocket
io.on('connection', function(socket) {
socket.on('subscribe', function(resource) {
// Authorize client and record subscription
});
});
UserSchema.post('save', function(doc) {
model_signals(doc.wasNew ? 'create' : 'update', ‘User’, doc);
});
function model_signals(action, modelName, doc) {
// Dispatch model event to WS subscribers
io.emit('publish',
{resource: modelName, action: action, data: doc.toObject()});
}
WS connect
model signal
WS publish
In a real application we would publish to Redis first, so all Node processes could publish.
browser
Reactcomponents
Front-end architecture
REST API
Websocket
Reactcomponents
Reactcomponents Action creators
“dispatch”
Actions
Store
api-events
reducers
event-router
let socket = this.socket = io(API_BASE_URL, {transports:['websocket']});
socket.on('publish', this._handleNotify.bind(this));
_handleNotify(data) {
store.dispatch(userAPIUpdate(data));
}
export function userAPIUpdate(event) {
switch(event.action) {
case 'create':
return {type: USERS_CREATE, payload: event.data};
…
export default function users(state=[], action) {
switch (action.type) {
case USERS_CREATE:
return [...state, action.payload];
}
}
api-events
reducers/ users
actions/ users
new user inserted
Real-time REST
● All resource changes are published over the real-time channel to interested subscribers
● Subscriptions are modeled with the resource path, like:○ SUBSCRIBE /users - listen to changes to all users○ SUBSCRIBE /users/1 - list to changes to user 1
● Simplifies design of the event channel○ Eliminates “ad-hoc message” pattern
the quiz!
https://quizlive-react.herokuapp.com
http://tinyurl.com/quizyyz
Take-aways
● Thick client + REST API is a very nice separation of concerns, but more work than MVC
● React/Flux/Redux stack is quite complicated, especially JSX
● Real-time isn’t free○ Puts query/serialization load on the backend
● But it’s a great user experience!
thank you
@persingerscott
top related