Socket.IO challenges added to Advanced Node and Express section. (#13302)

This commit is contained in:
Joseph Livengood
2017-02-12 15:33:15 -05:00
committed by Quincy Larson
parent db918e2821
commit 3861b5883a

View File

@ -501,6 +501,260 @@
"type": "backend", "type": "backend",
"challengeType": 0, "challengeType": 0,
"translations": {} "translations": {}
},
{
"id": "589fc820f9fc0f352b528e73",
"title": "Socket.IO Introduction",
"description": [
[
"",
"",
"<dfn>Socket.IO</dfn> enables real-time, reliable, speedy communication between your server and clients from all devices and browsers. It listens for connects on your server that come from the client which connects with a single javascript statement. The whole library is based on emitting, broadcasting, and recieving events that contain an event name and some data which can include things like strings, objects, arrays, and even blobs like files or video. This is used for all sorts of purposes including instant messaging online, real-time analytics, streaming, and document collaboration.<br>Minimal changes need to be made with your recent project to set it up to create a chat room for authenticated Github users- Open the starter project below and hit <strong>Remix it</strong> to create your own private version to get started!",
"https://gomix.com/#!/project/fcc-socket"
],
[
"",
"",
"To set up this project to get started now in the .env file:<ol><li>Create a random session secret</li><li>Fill in the database (it may be the same as the last few projects since this will use a new collection)</li><li>Obtain another set of Github OAuth credentials with the callback once again pointed at '/auth/github/callback' and add it in</li></ol>",
""
],
[
"",
"",
"A few changes have been made to this starter.<br>Instead of <code>app.listen</code>, you now have <pre>const http = require('http').Server(app);\n\nhttp.listen(process.env.PORT || 3000);</pre> This is required because for the initial handshake between the server and client, http is used. Setting up our server like this allows us to have both processes use a single port.<br>Also, we have declared <code>const sessionStore = new session.MemoryStore();</code> and used <code>sessionStore</code> as the store in our express session. In our previous apps we have not done this because by default it is done automattically. It is not this way so we can reference/use the <code>sessionStore</code> in other parts of our code now. In addition, we've given our session a key to reference it easier.",
""
]
],
"releasedOn": "",
"challengeSeed": [],
"tests": [],
"type": "Waypoint",
"challengeType": 7,
"isRequired": false,
"titleEs": "",
"descriptionEs": [
[]
],
"titleFr": "",
"descriptionFr": [
[]
],
"titleDe": "",
"descriptionDe": [
[]
]
},
{
"id": "589fc830f9fc0f352b528e74",
"title": "Setting up the Enviroment",
"description": [
"Add Socket.IO as a dependency and require/instanciate it in your server defined as 'io' with the http server as an argument. <code>const io = require('socket.io')(http);</code>",
"The first thing needing to be handled is listening for a new connection from the client. The <dfn>on</dfn> keyword does just that- listen for a specific event. It requires 2 arguments: a string containing the title of the event thats emitted, and a function with which the data is passed though. In the case of our connection listener, we use <em>socket</em> to define the data in the second argument. A socket is an individual client who is connected.",
"For listening for connections on our server, add the following between the comments in your project:<pre>io.on('connection', socket => {\n console.log('A user has connected');\n});</pre>",
"Now for the client to connect, you just need to add the following to your client.js which is loaded by the page after you've authenticated: <pre>/*global io*/\nvar socket = io();</pre>The comment supresses the error you would normally see since 'io' is not defined in the file. We've already added a reliable CDN to the Socket.IO library on the page in chat.pug.",
"Now try loading up your app and authenticate and you should see in your server console 'A user has connected'!",
"<strong>Note</strong><br><code>io()</code> works only when connecting to a socket hosted on the same url/server. For connecting to an external socket hosted elsewhere, you would use <code>io.connect('URL');</code>.",
"Submit your page when you think you've got it right."
],
"challengeSeed": [],
"tests": [
{
"text": "Socket.IO is a dependency",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'socket.io', 'Your project should list \"socket.io\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "Socket.IO has been properly required and instanciated",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js').then(data => {assert.match(data, /io.*=.*require.*('|\")socket.io('|\").*http/gi, 'You should correctly require and instanciate socket.io as io.');}, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "Socket.IO should be listening for connections",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /io.on.*('|\")connection('|\").*socket/gi, 'io should listen for \"connection\" and socket should be the 2nd arguments variable'); }, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "Your client should connect to your server",
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.*=.*io/gi, 'Your client should be connection to server with the connection defined as socket'); }, xhr => { throw new Error(xhr.statusText); })"
}
],
"solutions": [],
"hints": [],
"type": "backend",
"challengeType": 0,
"translations": {}
},
{
"id": "589fc831f9fc0f352b528e75",
"title": "Communicating by Emitting",
"description": [
"<dfn>Emit</dfn> is the most common way of communicating you will use. When you emit something from the server to 'io', you send an event's name and data to all the connected sockets. A good example of this concept would be emiting the current count of connected users each time a new user connects!",
"<hr>Start by adding a variable to keep track of the users just before where you are currently listening for connections. <code>var currentUsers = 0;</code>",
"Now when someone connects you should increment the count before emiting the count so you will want to add the incrementer within the connection listener. <code>++currentUsers;</code>",
"Finally after incrementing the count, you should emit the event(still within the connection listener). The event should be named 'user count' and the data should just be the 'currentUsers'. <code>io.emit('user count', currentUsers);</code>",
"<hr>Now you can implement a way for your client to listen for this event! Similarly to listening for a connection on the server you will use the <em>on</em> keyword. <pre>socket.on('user count', function(data){\n console.log(data);\n});</pre>",
"Now try loading up your app and authenticate and you should see in your client console '1' representing the current user count! Try loading more clients up and authenticating to see the number go up.",
"Submit your page when you think you've got it right."
],
"challengeSeed": [],
"tests": [
{
"text": "currentUsers is defined",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js').then(data => {assert.match(data, /currentUsers/gi, 'You should have variable currentUsers defined');}, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "Server emits the current user count at each new connection",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /io.emit.*('|\")user count('|\").*currentUsers/gi, 'You should emit \"user count\" with data currentUsers'); }, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "Your client is listening for 'user count' event",
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")user count('|\")/gi, 'Your client should be connection to server with the connection defined as socket'); }, xhr => { throw new Error(xhr.statusText); })"
}
],
"solutions": [],
"hints": [],
"type": "backend",
"challengeType": 0,
"translations": {}
},
{
"id": "589fc831f9fc0f352b528e76",
"title": "Handling a disconnect",
"description": [
"You may notice that up to now you have only been increasing the user count. Handling a user disconnecting is just as easy as handling the initial connect except the difference is you have to listen for it on each socket versus on the whole server.",
"<hr>To do this, add in to your existing connect listener a listener that listens for 'disconnect' on the socket with no data passed through. You can test this functionality by just logging to the console a user has disconnected. <code>socket.on('disconnect', () => { /*anything you want to do on disconnect*/ });</code>",
"To make sure clients continuously have the updated count of current users, you should decrease the currentUsers by 1 when the disconnect happens then emit the 'user count' event with the updated count!",
"<strong>Note</strong><br>Just like 'disconnect', all other events that a socket can emit to the server should be handled within the connecting listener where we have 'socket' defined.",
"Submit your page when you think you've got it right."
],
"challengeSeed": [],
"tests": [
{
"text": "Server handles the event disconnect from a socket",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /socket.on.*('|\")disconnect('|\")/gi, ''); }, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "Your client is listening for 'user count' event",
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")user count('|\")/gi, 'Your client should be connection to server with the connection defined as socket'); }, xhr => { throw new Error(xhr.statusText); })"
}
],
"solutions": [],
"hints": [],
"type": "backend",
"challengeType": 0,
"translations": {}
},
{
"id": "589fc831f9fc0f352b528e77",
"title": "Authentication with Socket.IO",
"description": [
"Currently, you cannot determine who is connected to your web socket. While 'req.user' containers the user object, thats only when your user interacts with the web server and with web sockets you have no req (request) and therefor no user data. One way to solve the problem of knowing who is connected to your web socket is by parsing and decoding the cookie that contains the passport session then deserializing it to obtain the user object. Luckily, there is a package on NPM just for this that turns a once complex task into something simple!",
"<hr>Add 'passport.socketio' as a dependency and require it as 'passportSocketIo'.",
"Now we just have to tell Socket.IO to use it and set the options. Be sure this is added before the existing socket code and not in the existing connection listener. For your server it should look as follows:<pre>io.use(passportSocketIo.authorize({\n cookieParser: cookieParser,\n key: 'express.sid',\n secret: process.env.SESSION_SECRET,\n store: sessionStore\n}));</pre>You can also optionally pass 'success' and 'fail' with a function that will be called after the authentication process completes when a client trys to connect.",
"The user object is now accessable on your socket object as <code>socket.request.user</code>. For example, now you can add the following: <code>console.log('user ' + socket.request.user.name + ' connected');</code> and it will log to the server console who has connected!",
"Submit your page when you think you've got it right. If you're running into errors, you can check out the project up to this point <a href='https://gist.github.com/JosephLivengood/a9e69ff91337500d5171e29324e1ff35'>here</a>."
],
"challengeSeed": [],
"tests": [
{
"text": "passportSocketIo is a dependency",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/package.json') .then(data => { var packJson = JSON.parse(data); assert.property(packJson.dependencies, 'passportSocketIo', 'Your project should list \"passportSocketIo\" as a dependency'); }, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "passportSocketIo is properly required",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js').then(data => {assert.match(data, /passportSockerIo.*=.*require.*('|\")passportSocketIo('|\")/gi, 'You should correctly require and instanciate socket.io as io.');}, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "passportSocketIo is properly setup",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /io.use.*passportSocketIo.authorize/gi, 'You should have told io to use passportSockIo.authorize with the correct options'); }, xhr => { throw new Error(xhr.statusText); })"
}
],
"solutions": [],
"hints": [],
"type": "backend",
"challengeType": 0,
"translations": {}
},
{
"id": "589fc832f9fc0f352b528e78",
"title": "Announcing New Users",
"description": [
"Many chat rooms are able to annouce when a user connects or disconnects and then display that to all of the connected users in the chat. Seeing as though you already are emitting an event on connect and disconnect, you will just have to modify this event to support such feature. The most logical way of doing so is sending 3 pieces of data with the event: name of the user connected/disconnected, the current user count, and if that name connected or disconnected.",
"<hr>Change the event name to 'user' and as the data pass an object along containing fields 'name', 'currentUsers', and boolean 'connected' (to be true if connection, or false for disconnection of the user sent). Be sure to make the change to both points we had the 'user count' event and set the disconnect one to sent false for field 'connected' instead of true like the event emitted on connect. <code>io.emit('user', {name: socket.request.user.name, currentUsers, connected: true});</code>",
"Now your client will have all the nesesary information to correctly display the current user count and annouce when a user connects or disconnects! To handle this event on the client side we should listen for 'user' and then update the current user count by using jQuery to change the text of <code>#num-users</code> to '{NUMBER} users online', as well as append a <code>&#60;li&#62;</code> to the unordered list with id 'messages' with '{NAME} has {joined/left} the chat.'.",
"An implementation of this could look like the following:<pre>socket.on('user', function(data){\n $('#num-users').text(data.currentUsers+' users online');\n var message = data.name;\n if(data.connected) {\n message += ' has joined the chat.';\n } else {\n message += ' has left the chat.';\n }\n $('#messages').append($('&#60;li&#62;').html('&#60;b&#62;'+ message +'&#60;\\/b&#62;'));\n});</pre>",
"Submit your page when you think you've got it right."
],
"challengeSeed": [],
"tests": [
{
"text": "Event 'user' is emitted with name, currentUsers, and connected",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /io.emit.*('|\")user('|\").*name.*currentUsers.*connected/gi, 'You should have an event emitted named user sending name, currentUsers, and connected'); }, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "Client properly handling and displaying the new data from event 'user'",
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")user('|\")[^]*num-users/gi, 'You should change the text of #num-users within on your client within the \"user\" even listener to show the current users connected'); assert.match(data, /socket.on.*('|\")user('|\")[^]*messages.*li/gi, 'You should append a list item to #messages on your client within the \"user\" event listener to annouce a user came or went'); }, xhr => { throw new Error(xhr.statusText); })"
}
],
"solutions": [],
"hints": [],
"type": "backend",
"challengeType": 0,
"translations": {}
},
{
"id": "589fc832f9fc0f352b528e79",
"title": "Sending and Displaying Chat Messages",
"description": [
"It's time you start allowing clients to send a chat message to the server to emit to all the clients! Already in your client.js file you should see there is already a block of code handling when the messgae form is submitted! (<code>$('form').submit(function(){ /*logic*/ });</code>)",
"<hr>Within the code you're handling the form submit you should emit an event after you define 'messageToSend' but before you clear the text box <code>#m</code>. The event should be named 'chat message' and the data should just be 'messageToSend'. <code>socket.emit('chat message', messageToSend);</code>",
"Now on your server you should be listening to the socket for the event 'chat message' with the data being named 'message'. Once the event is recieved it should then emit the event 'chat message' to all sockets <code>io.emit</code> with the data being an object containing 'name' and 'message'.",
"On your client now again, you should now listen for event 'chat message' and when recieved, append a list item to <code>#messages</code> with the name a colon and the message!",
"At this point the chat should be fully functional and sending messages across all clients! Submit your page when you think you've got it right. If you're running into errors, you can check out the project up to this point <a href='https://gist.github.com/JosephLivengood/3e4b7750f6cd42feaa2768458d682136'>here for the server</a> and <a href='https://gist.github.com/JosephLivengood/41ba76348df3013b7870dc64861de744'>here for the client</a>."
],
"challengeSeed": [],
"tests": [
{
"text": "Server listens for 'chat message' then emits it properly",
"testString": "getUserInput => $.get(getUserInput('url')+ '/_api/server.js') .then(data => { assert.match(data, /socket.on.*('|\")chat message('|\")[^]*io.emit.*('|\")chat message('|\").*name.*message/gi, 'Your server should listen to the socket for \"chat message\" then emit to all users \"chat message\" with name and message in the data object'); }, xhr => { throw new Error(xhr.statusText); })"
},
{
"text": "Client properly handling and displaying the new data from event 'chat message'",
"testString": "getUserInput => $.get(getUserInput('url')+ '/public/client.js') .then(data => { assert.match(data, /socket.on.*('|\")chat message('|\")[^]*messages.*li/gi, 'You should append a list item to #messages on your client within the \"chat message\" event listener to display the new message'); }, xhr => { throw new Error(xhr.statusText); })"
}
],
"solutions": [],
"hints": [],
"type": "backend",
"challengeType": 0,
"translations": {}
},
{
"id": "58a0b2caf9fc0f352b528e7a",
"title": "Continuing your Live Chat",
"description": [
[
"",
"",
"From this point, you could choose to implement a plethora of features with just the knowledge you have learned up to this point! Some ideas you could choose to continue this project on with include, but aren't limited to the following:<ul><li>Displaying the users profile picture next to the messages</li><li>Displaying a time stamp on messages</li><li>Saving the most recent messages so when a new user connects they can see the most recent messages sent before they connected</li><li>Add a sidebar showing active users</li><li>Create a {user} is typing feature</li><li>Turn the client into a React component you can add to any of your previous projects</li></ul>",
""
]
],
"releasedOn": "",
"challengeSeed": [],
"tests": [],
"type": "Waypoint",
"challengeType": 7,
"isRequired": false,
"titleEs": "",
"descriptionEs": [
[]
],
"titleFr": "",
"descriptionFr": [
[]
],
"titleDe": "",
"descriptionDe": [
[]
]
} }
] ]
} }