Front-end of koa+mysql+vue+socket.io stack development

Keywords: Javascript socket Vue React Webpack

The comparison between React and Vue is a hot topic in the front end.

  • Vue scaffolding, as well as the official provision of essential basic components, such as vuex, vue-router, are really friendly to novices; react gives these to the community to do, although this enlarges the ecological chain of react, but novices have a lot of trouble to come up with a plan to take advantage of, but fortunately there are many similar dva program.

  • One of the most delightful things about vue is that its two-way flow of data is particularly convenient in form development, and react is much more troublesome in this respect.

  • But vue's complex api is a big headache, with dozens of pages of documentation alone. Too much grammar, too many magic symbols, is the biggest obstacle to starting this framework for the faster and faster evolution of the front-end.

  • On the contrary, the number of react APIs is negligible, and it takes at most a few hours to read the official documents. You only need to understand JavaScript to understand a lot of react behavior. Many uses of react, its api is intuitive, and your guess about its use is basically inseparable, which greatly reduces the mental burden.

  • In addition, react's jsx grammar is more expressive, and hoc and hooks make the code easier to organize and reuse.

Although I prefer React, the need for work is not what you need, so this demo is the prelude to exploring Vue.

I've used Vue before, and remember that it's version 1.0. At that time, the trend was similar to the mvvm scheme of angular 1.x, with data flowing in two directions. At that time, the Vue was far less hot than it is now, and there were fewer components, no vue-router, no vuex, and the communication before the component was just too painful. Now vue 2.x has undergone tremendous changes compared with before, Vue is also moving towards react, and I can only start from scratch.

There's a bit of gossip going on, so let's get to the point.

Project configuration

Choose webpack 4 to package and manage. The template engine uses pug. The precompiled css is scss.

Configuration of webpack.common.js

// webpack.common.js
module.exports = {
    entry: './src/main.js',
    output: {
        path: resolve(__dirname, 'dist'),
        filename: '[name]-[hash].js'//Add hash to output file
    },
    optimization: { // Instead of commonchunk, code splitting
        runtimeChunk: 'single',
        splitChunks: {
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all'
                }
            }
        }
    },
    module: {
        rules: [
            {
                test:/\.vue$/,
                exclude: /node_modules/,
                use:['vue-loader']
            },
            {
                test: /\.js?$/,
                exclude: /node_modules/,
                use: ['babel-loader']//'eslint-loader'
            },
            {
                test: /\.pug$/,
                use: ['pug-plain-loader']
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.scss$/,
                use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
            },
            {   
                test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 1000
                    }
                }]
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
        new CleanWebpackPlugin([resolve(__dirname, 'dist')]),//When a new file is generated, the directory is emptied
        new HtmlWebpackPlugin({
            template: './public/index.html',//Template path
            filename: 'index.html',//Generated file name, default index.html
            favicon: './public/favicon.ico',
            minify: {
                removeAttributeQuotes:true,
                removeComments: true,
                collapseWhitespace: true,
                removeScriptTypeAttributes:true,
                removeStyleLinkTypeAttributes:true
             }
        }),
        new HotModuleReplacementPlugin()//HMR
    ]
};

Configuration of webpack.dev.js

It is to develop the configuration of devServer server and monitor code changes.

// webpack.dev.js
module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist',
        index:'index.html',
        port: 3002,
        compress: true,
        historyApiFallback: true,
        hot: true
    }
});

Configuration of babel.config.js

module.exports = {
  presets: [
    [
      '@vue/app', {
        "useBuiltIns": "entry"
      }
    ]
  ]
}

directory structure

public #Public directory
server #Backend directory
src    #Front end directory
├── assets #Static File Directory
├── common #Tools Directory
├── components #Component directory
├── store   # vuex store directory
├── App.vue # Root components
├── main.js # Entry file
└── router.js #Route    

Entry and routing

Routing file

Nested routing is used below, which is history-based or hashchange-based.

import Vue from 'vue'
import Router from 'vue-router'
//...

Vue.use(Router)

