一、项目概况

基于electron+vue+electron-vue+vuex+nodejs+vuevideoplayer+electron-builder等技术仿制微信电脑端界面聊天室实例,实现消息发送/动态表情,图片/视频预览,拖拽图片/粘贴截图发送,朋友圈/红包/换肤等功能。

二、效果图

三、技术栈

  • 框架技术:electron + electron-vue + vue
  • 状态管理:vuex
  • 地址路由:vue-router
  • 字体图标:阿里iconfont字体图标库
  • 弹窗插件:wcpop
  • 打包工具:electron-builder
  • 环境配置:node.js + chromium
  • 图片预览:vue-photo-preview
  • 视频组件:vue-video-player

 

如何配置开发环境及使用electron-vue,这里不作多介绍,可查阅官网及搜资料

https://github.com/simulatedgreg/electron-vue

注意:由于electron-vue作者长时间未更新,里面electron版本v2.0.4太旧,如遇问题,可升级到最新版本

◆ electron主进程index.js

通过browserwindow创建和控制浏览器窗口,官网有详细介绍,这里略过…

...

let mainwin
let tray
let forcequit = false
let logined = false

/**
 * 创建主窗口=============================
 */
function createmainwin() {
    mainwin = new browserwindow({
        // 背景颜色
        // backgroundcolor: '#ebebeb',
        width: common.win_size_main.width,
        height: common.win_size_main.height,
        title: common.win_title,
        usecontentsize: true,
        autohidemenubar: true,
        // 无边框窗口
        frame: false,
        resizable: true,
        // 窗口创建的时候是否显示. 默认值为true
        show: false,
        webpreferences: {
            // devtools: false,
            websecurity: false
        }
    })
    
    mainwin.setmenu(null)
    mainwin.loadurl(common.win_load_url())
    
    mainwin.once('ready-to-show', () => {
        mainwin.show()
        mainwin.focus()
    })
    
    // 点击关闭最小到托盘判断
    mainwin.on('close', (e) => {
        if(logined && !forcequit) {
            e.preventdefault()
            mainwin.hide()
        }else {
            mainwin = null
            app.quit()
        }
    })
    
    initialipc()
apptray.createtray() } app.on('ready', createmainwin) app.on('activate', () => { if(mainwin === null) { createmainwin() } }) ...

如上图:创建托盘图标及闪烁效果

/**
 * 托盘图标事件
 */
let flashtraytimer = null
let trayico1 = `${__static}/icon.ico`
let trayico2 = `${__static}/empty.ico`
let apptray = {
  // 创建托盘图标
  createtray() {
    tray = new tray(trayico1)
    const menu = menu.buildfromtemplate([
      {
        label: '打开主界面',
        icon: `${__static}/tray-ico1.png`,
        click: () => {
          if(mainwin.isminimized()) mainwin.restore()
          mainwin.show()
          mainwin.focus()
          
          this.flashtray(false)
        }
      },
      {
        label: '关于',
      },
      {
        label: '退出',
        click: () => {
          if(process.platform !== 'darwin') {
            mainwin.show()
            // 清空登录信息
            mainwin.webcontents.send('clearloggedinfo')
            
            forcequit = true
            mainwin = null
            app.quit()
          }
        }
      },
    ])
    tray.setcontextmenu(menu)
    tray.settooltip('electron-vchat v1.0.0')

    // 托盘点击事件
    tray.on('click', () => {
      if(mainwin.isminimized()) mainwin.restore()
      mainwin.show()
      mainwin.focus()

      this.flashtray(false)
    })
  },
  // 托盘图标闪烁
  flashtray(flash) {
    let hasico = false

    if(flash) {
      if(flashtraytimer) return
      flashtraytimer = setinterval(() => {
        tray.setimage(hasico ? trayico1 : trayico2)
        hasico = !hasico
      }, 500)
    }else {
      if(flashtraytimer) {
        clearinterval(flashtraytimer)
        flashtraytimer = null
      }

      tray.setimage(trayico1)
    }
  },
  // 销毁托盘图标
  destroytray() {
    this.flashtray(false)
    tray.destroy()
    tray = null
  }
}

