Post

Bootstrap socket.io

Harianto van Insulinde

This is bootstrap continuation from: Getting Started ExpressJS 2020
Alternative way for passing argument instead of global variable.

NPM install

bash
# install socket.io
npm i -S socket.io

Pass io as argument

app.js

js
module.exports = ({ express, app, io }) => {
    return app
}

Snippet: app.js

Becomes

js
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

js
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

js
#!/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

bash
git add app.js bin/www
git commit -m "Pass io as argument"

Integrating socket.io

js
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

handlebars
<script src="/socket.io/socket.io.js"></script>
<script>
    var socket = io();
</script>

Snippet: views/partials/script.hbs

Git commit: Integrating socket.io

bash
git add app.js views/partials/script.hbs
git commit -m "Integrating socket.io"

Optional Templates Examples

Template: Chat

u-box.less

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

handlebars
<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

js
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

js
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

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

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

handlebars
<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

handlebars
<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

bash
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

bash
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