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
}
Snippet: 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
}
File: 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 })
Snippet: 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)
}
File: 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
}
Snippet: app.js
<script src="/socket.io/socket.io.js"></script>
<script>
	var socket = io();
</script>
Snippet: 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;
	}
}
File: 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>
File: 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
}
Snippet: 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
}
File: 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
}
File: 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
}
Snippet: 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>
File: 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>
File: views/girls.hbs

Curl POST

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"message":"Women Bla Bla Bla"}' \
  http://localhost:3000/women
File: 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
File: 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.

Sources