导航

electron实现一个聊天的桌面应用

Posted on 2017-12-13 18:48  西门吹雪㊒头发  阅读(1569)  评论(0)    收藏  举报

基于Electron的聊天应用

Heading

技术栈 [electron|node|mongodb|socket.io]

Electron的组成

  1. 为什么是electron
  • JavaScript近几年的全领域发展,JavaScript是思想对java的前进,从compile once,run everywhere转变为code once,run everywhere,由于JavaScript本身的是一门解释性的脚本语言,这让它逐渐的成为全宇宙使用最广泛的语言,没有之一
  • 传统的PC软件开发成本太高,不仅技术门槛高,而且不同的平台app需要分别开发
  • 传统浏览器的一些限制,在浏览器里,Web页面通常运行在一个沙盒环境里,它不能访问本地的资源。比如在Web页面里,调用本地GUI是不允许的,而electron就提供了对GUI的调用API
  • 官网提供一个比较全面的DEMO,包括常规的系统级别操作,通信,截图,调用PDF等例子,API支持多语言,已经翻译好,而且有vue搭建的例子,本次没有使用vue,vue编译之后可能会将一些调用electron模块方法编译成script标签的可能。
  • 大家都在用,比VS Code
  1. Electron是什么?
    引用官网的一句话:Build cross platform desktop apps with JavaScript, HTML, and CSS
    Electron是基于Node.js和Chromium做的一个工具。electron是的可以使用前端技术实现桌面开发,并且支持多平台运行。最初是 Github 发布的 Atom 编辑器衍生出的 Atom Shell,后更名为 Electron 。Electron 提供了一个能通过 JavaScript 和 HTML 创建桌面应用的平台,同时集成 Node 来授予网页访问底层系统的权限。目前常见的有 NW、heX、Electron,可以打造桌面应用。

    Electron为用纯JavaScript创建桌面应用提供了运行时。原理是,Electron调用你在package.json中定义的main文件并执行它。main文件(通常被命名为main.js)会创建一个内含渲染完的web页面的应用窗口,并添加与你操作系统的原生GUI(图形用户界面)交互的功能。也就是当用Electron启动一个应用,会创建一个主进程。这个主进程负责与你系统原生的GUI进行交互并为你的应用创建GUI

    窗口是通过main文件里的主进程调用叫BrowserWindow的模块创建的。每个浏览器窗口会运行自己的渲染进程。渲染进程会在窗口中渲染出web页面(引用了CSS,JavaScript,图片等的HTML文件)。web页面是Chromium渲染的,因为各系统下标准是统一的的,所以兼容性很好。

环境搭建

它的方式是使用 nodeJs API 调用系统资源,所以第一步就是安装 node.js 环境

  1. 安装Node.js和 npm
  2. 全局安装 electron
npm install electron -g
  1. 创建一个目录和必备文件
mkdir hello-world && cd hello-world //在hello-word文件夹下新建main.js package.json    index.html三个文件
  1. 在package.json文件中写入
        {
          "name"    : "your-app",
          "version" : "0.1.0",
          "main"    : "main.js"
        }
  1. 在main.js文件中写入
const {app, BrowserWindow} = require('electron')
const path = require('path')
const url = require('url')
let win
function createWindow () {
  // Create the browser window.
  win = new BrowserWindow({width: 800, height: 600})
  // and load the index.html of the app.
  win.loadURL(url.format({
    pathname: path.join(__dirname, 'index.html'),
    protocol: 'file:',
    slashes: true
  }))
  win.webContents.openDevTools()
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow()
  }
})
  1. 在index.html中写入
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using node <script>document.write(process.versions.node)</script>,
    Chrome <script>document.write(process.versions.chrome)</script>,
    and Electron <script>document.write(process.versions.electron)</script>.
  </body>
</html>
  1. 在终端运行
 electron . 

你可以看到一个桌面应用程序

数据通讯

Electron 有两个进程,分别为 main 和 renderer,而两者之间是通过 ipc 进行通讯。main 端有 ipcMain,renderer 端有 ipcRenderer,分别用于通讯。

同窗口

renderer.js 中可以主动调用 ipcRenderer 发送消息给 main.js,main.js 的 ipcMain 会送消息给 renderer.js。

//发送消息
ipcRenderer.send('events-hello', 'Hi Steve Jobs')
//接收消息
ipcMain.on('events-hello', function (event, data) {
//回送消息
console.log(data);
event.sender.send('events-reply', 'Hello Man')
})
//接收回送消息
ipcRenderer.on('events-reply', function (event, data) {
console.log(data);
})

跨窗口

需要先找到对应窗口对象,例如发送消息到下载窗口,下载窗口对象为downloadWindow

//发送消息
downloadWindow.webContents.send('download-file-update', file);
//接收消息
ipcRenderer.on('download-file-update', function (event, data) {
console.log(data);
})

实例会在后续代码中演示

用户信息结构

数据库采用mongodb来保存数据

user: {
        logo: { type: String },  
        name: { type: String, required: true },
        password: { type: String, required: true },
        sex: { type: String, default: "boy" },
        status: { type: String, default: "down" },
        socket: String,
        account: { type: String, required: true },
        group: [      // 自己的好友分组
            {
                type: Schema.Types.ObjectId,
                ref: 'group'        // ref->m   populate.path->字段名
            }
        ],
        // 群组管理
        chatgroup: [   //自己属于的聊天群
            {
                type: Schema.Types.ObjectId,
                ref: 'chatgroup'        // ref->m   populate.path->字段名
            }
        ],
        friends: [{  //好友
            user: {
                type: Schema.Types.ObjectId,
                ref: 'user'
            },
            remark: String,
            groupId: Schema.Types.ObjectId,
            news: {
                type: Schema.Types.ObjectId,
                ref: 'news'
            }
        }],
        framef: [   //向你好友请求的用户
            {
                type: Schema.Types.ObjectId,
                ref: 'user'
            }
        ]
    },
    group: {
        gname: { type: String, require: true },
        ismain: {type: Boolean , default: false}
    },
    news: {
        time: String,
        msg: String
    },
    chatgroup: {
        gnumber: String,
        cgname: { type: String, require: true },
        member: [
            {
                type: Schema.Types.ObjectId,
                ref: 'user'
            }
        ]
    },

