基于Electron的聊天应用
Heading
技术栈 [electron|node|mongodb|socket.io]
Electron的组成
- 为什么是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
- 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 环境
- 安装Node.js和 npm
- 全局安装 electron
npm install electron -g
- 创建一个目录和必备文件
mkdir hello-world && cd hello-world //在hello-word文件夹下新建main.js package.json index.html三个文件
- 在package.json文件中写入
{
"name" : "your-app",
"version" : "0.1.0",
"main" : "main.js"
}
- 在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()
}
})
- 在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>
- 在终端运行
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打包
- 安装electron-packager
npm install --save-dev electron-packager
- 正式打包
注意: 此过程必须要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:可选选项
- 成功之后:
![]()



浙公网安备 33010602011771号