◆ 渲染进程主页面main.js及app.vue

/**
 * @desc   主入口main.js
 * @about  q:282310962  wx:xy190310
 */

import vue from 'vue'
import axios from 'axios'

import app from './app'
import router from './router'
import store from './store'

// 引入组件配置
import $components from './components'
vue.use($components)

if (!process.env.is_web) vue.use(require('vue-electron'))
vue.http = vue.prototype.$http = axios


/* eslint-disable no-new */
new vue({
  components: { app },
  router,
  store,
  template: '<app/>'
}).$mount('#app')
<template>
  <div id="app">
    <div class="elv-container" :style="$store.state.winskin && {'background-image': 'url('+$store.state.winskin+')'}">
      <div class="elv-wrapper flexbox">
        <!-- //侧边栏 -->
        <side-bar v-if="!$route.meta.hidesidebar" />

        <!-- //主布局 -->
        <div class="elv-mainbx flex1 flexbox flex-col">
          <!-- ...顶部按钮 -->
          <win-bar />
          
          <keep-alive>
            <router-view></router-view>
          </keep-alive>
        </div>
      </div>
    </div>
  </div>
</template>

至于状态管理及路由配置基本和vue里面使用一样,这里也略过…

◆ electron自定义最大/小化、关闭按钮、无边框窗口拖动

配置browserwindow里面frame:false就会是无边框窗口,这时就可以自定义最大/小,关闭按钮,那么问题来了,无边框窗口如何进行拖动尼?

  • 1、通过mousedown、mousemove等事件处理
  • 2、设置需要拖动区css属性 -webkit-app-region
.elv__drag{-webkit-app-region: drag; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none;}
.elv__nodrag{-webkit-app-region: no-drag;}

注意:默认设置-webkit-app-region: drag后,下面的元素不能点击操作,可通过设置需点击元素no-drag即可。

import { app, remote, ipcrenderer } from 'electron'
import { mapstate, mapmutations } from 'vuex'

let currentwin = remote.getcurrentwindow()

export default {
    props: {
        title: string,
    },
    data () {
        return {// 是否置顶
            isalwaysontop: false,
            // 窗口是否可以最小化
            isminimizable: true,
            // 窗口是否可以最大化
            ismaximizable: true,
        }
    },
    computed: {
        ...mapstate(['iswinmaxed'])
    },
    mounted() {if(!currentwin.isminimizable()) {
            this.isminimizable = false
        }
        if(!currentwin.ismaximizable()) {
            this.ismaximizable = false
        }
        if(this.iswinmaxed && currentwin.ismaximizable()) {
            currentwin.maximize()
        }

        // 监听是否最大化
        currentwin.on('maximize', () => {
            this.set_winmaximize(true)
        })
        currentwin.on('unmaximize', () => {
            this.set_winmaximize(false)
        })
    },
    methods: {
        ...mapmutations(['set_winmaximize']),

        // 置顶窗口
        handlefixtop() {
            this.isalwaysontop = !this.isalwaysontop
            currentwin.setalwaysontop(this.isalwaysontop)
        },

        // 最小化
        handlemin() {
            currentwin.minimize()
        },

        // 最大化
        handlemax() {
            if(!currentwin.ismaximizable()) return
            if(currentwin.ismaximized()) {
                currentwin.unmaximize()
                this.set_winmaximize(false)
            }else {
                currentwin.maximize()
                this.set_winmaximize(true)
            }
        },

        // 关闭
        handlequit() {
            currentwin.close()
        }
    }
}

◆ 聊天编辑器光标处插入表情、div可编辑contenteditable=”true”双向绑定

如何实现electron vue中向编辑框contenteditable光标处插入动态表情,类似qq、微信聊天编辑器??

  • 1、使用input、textarea文本框实现

