koa+mysql+vue+socket.io全棧開(kāi)發(fā)之前端篇

原文地址:koa+mysql+vue+socket.io全棧開(kāi)發(fā)之前端篇

ReactVue 之間的對(duì)比渊季,是前端的一大熱門(mén)話(huà)題更耻。

  • vue 簡(jiǎn)易上手的腳手架测垛,以及官方提供必備的基礎(chǔ)組件,比如 vuex秧均,vue-router食侮,對(duì)新手真的比較友好;react 則把這些都交給社區(qū)去做目胡,雖然這壯大了 react 的生態(tài)鏈锯七,但新手要弄出一套趁手的方案挺麻煩的,不過(guò)好在現(xiàn)在有很多類(lèi)似 dva的方案了誉己。

  • vue 比較討喜的一點(diǎn)眉尸,就是它的數(shù)據(jù)雙向流動(dòng)在表單開(kāi)發(fā)時(shí)特別方便,而 react 在這方面可就麻煩多了巨双。

  • 但是 vue 復(fù)雜的 api 噪猾,簡(jiǎn)直讓人頭大,光是文檔說(shuō)明都幾十頁(yè)了筑累。太多的語(yǔ)法袱蜡,太多的魔法符號(hào),對(duì)進(jìn)化速度越來(lái)越快的前端屆來(lái)說(shuō)慢宗,就是入手這個(gè)框架的最大阻礙坪蚁。

  • 而相反 react 的 api 數(shù)量簡(jiǎn)直可以忽略不計(jì)了,頂多花幾小時(shí)就能看完官方文檔婆廊。你只要理解 JavaScript迅细,就能理解 react 的很多行為。react 的很多用法淘邻,它的 api 都是符合直覺(jué)的茵典,你對(duì)它用法的猜測(cè)基本都是八九不離十的,這真是大大降低了心智負(fù)擔(dān)宾舅。

  • 除此之外统阿,reactjsx 語(yǔ)法表達(dá)能力更強(qiáng)彩倚,還有 hochooks 使代碼也更容易組織和復(fù)用。

雖然我更喜歡 React 扶平,但工作上的需求帆离,還不是要你用什么你就得用什么??,所以這個(gè) demo 就當(dāng)是探索 Vue 的前奏结澄。

之前我還是有用過(guò) vue 的哥谷,記得還是 1.0 版本,當(dāng)時(shí)的潮流就是類(lèi)似 angular 1.x 的 mvvm 方案麻献,數(shù)據(jù)雙向流動(dòng)们妥。那時(shí)的 vue 遠(yuǎn)沒(méi)有現(xiàn)在的熱度,組件也少勉吻,沒(méi)有 vue-router监婶,沒(méi)有 vuex,組件之前的通信簡(jiǎn)直太痛苦了〕萏遥現(xiàn)在 vue 2.x 比起之前惑惶,已經(jīng)發(fā)生了天翻地覆的變化,vue 也在不斷向 react 靠攏短纵,而我也只能從頭開(kāi)始學(xué)起带污。

閑話(huà)說(shuō)得有點(diǎn)多,還是趕緊進(jìn)入主題吧

項(xiàng)目配置

選擇 webpack 4 打包和管理踩娘,template 引擎使用 pug 刮刑,css 預(yù)編譯是 scss。

webpack.common.js 的配置

// webpack.common.js
module.exports = {
    entry: './src/main.js',
    output: {
        path: resolve(__dirname, 'dist'),
        filename: '[name]-[hash].js'//輸出文件添加hash
    },
    optimization: { // 代替commonchunk, 代碼分割
        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')]),//生成新文件時(shí)养渴,清空生出目錄
        new HtmlWebpackPlugin({
            template: './public/index.html',//模版路徑
            filename: 'index.html',//生成后的文件名,默認(rèn)index.html
            favicon: './public/favicon.ico',
            minify: {
                removeAttributeQuotes:true,
                removeComments: true,
                collapseWhitespace: true,
                removeScriptTypeAttributes:true,
                removeStyleLinkTypeAttributes:true
             }
        }),
        new HotModuleReplacementPlugin()//HMR
    ]
};

webpack.dev.js 的配置

就是開(kāi)發(fā)服務(wù)器 devServer的配置,監(jiān)控代碼變更泛烙。

// 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
    }
});

babel.config.js 的配置

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

目錄結(jié)構(gòu)

public #公共目錄
server #后端目錄
src    #前端目錄
├── assets #靜態(tài)文件目錄
├── common #工具目錄
├── components #組件目錄
├── store   # vuex store目錄
├── App.vue # 根組件
├── main.js # 入口文件
└── router.js #路由    

入口和路由

路由文件

下面使用了嵌套路由理卑,使用的是基于 history 的路由,也可以選擇基于 hashchange的路由蔽氨。

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

Vue.use(Router)

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

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

入口文件

把router藐唠,store 和根組件組合起來(lái)

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')

模塊的編寫(xiě)

模版,邏輯代碼鹉究,樣式合成到一個(gè)頁(yè)面也是我欣賞 vue 的一個(gè)方面宇立,因?yàn)檫@樣你就不需要在多個(gè)文件之間反復(fù)的切換。