//Route
const routes = [{
    path: '/',
    name: 'home',
    component: Index
},{
    path: '/sign',
    name: 'sign',
    component: Sign,
    children: [ //Nested Route
        {
            path: "log",
            name: "login",
            component: Login
        },
        {
            path: "reg",
            name: "register",
            component: Register
        },
        { path: '*', redirect: 'log' }
    ]
}, { path: '*', redirect: '/' }]

export default new Router({
    mode: "history",
    routes
})

Entry file

Combine router, store and root components

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '../public/base.min.css'
import '../public/fontello.css'

Vue.config.productionTip = false
new Vue({
    router,
    store,
    render: h => h(App),
}).$mount('#app')

Module Writing

Template, logical code, and style synthesis into a page is also an aspect of my appreciation of vue, because you don't need to switch between multiple files repeatedly.

Template template

pug is the previous jade. Its simplicity will make the template clearer under complex pages. At the very least, it will make you knock less code. Here's an example of some code on index pages.

<template lang="pug">
div.content
    div.bar
        header(v-drag)
            div.avatar(v-on:click="profile(selfInfo)")
                img(:src="selfInfo.avatar? selfInfo.avatar: aPic.src") 
            div.name {{ selfInfo.nick }}
                p {{ selfInfo.signature}}
            i.icon-logout(v-on:click="logout")
        div.body
            div.main-panel(v-if="!isSearch")        
                nav
                    div(v-on:click="showTab(0)" :class="{active:tabIndex==0}") Good friend
                    div(v-on:click="showTab(1)" :class="{active:tabIndex==1}") Grouping
                    div(v-on:click="showTab(2)" :class="{active:tabIndex==2}") news
                        span(v-if="dealCount") {{dealCount}}    
                ul.friends(v-if="tabIndex == 0")
                    li(v-for="item in friends" :key="item.id")
                        div.avatar(v-on:click="profile(item)")
                            img(:src="item.avatar? item.avatar: aPic.src") 
                        p(v-on:click="chatWin(item)") {{item.nick}}
                        span(v-if="item.reads && item.reads > 0") ({{item.reads}})
        //Dynamic Creation of Components
    component(:is="item.component"  v-for="(item,i) in wins" :key="item.id" 
        :info="item.info"
        :sty="item.sty"
        :msgs="item.msgs"
        v-on:close="closeWin(i)"
        v-on:setZ="setZ(i)")
</template>

Dynamic Creation of Components

What do you mean by using the concept of dynamic component creation in vue? This component does not exist on the current page, and we need to trigger it before we can start creating it. For example, when you click on a button, you start loading the creation component and then fill it into the page. The following is the compilation of related functions of dynamic components.

data() {
    return {
       wins: [] //Component list
    }
},
methods: {  
  addWin(info, com) { // Method of adding components
      this.wins.push({
          msgs: info.msgs || [],
          info,
          sty: {
              left: l * 30 + 270,
              top: l * 30 + 30,
              z: 0
          },
          component: com
      });
  }
}  

//Stuffing assembly
component(:is="item.component"  v-for="(item,i) in wins" :key="item.id" 
  :info="item.info"
  :sty="item.sty"
  :msgs="item.msgs"
  v-on:close="closeWin(i)"
  v-on:setZ="setZ(i)")

javascript section

Here's the business logic part. Take part of the code as an example. Specific parts refer to official documents.

<script>
import { mapState, mapGetters } from "vuex";
import ChatMsg from "./ChatMsg.vue";
import Profile from "./Profile.vue";
import { get, post } from "../common/request";

export default {
    name: "index",
    data() {
        return {
            tabIndex: 0,
            wins: [],
            aPic: {
                src: require("../assets/avatar.jpg")
            }
        };
    },
    async created() {
        //...
    },
    computed: {
        ...mapState(["selfInfo"]),
        ...mapGetters([
            "isLogin",
            "friends",
            "msgs"
        ])
    },
    watch: {
        isLogin: {
            //Monitor login status
            handler: function(val, old) {
                            //...
            }
            // immediate: true // Enter component immediately executes once
        }
    },
    methods: {
        addWin(info, com) {},
      sendMsg(user,data){}
      //...
      }
}
</script>

