React 与 vue 之间的对比, 是前端的一大热门话题.
vue 简易上手的脚手架, 以及官方提供必备的基础组件, 比如 vuex,vue-router, 对新手真的比较友好; react 则把这些都交给社区去做, 虽然这壮大了 react 的生态链, 但新手要弄出一套趁手的方案挺麻烦的, 不过好在现在有很多类似 dva 的方案了.
vue 比较讨喜的一点, 就是它的数据双向流动在表单开发时特别方便, 而 react 在这方面可就麻烦多了.
但是 vue 复杂的 API , 简直让人头大, 光是文档说明都几十页了. 太多的语法, 太多的魔法符号, 对进化速度越来越快的前端届来说, 就是入手这个框架的最大阻碍.
而相反 react 的 API 数量简直可以忽略不计了, 顶多花几小时就能看完官方文档. 你只要理解 JavaScript, 就能理解 react 的很多行为. react 的很多用法, 它的 API 都是符合直觉的, 你对它用法的猜测基本都是八九不离十的, 这真是大大降低了心智负担.
除此之外, react 的 jsx 语法表达能力更强, 还有 hoc 和 hooks 使代码也更容易组织和复用.
虽然我更喜欢 React , 但工作上的需求, 还不是要你用什么你就得用什么, 所以这个 demo 就当是探索 Vue 的前奏.
之前我还是有用过 vue 的, 记得还是 1.0 版本, 当时的潮流就是类似 angular 1.x 的 mvvm 方案, 数据双向流动. 那时的 vue 远没有现在的热度, 组件也少, 没有 vue-router, 没有 vuex, 组件之前的通信简直太痛苦了. 现在 vue 2.x 比起之前, 已经发生了天翻地覆的变化, vue 也在不断向 react 靠拢, 而我也只能从头开始学起.
闲话说得有点多, 还是赶紧进入主题吧
项目配置
选择 webpack 4 打包和管理, template 引擎使用 pug ,CSS 预编译是 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')]),// 生成新文件时, 清空生出目录
- new htmlWebpackPlugin({
- template: './public/index.html',// 模版路径
- filename: 'index.html',// 生成后的文件名, 默认 index.HTML
- favicon: './public/favicon.ico',
- minify: {
- removeAttributeQuotes:true,
- removeComments: true,
- collapseWhitespace: true,
- removeScriptTypeAttributes:true,
- removeStyleLinkTypeAttributes:true
- }
- }),
- new HotModuleReplacementPlugin()//HMR
- ]
- };
webpack.dev.JS 的配置
就是开发服务器 devServer 的配置, 监控代码变更.
- // 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"
- }
- ]
- ]
- }
目录结构
- public #公共目录
- server #后端目录
- src #前端目录
├── assets #静态文件目录
├── 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 和根组件组合起来
- 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')
模块的编写
模版, 逻辑代码, 样式合成到一个页面也是我欣赏 vue 的一个方面, 因为这样你就不需要在多个文件之间反复的切换.
模版 template
pug 就是之前的 jade, 它的简洁在复杂的页面下会让 template 清晰不少, 最起码会让你少敲代码, 这里以 index 页面的部分代码为例.
- <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}})
- // 动态创建组件
- 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>
动态创建组件
上面用到了 vue 的 动态创建组件 的概念, 什么意思呢? 这个组件在当前页面中是不存在的, 需要我们触发之后, 才开始创建. 比如, 当你点击某个按钮, 才开始加载创建组件, 然后填充到页面中来. 下面就是动态组件相关功能的编写.
- 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 部分
这里就是业务逻辑的部分了, 以部分代码为例, 具体的部分参考官方的文档
- <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: {
- // 监听登录状态
- handler: function(val, old) {
- //...
- }
- // ,immediate: true // 进入组件立即执行一次
- }
- },
- methods: {
- addWin(info, com) {},
- sendMsg(user,data){}
- //...
- }
- }
- </script>
style 部分
使用了 vue 默认的 scoped , 当然最完善的方案是 CSS-module, 配置要复杂一些, 当然这要看你项目需求. 预编译器使用的是 SCSS, 个人认为比较强大和方便.
<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, 使用起来也更加简单和方便, 尽管相比 redux 可能没有那么 "纯", 但好用就行. vuex 直接把异步的 action 封装进里面, 使用 module 将不同组件的状态区分开来. 可以说 vuex 的 store 集中了 项目大部分与 状态相关的业务逻辑, 这也是 vue 项目的一大关键点.
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 对应 redux 的 state,mutations 则类似 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
vuex 的 aciton 就是将异步的动作封装起来. 而 redux 得通过 redux-saga 之类的中间件才能实现类似的效果.
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 可以看成是对 state 中某些字段的封装
export const visible = state => state.modal.visible export const isLogin = state => !!state.loginInfo.token modules
随着项目规模的扩展, 拆分和模块化都是一个必然. 针对某个子模块而设置的 store, 它的结构和根 store 一样, module 的 store 最终会合并到根 store 里面. msg 为例的编写方式如下:
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 使用起来, 让我们实现 好友聊天和分组聊天的功能, socket.io 的介绍可以看我之前的文章 关于 socket.io 的使用.
客户端
首先连接服务端的 socket, 然后将自身的用户信息注册到 socket.io 服务, 这样服务端才知道你是谁, 也才能与其他人实行通信.
async created() {// vue 组件创建时建立 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); // 注册用户信息后才开始与服务端通信 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"); }); // 接收申请好友和组群 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() { // 组件销毁之前, 将 socket 关闭 this.socket.close(); },
服务端
socket.io 对应的服务端部分, 逻辑主要包括用户注册, 两人聊天, 群聊天, 当然对应的信息需要保存到数据库. 这里的技巧就是使用变量记录当前所有登录用户的信息.
const auth = require('./auth.js') const { insertMsg, insertToUser } = require('../daos/message'); const log = require('../common/logger') let MAP = {};// 用户 id 和 socket id let LIST = []; // 用户信息 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' }); // 用户注册 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];// 接收用户 socket.id const cid = findUid(socket.id);// 发送用户 id if (sid) { // 好友在线则发送 socket.to(sid).emit('reply', { id: cid, self: false }, { date: currTime(), msg }); } // 给自己也发一份 socket.emit('reply', { id: uid, self: true }, { date: currTime(), msg }); // 保存数据库 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', () => { //... }); }); };
客户端的启动
首先得编写 client.JS, 将前端服务启动起来, 依然还是使用我们高效的 koa 框架. 我这里图省事, 和之前的服务端所在同一个根目录下, 真正项目会将服务端部分和客户端部分 分离到不同目录或不同的服务器的.
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); }
启动服务端和客户端, 我们整个 demo 就能运行, 主要实现如下功能点:
主页面的所有的窗口都可以拖动, 关闭
可以编辑用户信息, 群组信息, 每个用户可以新建 3 个群组
可以好友聊天, 群组聊天
搜索用户和群组
好友申请和群组申请
在线时, 可以获得好友上线下线提醒, 实时答复用户申请
离线时, 仍然可以给用户和群组留言, 下次登录获得提醒
后续
接下来可以优化和增强的地方, 我想到以下几点:
使用 nuxt 将 vue 进行服务端渲染 , 进一步提高性能
node 部分, 使用 pm2 进行部署.
源代码: vue_qq
来源: https://www.cnblogs.com/edwardloveyou/p/10702471.html