模版template

pug 就是之前的 jade自赔,它的簡(jiǎn)潔在復(fù)雜的頁(yè)面下會(huì)讓 template 清晰不少妈嘹,最起碼會(huì)讓你少敲代碼,這里以index 頁(yè)面的部分代碼為例绍妨。

<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}") 好友
                    div(v-on:click="showTab(1)" :class="{active:tabIndex==1}") 分組
                    div(v-on:click="showTab(2)" :class="{active:tabIndex==2}") 消息
                        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}})
        //動(dòng)態(tài)創(chuàng)建組件
    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>

動(dòng)態(tài)創(chuàng)建組件

上面用到了 vue 的 動(dòng)態(tài)創(chuàng)建組件 的概念润脸,什么意思呢柬脸?這個(gè)組件在當(dāng)前頁(yè)面中是不存在的,需要我們觸發(fā)之后毙驯,才開(kāi)始創(chuàng)建倒堕。比如,當(dāng)你點(diǎn)擊某個(gè)按鈕爆价,才開(kāi)始加載創(chuàng)建組件垦巴,然后填充到頁(yè)面中來(lái)。下面就是動(dòng)態(tài)組件相關(guān)功能的編寫(xiě)铭段。

data() {
    return {
       wins: [] //組件列表
    }
},
methods: {  
  addWin(info, com) { // 添加組件的方法
      this.wins.push({
          msgs: info.msgs || [],
          info,
          sty: {
              left: l * 30 + 270,
              top: l * 30 + 30,
              z: 0
          },
          component: com
      });
  }
}  

//填充組件
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部分

這里就是業(yè)務(wù)邏輯的部分了魂那,以部分代碼為例, 具體的部分參考官方的文檔

<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: {
            //監(jiān)聽(tīng)登錄狀態(tài)
            handler: function(val, old) {
                            //...
            }
            // ,immediate: true //進(jìn)入組件立即執(zhí)行一次
        }
    },
    methods: {
        addWin(info, com) {},
      sendMsg(user,data){}
      //...
      }
}
</script>

style部分

使用了 vue 默認(rèn)的 scoped ,當(dāng)然最完善的方案是 css-module稠项,配置要復(fù)雜一些涯雅,當(dāng)然這要看你項(xiàng)目需求。預(yù)編譯器使用的是 scss展运,個(gè)人認(rèn)為比較強(qiáng)大和方便活逆。

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

vuex的使用

vuex 相比 react 中的 redux,使用起來(lái)也更加簡(jiǎn)單和方便拗胜,盡管相比 redux 可能沒(méi)有那么 "純"蔗候,但好用就行。 vuex 直接把異步的 action 封裝進(jìn)里面埂软,使用module將不同組件的狀態(tài)區(qū)分開(kāi)來(lái)锈遥。可以說(shuō) vuex 的 store 集中了 項(xiàng)目大部分與 狀態(tài)相關(guān)的業(yè)務(wù)邏輯勘畔,這也是 vue 項(xiàng)目的一大關(guān)鍵點(diǎn)所灸。

store

vuex 的 store 和 redux 的 store 一樣。

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
    }
})

全局 state 和 mutations

vuex 中的 state 對(duì)應(yīng) reduxstate炫七,mutations 則類(lèi)似 redux 中的 action爬立,其中mutations是同步的。

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();
    }
}

全局 action 和 getters

vuexaciton 就是將異步的動(dòng)作封裝起來(lái)万哪。而redux 得通過(guò) redux-saga 之類(lèi)的中間件才能實(shí)現(xiàn)類(lèi)似的效果侠驯。

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可以看成是對(duì)state 中某些字段的封裝

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

modules

隨著項(xiàng)目規(guī)模的擴(kuò)展,拆分和模塊化都是一個(gè)必然奕巍。針對(duì)某個(gè)子模塊而設(shè)置的store吟策,它的結(jié)構(gòu)和根store一樣,module 的 store 最終會(huì)合并到根 store里面的止。msg為例的編寫(xiě)方式如下:

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的接入

接著將websocket使用起來(lái)檩坚,讓我們實(shí)現(xiàn) 好友聊天和分組聊天的功能,socket.io 的介紹可以看我之前的文章 關(guān)于socket.io的使用

客戶(hù)端

首先連接服務(wù)端的 socket效床,然后將自身的用戶(hù)信息注冊(cè)到 socket.io 服務(wù)睹酌,這樣服務(wù)端才知道你是誰(shuí),也才能與其他人實(shí)行通信剩檀。

async created() {// vue 組件創(chuàng)建時(shí)建立socket連接
  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);

  //注冊(cè)用戶(hù)信息后才開(kāi)始與服務(wù)端通信
  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, "上線了");
    });
    this.socket.on("userout", (map, user) => {
      this.$store.commit("friendStatus", map);
      showTip(user, "下線了");
    });

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

    //接收申請(qǐng)好友和組群
    this.socket.on("apply", data => {
      this.$store.commit("addMsg", data);
    });

    //接收聊天信息
    this.socket.on("reply", (user, data) => {
      this.sendMsg(user, data);
    });

    //接收群組聊天信息
    this.socket.on("groupReply", (info, data) => {
      this.sendGroupMsg(info, data);
    });
  });
},
beforeDestroy() { //組件銷(xiāo)毀之前憋沿,將socket 關(guān)閉
    this.socket.close();
},