Part style

Use vue default scoped, of course, the most perfect solution is css-module, the configuration is more complex, of course, depending on your project requirements. The precompiler uses scss, which I think is more powerful and convenient.

<style lang="scss" scoped>
$blue: hsl(200, 100%, 45%);
@mixin nowrap {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.content {
    height: 100%;
    width: 1000px;
    margin: 0 auto;
    position: relative;
}
.main-panel {
    width: 100%;
}
.search-panel {
    width: 100%;
    min-height: 313px;
    max-height: 513px;
    li {
        line-height: 2;
    }
}
.bar {
    position: absolute;
    top: 30px;
    width: 250px;
    background-color: #fff;
    user-select: none;
    box-shadow: 0 6px 20px 0 hsla(0, 0%, 0%, 0.19),
        0 8px 17px 0 hsla(0, 0%, 0%, 0.2);
    header {
        display: flex;
        align-items: flex-start;
        align-items: center;
        background-color: $blue;
        color: #fff;
        .avatar {
            width: 30px;
            height: 30px;
            margin: 10px;
            border: 1px solid $blue;
            border-radius: 50%;
            overflow: hidden;
            cursor: pointer;
            &:hover {
                border-color: #fff;
            }
            img {
                width: 100%;
                height: 100%;
            }
        }
    }
}
<style>

Use of vuex

Vuex is simpler and more convenient to use than Redux in react, although it may not be as "pure" as redux, but it is easy to use. Vuex encapsulates asynchronous action s directly and uses module s to distinguish the states of different components. It can be said that the store of vuex centralizes most of the business logic related to the state of the project, which is also a key point of vue project.

store

vuex stores are the same as redux stores.

import Vue from 'vue'
import Vuex from 'vuex'
import { state, mutations } from './mutations'
import * as getters from './getters'
import * as actions from './actions'
import friend from './modules/friend'
import msg from './modules/msg'

Vue.use(Vuex)

export default new Vuex.Store({
    actions,
    getters,
    state,
    mutations,
    modules: {
        friend,
        msg
    }
})

Global state and mutations

The state in vuex corresponds to the state of redux, while mutations are similar to action s in redux, where mutations are synchronous.

export const state = {
    loginInfo: { token },
    selfInfo: selfInfo,
    dialog: { txt: 'content', cancal: false, callback: () => { }, show: false }
}

export const mutations = {
    showDialog(state, payload) {
        state.modal.visible = true;
        state.dialog = Object.assign({}, state.dialog, payload);
        state.dialog.show = true;
    },
    closeDialog(state) {
        state.modal.visible = false;
        state.dialog.show = false;
    },
    setLoginInfo(state) {
        state.loginInfo = { token: localStorage.getItem("token") };
    },
    setSelfInfo(state, payload) {
        state.selfInfo = payload;
        localStorage.setItem("selfInfo", JSON.stringify(payload));
    },
    logout() {
        state.loginInfo = {};
        state.selfInfo = {};
        localStorage.clear();
    }
}

Global action s and getters

vuex's aciton encapsulates asynchronous actions. Redux has to use middleware such as redux-saga to achieve similar results.

import { get, post } from "../common/request";

export const getInfo = ({ commit }) => {
    return  get("/getinfo").then(res => {
        if (res.code == 0) {
            commit("setSelfInfo", res.data.user);
            commit("setFriends", res.data.friends);
            commit("setGroup", res.data.groups);
            commit("setMsgs", res.data.msgs);
        } else if (res.code == 1) {
            commit("logout");
        } else {
            commit('showDialog',{txt:res.message})
        }
    }).catch(err=>{
        commit('showDialog',{txt:err.message})
    });
}

export const updateSelf=({commit},form)=>{
    post("/updateinfo", form).then(res => {
        if (res.code == 0) {
            commit("updateSelfInfo", form);
        } else if (res.code == 1) {
            commit("logout");
        } else {
            commit('showDialog',{txt:res.message})
        }
    }).catch(err=>{
        commit('showDialog',{txt:err.message})
    });
}

getters can be seen as encapsulating certain fields in state

export const visible = state => state.modal.visible
export const isLogin = state => !!state.loginInfo.token

modules

With the expansion of project scale, splitting and modularization are inevitable. Stores set up for a sub-module have the same structure as root stores. Module stores will eventually be merged into root stores. The writing method of msg as an example is as follows:

import { get, post } from "../../common/request";

export default {
    state: {
        msgs: []
    },
    getters: {
        msgs: state => state.msgs,
        dealCount: state => state.msgs.filter(i => i.status == 0).length
    },
    actions: {
        accept({ commit }, form) {
            return post("/accept", { id: form.id, friend_id: form.from_id }).then(res => {
                if (res.code == 0) {
                    commit("setMsgState", { id: form.id, status: 1 });
                    commit("addFriend", Object.assign({}, form, { id: form.from_id }));
                } else {
                    commit('showDialog',{txt:res.message})
                }
            }).catch(err=>{
                commit('showDialog',{txt:err.message})
            });
        },
        reject({ commit }, form) {
            post("/reject", { id: form.id }).then(res => {
                if (res.code == 0) {
                    form.status = 2;
                    commit("setMsgState", form);
                } else {
                    commit('showDialog',{txt:res.message})
                }
            }).catch(err=>{
                commit('showDialog',{txt:err.message})
            });
        }
    },
    mutations: {
        setMsgs(state, payload) {
            state.msgs = payload;
        },
        setMsgState(state, payload) {
            state.msgs.forEach(i => {
                if (i.id == payload.id) {
                    i.status = payload.status;
                }
            })
        },
        addMsg(state, payload) {
            state.msgs.unshift(payload);
        }
    }
}

socket.io access

Then we use websocket to realize the functions of friend chat and group chat. The introduction of socket.io can see my previous articles. On the Use of socket.io.

Client

First connect the socket of the server, and then register the user information to the socket.io service, so that the server knows who you are and can communicate with others.

async created() {// Establishing socket connection when vue component is created
  const token = localStorage.getItem("token") || "";
  if (!token) {
        return this.$router.push("/sign/log");
  }
  await this.$store.dispatch("getInfo");
  this.socket = io("http://localhost:3001?token=" + token);

  //Communicate with server only after registering user information
  this.socket.emit("sign", { user: this.selfInfo, rooms }, res => {
    // console.log(res);
    this.$store.commit("friendStatus", res.data);
    this.socket.on("userin", (map, user) => {
      this.$store.commit("friendStatus", map);
      showTip(user, "Online.");
    });
    this.socket.on("userout", (map, user) => {
      this.$store.commit("friendStatus", map);
      showTip(user, "Offline.");
    });

    this.socket.on("auth", data => {
      this.$store.commit('showDialog',{txt:data.message})
      this.$store.commit("logout");
    });

    //Receiving application friends and groups
    this.socket.on("apply", data => {
      this.$store.commit("addMsg", data);
    });

    //Receiving chat information
    this.socket.on("reply", (user, data) => {
      this.sendMsg(user, data);
    });

    //Receiving Group Chat Information
    this.socket.on("groupReply", (info, data) => {
      this.sendGroupMsg(info, data);
    });
  });
},
beforeDestroy() { //Close the socket before the component is destroyed
    this.socket.close();
},

Server side

socket.io corresponds to the server-side part, the logic mainly includes user registration, two-person chat, group chat, of course, the corresponding information needs to be saved to the database. The trick here is to use variables to record the information of all current logged-in users.

const auth = require('./auth.js')
const { insertMsg, insertToUser } = require('../daos/message');
const log = require('../common/logger')

let MAP = {};//User id and socket id
let LIST = []; //User information
let ROOMS = []; //Room

const currTime = () => {
    const d = new Date(), date = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
    return ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2) + ':' + ('0' + d.getSeconds()).slice(-2);
};

