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:
- All windows on the main page can be dragged and closed.
- You can edit user information, group information, and each user can create three new groups.
- Can chat with friends, group chat
- Search Users and Groups
- Friend Application and Group Application
- Online, you can get friends online and offline reminders, real-time answer multiplexer applications
- 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:
- Using nuxt to render vue on server to further improve performance
- The node section is deployed using pm2.
Source code: vue_qq