服務(wù)端

socket.io 對(duì)應(yīng)的服務(wù)端部分,邏輯主要包括用戶(hù)注冊(cè)沪猴,兩人聊天辐啄,群聊天,當(dāng)然對(duì)應(yīng)的信息需要保存到數(shù)據(jù)庫(kù)运嗜。 這里的技巧就是使用變量記錄當(dāng)前所有登錄用戶(hù)的信息壶辜。

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

let MAP = {};//用戶(hù)id和socket id
let LIST = []; //用戶(hù)信息
let ROOMS = []; //房間

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'
        });
                
        //用戶(hù)注冊(cè)
        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);//加入自己所在的組
            socket.emit('userin', MAP, user);
            socket.broadcast.emit('userin', MAP, user);

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

        //兩人聊天
        socket.on('send', async (uid, msg) => {
            const sid = MAP[uid];//接收用戶(hù)socket.id
            const cid = findUid(socket.id);//發(fā)送用戶(hù)id

            if (sid) { // 好友在線則發(fā)送
                socket.to(sid).emit('reply', { id: cid, self: false }, { date: currTime(), msg });
            }
            // 給自己也發(fā)一份
            socket.emit('reply', { id: uid, self: true }, { date: currTime(), msg });
            // 保存數(shù)據(jù)庫(kù)
            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);
            }
        });

        //群組聊天
        socket.on('groupSend', async ({gid,user}, msg) => {
                    //...
        });

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

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

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

客戶(hù)端的啟動(dòng)

首先得編寫(xiě)client.js,將前端服務(wù)啟動(dòng)起來(lái)担租,依然還是使用我們高效的koa框架砸民。我這里圖省事,和之前的服務(wù)端所在同一個(gè)根目錄下奋救,真正項(xiàng)目會(huì)將服務(wù)端部分和客戶(hù)端部分 分離到不同目錄或不同的服務(wù)器的岭参。

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);
}

啟動(dòng)服務(wù)端和客戶(hù)端,我們整個(gè)demo就能運(yùn)行尝艘,主要實(shí)現(xiàn)如下功能點(diǎn):

  1. 主頁(yè)面的所有的窗口都可以拖動(dòng)演侯,關(guān)閉
  2. 可以編輯用戶(hù)信息,群組信息背亥,每個(gè)用戶(hù)可以新建3個(gè)群組
  3. 可以好友聊天秒际,群組聊天
  4. 搜索用戶(hù)和群組
  5. 好友申請(qǐng)和群組申請(qǐng)
  6. 在線時(shí),可以獲得好友上線下線提醒狡汉,實(shí)時(shí)答復(fù)用戶(hù)申請(qǐng)
  7. 離線時(shí)娄徊,仍然可以給用戶(hù)和群組留言,下次登錄獲得提醒
web QQ

后續(xù)

接下來(lái)可以?xún)?yōu)化和增強(qiáng)的地方轴猎,我想到以下幾點(diǎn):

  1. 使用 nuxt 將 vue 進(jìn)行服務(wù)端渲染 嵌莉,進(jìn)一步提高性能
  2. node 部分,使用 pm2 進(jìn)行部署捻脖。

源代碼: vue_qq

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市中鼠,隨后出現(xiàn)的幾起案子可婶,更是在濱河造成了極大的恐慌,老刑警劉巖援雇,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矛渴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)具温,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)蚕涤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人铣猩,你說(shuō)我怎么就攤上這事揖铜。” “怎么了达皿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵天吓,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我峦椰,道長(zhǎng)龄寞,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任汤功,我火速辦了婚禮物邑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘滔金。我一直安慰自己色解,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布鹦蠕。 她就那樣靜靜地躺著冒签,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钟病。 梳的紋絲不亂的頭發(fā)上萧恕,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音肠阱,去河邊找鬼票唆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛屹徘,可吹牛的內(nèi)容都是我干的走趋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼噪伊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼簿煌!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起鉴吹,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤姨伟,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后豆励,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體夺荒,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了技扼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伍玖。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖剿吻,靈堂內(nèi)的尸體忽然破棺而出窍箍,到底是詐尸還是另有隱情,我是刑警寧澤和橙,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布仔燕,位于F島的核電站,受9級(jí)特大地震影響魔招,放射性物質(zhì)發(fā)生泄漏晰搀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一办斑、第九天 我趴在偏房一處隱蔽的房頂上張望外恕。 院中可真熱鬧,春花似錦乡翅、人聲如沸鳞疲。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)尚洽。三九已至,卻和暖如春靶累,著一層夾襖步出監(jiān)牢的瞬間腺毫,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工挣柬, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留潮酒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓邪蛔,卻偏偏與公主長(zhǎng)得像急黎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子侧到,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容