socket.io

io.sockets.emit(‘String’,data);//给所有客户端广播消息
socket.broadcast.emit('String', data); //给所有客户端广播消息(发送消息过来的客户端自己以外其他客户端)
io.sockets.socket(socketid).emit(‘String’, data);//给指定的客户端发送消息
socket.emit(‘String’, data);//给该socket的客户端发送消息

群聊与单聊的实现

  global.io.on('connection', function (socket) {
        //昵称设置
        //  socket.userIndex = req.session.user;
        //  socket.nickname = req.session.user;
        //  socket.emit('loginSuccess');
        //接收新消息
        socket.on('login', function (msg) {
            var User = global.dbHandel.getModel('user');
            User.update({ _id: msg.user }, { status: 'up', socket: socket.id }, function (err, result) {
                if (err) {
                    console.log('系统内部错误')
                } else {
                    console.log('p', socket.id, msg.user)
                }
            })
            //  console.log(socket.id)
            //  socket.userIndex = users.length;
            //  socket.nickname = msg;
            //  users.push(msg);
            //将消息发送到除自己外的所有用户
            //  io.sockets.emit('system', msg, users.length, 'login'); //向所有连接到服务器的客户端发送当前登陆用户的昵称
        });
        socket.on('postMsg', function (msg) {
            var User = global.dbHandel.getModel('user');
            var Chatgroup = global.dbHandel.getModel('chatgroup')
            if (msg.type === 'single') {
                User.findOne({ _id: msg.to }, ['socket'], function (err, suc) {
                    socket.broadcast.to(suc.socket).emit("toSomeone", { infor: msg.msg, from: msg.from, type: 'single' })
                })
                // socket.broadcast.to(socketList[data.user].id).emit("toSomeone",data.str)
            } else {
                Chatgroup.findOne({ _id: msg.to }, ['member'], function (err, sgroups) {
                    console.log(7, sgroups)
                    for (let q = 0; q < sgroups.member.length; q++) {
                        if (JSON.stringify(sgroups.member[q]) !== '"' + msg.from + '"') {
                            User.findOne({ _id: sgroups.member[q] }, ['socket'], function (err, suc) {
                                socket.broadcast.to(suc.socket).emit("toSomeone", { infor: msg.msg, from: msg.from, type: 'group', target: msg.to })
                            })
                        }
                    }
                })
            }
            //  console.log(msg)
            //将消息发送到除自己外的所有用户
            // socket.broadcast.emit('newMsg', socket.nickname, msg, 'blue');
        });

登陆流程

login.js:

  const ipc = require('electron').ipcRenderer
    $.ajax({
        url: 'http://localhost:5000/login',
        type: 'post',
        data: data,
        success: function (data, status) {
            if (status == 'success') {
                if (data.code) {
                    $('#errorInfor').text(data.error)
                } else {
                   let winId = BrowserWindow.getFocusedWindow().id
                    winchat = new BrowserWindow({
                        width: 1500,
                        height: 1000,
                        frame: true
                    })
                    winchat.on('close', () => { winchat = null })
                    // winchat.webContents.openDevTools()
                    winchat.loadURL(url.format({
                        pathname: path.join(__dirname, '../../render-process/html/chat.html'),
                        protocol: 'file:',
                        slashes: true,
                        resizable: true
                    }))
                     winchat.webContents.on('did-finish-load', (event) => {
                        winchat.webContents.send('msg', winId, data)
                        ipc.send('login-success')
                    })
                }
            }
        },

main.js:

const ipcMain  = require('electron')
    ipcMain.on('login-success', (data) => {
        win.destroy()
        win = null
    })

chat.js:

 const { ipcRenderer } = require('electron')
    ipcRenderer.on('msg', (event, winId, msg) => {
        selfUserId = msg._id
        localStorage.selfUserId = msg._id;
        $("#manageFriends").html(friendMhtml(msg))
        $("#getInforDiv").html(inforHtmlDiv(msg.framef))
        $("#manageChatGroup").html(chatgroupFun(msg.chatgroup))
        socketFun(msg._id)
    })

数据持久化

简易用户的一些 String Number JSON 数据,可使用 electron-json-storage、electron-config。(重新安装APP这里的数据不会丢失)大量数据,可使用 nedb (JavaScript Database)。这些都是官方推荐的

db = new Datastore({
    filename: path.join(remote.app.getPath('userData'), `/${msg._id}.db`),
    autoload: true
})

electron打包

  1. 安装electron-packager
 npm install --save-dev electron-packager 
  1. 正式打包

注意: 此过程必须要FQ。

electron-packager <location of project> <name of project> <platform> <architecture> <electron version> <optional options>
// 例如:electron-packager ./app 有聊 --all --out ./OutApp --version 1.4.0 --overwrite --icon=./app/img/icon/icon

命令说明:

  • location of project:项目所在路径
  • name of project:打包的项目名字
  • platform:确定了你要构建哪个平台的应用(Windows、Mac 还是 Linux)
  • architecture:决定了使用 x86 还是 x64 还是两个架构都用
  • electron version:electron 的版本
  • optional options:可选选项
  1. 成功之后:

代码git地址