module.exports = io => {
    // middleware
    io.use(auth);
    //namespace (/)
    io.on('connection', socket => {
        socket.emit('open', {
            code: 0,
            handshake: socket.handshake,
            namespace: '/',
            message: 'welcome to main channel, please sign'
        });
                
        //User registration
        socket.on('sign', ({ user, rooms }, fn) => {
            if (!user.id) {
                return fn({ code: 2, message: 'id not exist' });
            }
            MAP[user.id] = socket.id;
            user.socketId = socket.id;
            LIST.push(user);

            socket.join(rooms);//Join your group
            socket.emit('userin', MAP, user);
            socket.broadcast.emit('userin', MAP, user);

            fn({
                code: 0,
                message: 'sign success',
                data: MAP
            });
        });

        //Two people chatting
        socket.on('send', async (uid, msg) => {
            const sid = MAP[uid];//Receiving user socket.id
            const cid = findUid(socket.id);//Send user id

            if (sid) { // Friends send online
                socket.to(sid).emit('reply', { id: cid, self: false }, { date: currTime(), msg });
            }
            // Send a copy to yourself
            socket.emit('reply', { id: uid, self: true }, { date: currTime(), msg });
            // Save the database
            try {
                const ret = await insertMsg({ send_id: cid, receive_id: uid, content: msg });
                insertToUser({ user_id: uid, send_id: cid, message_id: ret.insertId, is_read: sid ? 1 : 0 });
            } catch (err) {
                log.error(err);
            }
        });

        //Group Chat
        socket.on('groupSend', async ({gid,user}, msg) => {
                    //...
        });

        socket.on('acceptFriend', (uid) => {
                    //...
        });

        socket.on('sendApply', (uid, data) => {
                    //...
        });

        socket.on('disconnect', () => {
                    //...
        });
    });
};