通过给input或textarea文本框插入[奋斗]、(:17 等表情符标签,展示信息的时候解析标签就行

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col col-sm-12">
                    <button class="btn btn-success" data-emoj="[笑脸]">[笑脸]</button>
                    <button class="btn btn-success" data-emoj="[奋斗]">[奋斗]</button>
                    <button class="btn btn-success" data-emoj="[:17]">[:17]</button>
                </div>
                <div class="col col-sm-12">
                    <textarea class="form-control" id="content" rows="10"></textarea>
                </div>
            </div>
        </div>
     
        <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
        <script>
            (function ($) {
                $.fn.extend({
                    insertemojatcaret: function (myvalue) {
                        var $t = $(this)[0];
                        if (document.selection) {
                            this.focus();
                            sel = document.selection.createrange();
                            sel.text = myvalue;
                            this.focus();
                        } else if ($t.selectionstart || $t.selectionstart == '0') {
                            var startpos = $t.selectionstart;
                            var endpos = $t.selectionend;
                            var scrolltop = $t.scrolltop;
                            $t.value = $t.value.substring(0, startpos) + myvalue + $t.value.substring(endpos, $t.value.length);
                            this.focus();
                            $t.selectionstart = startpos + myvalue.length;
                            $t.selectionend = startpos + myvalue.length;
                            $t.scrolltop = scrolltop;
                        } else {
                            this.value += myvalue;
                            this.focus();
                        }
                    }
                });
            })(jquery);
                 
            $("button").on("click", function() {
                $("#content").insertemojatcaret($(this).attr("data-emoj"));
            });
        </script>
    </body>
</html>
  • 2、运用h5中div可编辑contenteditable=”true”实现

在vue页面设置contenteditable=”true” 实现富文本文本框效果,由于div不能绑定v-model,只能使用vue提供的自定义组件v-model功能。

https://cn.vuejs.org/v2/guide/components-custom-events.html#%e8%87%aa%e5%ae%9a%e4%b9%89%e7%bb%84%e4%bb%b6%e7%9a%84-v-model

思路:新建一个chatinputedit.vue组件,通过监听数据变化返回父组件数据

1):调用chatinputedit.vue组件,并给其绑定v-model

<template>
    ...
    <chatinputedit ref="chatinput" v-model="editortext" />
</template>

...

export default {
    data () {
        return {
            editortext: '',
            
            ...
        }
    },
    ...
}

2):v-model中传入的值在子组件prop中获取并监听value变化

export default {
    props: {
        value: { type: string, default: '' }
    },
    data () {
        return {
            editortext: this.value,
            ...
        }
    },
    watch: {
        value() {
            ...
        }
    },
}

3):监听获取到的值赋值给子组件中的v-html参数,就打通双向绑定链路了

/**
 * contenteditable光标处插入内容
 */
inserthtmlatcaret(html) {
    let sel, range;
    if(!this.$refs.editor.childnodes.length) {
        this.$refs.editor.focus()
    }
    if (window.getselection) {
        // ie9 and non-ie
        sel = window.getselection();

        if (sel.getrangeat && sel.rangecount) {
            range = sel.getrangeat(0);
            range.deletecontents();
            let el = document.createelement("div");
            el.appendchild(html)
            var frag = document.createdocumentfragment(), node, lastnode;
            while ((node = el.firstchild)) {
                lastnode = frag.appendchild(node);
            }
            range.insertnode(frag);
            if (lastnode) {
                range = range.clonerange();
                range.setstartafter(lastnode);
                range.collapse(true);
                sel.removeallranges();
                sel.addrange(range);
            }
        }
    } else if (document.selection && document.selection.type != "control") {
        // ie < 9
        document.selection.createrange().pastehtml(html);
    }
}

◆ electron+vue实现微信截图功能

node中通过的execfile方法执行exe文件,exe调用同级目录下的微信截图dll,调出截图工具

handlecapturescreen() {
    return new promise((resolve) => {
        const { execfile } = require('child_process')
        var screenwin = execfile('./static/printscr.exe')
        screenwin.on('exit', function(code) {
            let pngs = require('electron').clipboard.readimage().topng()
            let imgdata = new buffer.from(pngs, 'base64')
            let imgs = 'data:image/png;base64,' + btoa(new uint8array(imgdata).reduce((data, byte) => data + string.fromcharcode(byte), ''))
            resolve(imgs)
        })
    })
},

okay。以上就是基于electron+vue开发仿微信客户端聊天实例分享,希望能有些帮助!!