Bootstrap socket.io
This is bootstrap continuation from: Getting Started ExpressJS 2020
Alternative way for passing argument instead of global variable.
NPM install
# install socket.io
npm i -S socket.io
Pass io as argument
app.js
module.exports = ({ express, app, io }) => {
return app
}
app.js
Becomes
var createError = require('http-errors')
var path = require('path')
var cookieParser = require('cookie-parser')
var logger = require('morgan')
var sassMiddleware = require('node-sass-middleware')
var hbs = require('hbs')
var indexRouter = require('./routes/index')
var usersRouter = require('./routes/users')
module.exports = ({ express, app, io }) => {
// view engine setup
hbs.registerPartials(__dirname + '/views/partials')
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'hbs')
// express routes
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(
sassMiddleware({
src: path.join(__dirname, 'resources', 'scss'),
dest: path.join(__dirname, 'public', 'css'),
prefix: '/css',
indentedSyntax: false, // true = .sass and false = .scss
sourceMap: true
})
)
app.use(express.static(path.join(__dirname, 'public')))
app.use('/', indexRouter)
app.use('/users', usersRouter)
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404))
})
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message
res.locals.error = req.app.get('env') === 'development' ? err : {}
// render the error page
res.status(err.status || 500)
res.render('error')
})
return app
}
app.js
Basic understanding for passing an argument io
, and later on, refactoring the code.
www
var http = require('http'),
express = require('express'),
app = express()
/**
* Create HTTP server.
*/
var server = http.createServer(app),
io = require('socket.io')(server)
require('../app')({ express, app, io })
bin/www
Becomes
#!/usr/bin/env node
/**
* Module dependencies.
*/
var debug = require('debug')('express-2020:server')
var http = require('http'),
express = require('express'),
app = express()
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000')
app.set('port', port)
/**
* Create HTTP server.
*/
var server = http.createServer(app)
var io = require('socket.io')(server)
require('../app')({ express, app, io })
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10)
if (isNaN(port)) {
// named pipe
return val
}
if (port >= 0) {
// port number
return port
}
return false
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error
}
var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
break
case 'EADDRINUSE':
console.error(bind + ' is already in use')
process.exit(1)
break
default:
throw error
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address()
var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
debug('Listening on ' + bind)
}
bin/www
Git commit: Pass io as argument
git add app.js bin/www
git commit -m "Pass io as argument"
Integrating socket.io
module.exports = io => {
io.on('connection', socket => {
console.log('Socket Connect:', { id: socket.id })
socket.on('disconnect', message => {
console.log({ id: socket.id, message })
})
})
return app
}
app.js
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
</script>
views/partials/script.hbs
Git commit: Integrating socket.io
git add app.js views/partials/script.hbs
git commit -m "Integrating socket.io"
Optional Templates Examples
Template: Chat
u-box.less
/**
** mixins
**/
.flex() {
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
display: -ms-flexbox; /* TWEENER - IE 10 */
display: -webkit-flex; /* NEW - Chrome */
display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
.flex--column() {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.flex--row() {
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
}
.flex--align-center() {
align-items: center;
}
.flex-flex(@flex: 1) {
-webkit-box-flex: @flex; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-flex: @flex; /* OLD - Firefox 19- */
-webkit-flex: @flex; /* Chrome */
-ms-flex: @flex; /* IE 10 */
flex: @flex;
}
.flex-content(@content) {
-webkit-justify-content: @content;
-ms-flex-pack: @content;
justify-content: @content;
}
/* END mixins */
/**
** utils
**/
.u-box {
.flex();
/*
* Flex Direction: COLUMN - Where elements goes from TOP to BOTTOM
*/
&--column {
.flex--column();
}
/*
* Flex Direction: ROW - Where elements goes from LEFT to RIGHT
*/
&--row {
.flex--row();
}
// flex-wrap on default `nowrap` where items is inline
&--wrap {
flex-wrap: wrap;
}
&--wrap-reverse {
flex-wrap: wrap-reverse;
}
&--list {
margin-right: -1rem;
margin-bottom: -1rem;
> * {
margin-right: 1rem;
margin-bottom: 1rem;
&:empty {
display: none;
}
}
}
/*
* Justify content HORIZONTALLY - left/right/row
*/
&--justify {
&-flex-start {
justify-content: flex-start;
}
&-flex-end {
justify-content: flex-end;
}
&-center {
justify-content: center;
}
&-space-between {
justify-content: space-between;
}
&-space-around {
justify-content: space-around;
}
}
&--inline {
display: inline-flex;
}
/*
* Align content VERTICALLY - up/down/column
*/
&--align {
&-flex-start {
align-items: flex-start;
}
&-flex-end {
align-items: flex-end;
}
&-center {
align-items: center;
}
&-stretch {
align-items: stretch;
}
&-space-between {
align-items: space-between;
}
&-space-around {
align-items: space-around;
}
}
&--align-center {
-webkit-align-items: center; /* Safari 7.0+ */
align-items: center;
}
&--flex-end {
-webkit-justify-content: flex-end;
justify-content: flex-end;
}
&--queries {
.flex--column();
}
}
.u-flex {
.flex-flex(1);
&--minWidth {
flex-basis: 320px;
}
&--grow {
flex-grow: 1;
}
&--shrink {
flex-shrink: 0;
}
}
public/less/u-box.less
<link rel="stylesheet/less" type="text/less" href="less/u-box.less" />
<style rel="stylesheet/less" type="text/less">
body {
margin: 0;
padding: 0;
}
.app-tpl {
height: ~"calc(100vh)";
overflow-y: auto;
overflow-x: hidden;
background-color: rgb(248, 239, 201);
}
.messages {
list-style: none;
margin: 0;
padding: 0;
li {
&:nth-child(odd) {
background-color: yellow;
}
}
}
</style>
<script src="//cdnjs.cloudflare.com/ajax/libs/less.js/2.7.1/less.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script type="text/html" id="app-tpl">
<div class="app-tpl u-box u-box--column">
<main class="u-flex u-box u-box--column">
<ul class="u-flex messages">
<li>Welcome</li>
<li v-for="msg in messages"><strong v-text="msg.id" />: \{{msg.message}}</li>
</ul>
</main>
<footer>
<form class="u-box" @submit="onSubmit">
<input class="u-flex" v-model="formData.message" type="text" placeholder="Message" />
<button type="submit">Send</button>
</form>
</footer>
</div>
</script>
<script>
var socket = io(),
el = document.currentScript
const storeMessages = {
state: Vue.observable({
messages: []
}),
methods: {
onMessage(message) {
this.state.messages.push(message)
}
}
}
const App = new Vue({
template: '#app-tpl',
data: () => ({
formData: {
message: ''
}
}),
computed: {
messages() {
return storeMessages.state.messages || []
}
},
methods: {
onSubmit(event) {
event.preventDefault()
const { formData } = this
socket.emit('chat-message', formData.message)
console.log('message:', formData.message)
formData.message = ''
}
}
})
App.$mount(el)
socket.on('chat-message', storeMessages.methods.onMessage.bind(storeMessages))
</script>
views/index.hbs
module.exports = ({ express, app, io }) => {
io.on('connection', socket => {
console.log('Socket Connect:', { id: socket.id })
io.emit('chat-message', { id: socket.id, message: 'ENTERS ROOM' })
socket.on('disconnect', message => {
console.log({ id: socket.id, message })
socket.broadcast.emit('chat-message', { id: socket.id, message: 'LEFT' })
})
socket.on('chat-message', msg => {
console.log('chat-message:', msg)
io.emit('chat-message', { id: socket.id, message: msg })
})
})
return app
}
app.js
Manual: Chat
- Open Index with different tabs
- Type something and see messages accumulate
Template: Routes
Routes
module.exports = ({ Router, io }) => {
const router = Router()
/* GET users listing. */
router
.get('/', function (req, res, next) {
res.render('women', { title: 'Women Page' })
})
.post('/', (req, res) => {
io.emit('women-message', { ...req.body, id: 'women' })
res.json(req.body)
})
return router
}
routes/io-women.js
module.exports = ({ Router, io }) => {
const router = Router()
/* GET users listing. */
router
.get('/', function (req, res, next) {
res.render('girls', { title: 'Girls Page' })
})
.post('/', (req, res) => {
io.emit('girls-message', { ...req.body, id: 'girls' })
res.json(req.body)
})
return router
}
routes/io-girls.js
module.exports = ({ express, app, io }) => {
const { Router } = express
app.use('/women', require('./routes/io-women')({ Router, io }))
app.use('/girls', require('./routes/io-girls')({ Router, io }))
io.on('connection', socket => {
console.log('Socket Connect:', { id: socket.id })
socket.on('disconnect', message => {
console.log({ id: socket.id, message })
})
socket.on('women-message', msg => {
console.log('women-message:', msg)
io.emit('women-message', { id: socket.id, message: msg })
})
socket.on('girls-message', msg => {
console.log('girls-message:', msg)
io.emit('girls-message', { id: socket.id, message: msg })
})
})
return app
}
app.js
Style
Layout
<link rel="stylesheet/less" type="text/less" href="less/u-box.less" />
<style rel="stylesheet/less" type="text/less">
body {
margin: 0;
padding: 0;
}
.app-tpl {
height: ~"calc(100vh)";
overflow-y: auto;
overflow-x: hidden;
background-color: rgb(248, 239, 201);
}
.messages {
list-style: none;
margin: 0;
padding: 0;
li {
&:nth-child(odd) {
background-color: yellow;
}
}
}
</style>
<script src="//cdnjs.cloudflare.com/ajax/libs/less.js/2.7.1/less.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script type="text/html" id="app-tpl">
<div class="app-tpl u-box u-box--column">
<h1>{{title}}</h1>
<main class="u-flex u-box u-box--column">
<ul class="u-flex messages">
<li>Welcome</li>
<li v-for="msg in messages"><strong v-text="msg.id" />: \{{msg.message}}</li>
</ul>
</main>
<footer>
<form class="u-box" @submit="onSubmit">
<input class="u-flex" v-model="formData.message" type="text" placeholder="Message" />
<button type="submit">Send</button>
</form>
</footer>
</div>
</script>
<script>
var socket = io(),
el = document.currentScript
const storeMessages = {
state: Vue.observable({
messages: []
}),
methods: {
onMessage(message) {
this.state.messages.push(message)
}
}
}
const App = new Vue({
template: '#app-tpl',
data: () => ({
formData: {
message: ''
}
}),
computed: {
messages() {
return storeMessages.state.messages || []
}
},
methods: {
onSubmit(event) {
event.preventDefault()
const { formData } = this
socket.emit('women-message', formData.message)
console.log('message:', formData.message)
formData.message = ''
}
}
})
App.$mount(el)
socket.on('women-message', storeMessages.methods.onMessage.bind(storeMessages))
</script>
views/women.hbs
<link rel="stylesheet/less" type="text/less" href="less/u-box.less" />
<style rel="stylesheet/less" type="text/less">
body {
margin: 0;
padding: 0;
}
.app-tpl {
height: ~"calc(100vh)";
overflow-y: auto;
overflow-x: hidden;
background-color: rgb(248, 239, 201);
}
.messages {
list-style: none;
margin: 0;
padding: 0;
li {
&:nth-child(odd) {
background-color: yellow;
}
}
}
</style>
<script src="//cdnjs.cloudflare.com/ajax/libs/less.js/2.7.1/less.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script type="text/html" id="app-tpl">
<div class="app-tpl u-box u-box--column">
<h1>{{title}}</h1>
<main class="u-flex u-box u-box--column">
<ul class="u-flex messages">
<li>Welcome</li>
<li v-for="msg in messages"><strong v-text="msg.id" />: \{{msg.message}}</li>
</ul>
</main>
<footer>
<form class="u-box" @submit="onSubmit">
<input class="u-flex" v-model="formData.message" type="text" placeholder="Message" />
<button type="submit">Send</button>
</form>
</footer>
</div>
</script>
<script>
var socket = io(),
el = document.currentScript
const storeMessages = {
state: Vue.observable({
messages: []
}),
methods: {
onMessage(message) {
this.state.messages.push(message)
}
}
}
const App = new Vue({
template: '#app-tpl',
data: () => ({
formData: {
message: ''
}
}),
computed: {
messages() {
return storeMessages.state.messages || []
}
},
methods: {
onSubmit(event) {
event.preventDefault()
const { formData } = this
socket.emit('girls-message', formData.message)
console.log('message:', formData.message)
formData.message = ''
}
}
})
App.$mount(el)
socket.on('girls-message', storeMessages.methods.onMessage.bind(storeMessages))
</script>
views/girls.hbs
Curl POST
curl --header "Content-Type: application/json" \
--request POST \
--data '{"message":"Women Bla Bla Bla"}' \
http://localhost:3000/women
curl-women.sh
terminal:chmod +x curl-women.sh
terminal:./curl-women.sh
curl --header "Content-Type: application/json" \
--request POST \
--data '{"message":"Girls Bla"}' \
http://localhost:3000/girls
curl-girls.sh
terminal:chmod +x curl-girls.sh
terminal:./curl-girls.sh
Open Pages
Manual: Routes
- Open two pages: Women and Girls in the browser
- Use terminal to POST a JSON data with curl, for example:
./curl-women.sh
- And see new message appear in the chat.