Client Startup

First of all, we have to write client.js to start the front-end service, still using our efficient koa framework. It's easy for me to figure out here. Under the same root directory as the previous server, the real project will separate the server end and the client end into different directories or servers.

const koa = require('koa')
const app = new koa()
const static = require('koa-static')
const compress = require('koa-compress')
const router = require('koa-router')()
const { clientPort } = require('./server/config/app')
const tpl = require('./server/middleware/tpl')
const path = require('path')

// gzip
app.use(compress({
    filter: function (content_type) {
        return /text|javascript/i.test(content_type)
    },
    threshold: 2048,
    flush: require('zlib').Z_SYNC_FLUSH
}));

// set static directiory
app.use(static(path.join(__dirname, 'dist'), { index: false }));

// simple template engine
app.use(tpl({
    path: path.join(__dirname, 'dist')
}));

// add routers
router
    .get('/', ctx => {
        ctx.render('index.html');
    })
    .get('/sign/*', ctx => {
        ctx.redirect('/');
    })

app.use(router.routes())
    .use(router.allowedMethods());

// deal 404
app.use(async (ctx, next) => {
    ctx.status = 404;
    ctx.body = { code: 404, message: '404! not found !' };
});

// koa already had event to deal with the error, just rigister it
app.on('error', (err, ctx) => {
    ctx.status = 500;
    ctx.statusText = 'Internal Server Error';
    if (ctx.app.env === 'development') { //throw the error to frontEnd when in the develop mode
        ctx.res.end(err.stack); //finish the response
    } else {
        ctx.body = { code: -1, message: 'Server Error' };
    }
});

if (!module.parent) {
    app.listen(clientPort);
    console.log('app server running at: http://localhost:%d', clientPort);
}

Starting the server and client, we can run the entire demo, mainly to achieve the following functions:

  1. All windows on the main page can be dragged and closed.
  2. You can edit user information, group information, and each user can create three new groups.
  3. Can chat with friends, group chat
  4. Search Users and Groups
  5. Friend Application and Group Application
  6. Online, you can get friends online and offline reminders, real-time answer multiplexer applications
  7. When you are offline, you can still leave messages to users and groups. Next time you log in, you can get a reminder.

Follow-up

Next, I think of the following points:

  1. Using nuxt to render vue on server to further improve performance
  2. The node section is deployed using pm2.

Source code: vue_qq

Posted by ealderton on Sat, 04 May 2019 15:10:38 -0700