Electron入门

前言

最近公司要做windows客户端,但是公司还缺少C#的开发人员🎫,网上很早就有了electron实现windows客户端,于是就需要前端的同事通过electron来实现一款应用替代C# winform 窗体🎨。

什么是electron?

官网解释:使用 JavaScriptHTMLCSS 构建跨平台的桌面应用程序,每一个窗体就是对应的一个html页面

快速入门

官网也提供了快速入门的方案,相关代码如下。

1
2
3
4
5
6
7
8
#克隆这个仓库
git 克隆 https://github.com/electron/electron-quick-start
#进入仓库
cd electron-quick-start
#安装依赖
npm 安装
#运行应用程序
npm start

不出意外的情况下,你就会弹出如下窗口💚

自己实现hello World

全局安装Electron

1
2
# 如果安装失败可以使用cnpm或者改用淘宝源进行安装(可以全局安装也可以局部安装)
npm install -g electron

初始化项目

新建一个文件夹尽量使用英文,在该目录下执行

1
npm init --yes

创建入口文件也就是主进程文件(下文会提什么是主进程)要与package.jsonmain对应的文件名一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* @Author: drinkwd
* @Date: 2022-04-11 17:38:08
* @LastEditors: drinkwd
* @LastEditTime: 2022-04-12 14:25:18
* @Description: 主进程入口
*/
const {app, BrowserWindow } = require('electron')
let mainWindow = null // 主窗口
// app代表electron的引用,BrowserWindow代表窗体
// 当app准本就绪之后创建一个800*600的窗体
app.whenReady().then(() => {
// 创建窗体
mainWindow = new BrowserWindow({
width: 800,
height: 800,
})
// 加载渲染进程文件
mainWindow.loadFile('index.html')
// 监听窗口关闭回调,减少资源占用
mainWindow.on('closed', () => {
mainWindow = null
})
})

创建渲染进程文件(下文会提什么是渲染进程)

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
hello World
</body>
</html>

查看效果

1
npm start # 不出意外的话会出现一个hello World窗体

如果报这个Cannot find module 'fs/promises'错误,需要升级一下nodejs的版本

Electron中主进程与渲染进程的关系

  • 一般一个程序只有一个主进程也就是package.json中对应的入口文件💨。
  • 渲染进程就是在主进程中加载的html文件,一个程序可以有多个渲染进程,并由主进程读取控制💫。

这个时候在回过头看上面的代码是不是眼前一亮,更加清晰明了🍺。

基础案例

注:不同版本的electron有些语法可能会有区别,如果有效果没有生效,先查看版本在进行排错,可能会减少很多坑。

案例1 读取文件内容
  • 在文件根目录创建file.txt文件
    1
    2
    # file.txt
    file中的文件内容测试
  • 在index.html的body中加入相关元素,触发读取事件,展示读取内容
    1
    2
    3
    4
    5
    6
    7
    8
    <body>
    <button id="btn">
    读取文件
    </button>
    <!--展示读取内容-->
    <div id="content"></div>
    <script src="render/index.js"></script>
    </body>
  • 编写读取代码,在根目录下新建目录及文件render->index.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var fs = require("fs")
    window.onload = function(){
    var btn = this.document.querySelector('#btn')
    var content = this.document.querySelector('#content')
    btn.onclick = function(){
    fs.readFile('file.txt',(err,data) =>{
    content.innerHTML = data
    })
    }
    }
  • 运行npm start 表面上看是没有什么问题但是你可能会得到如下错误信息

原因:如果想在项目中使用nodejs中的内容必须在创建窗口时引用如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
* @Author: drinkwd
* @Date: 2022-04-11 17:38:08
* @LastEditors: drinkwd
* @LastEditTime: 2022-04-12 14:25:18
* @Description: 主进程入口
*/
const {app, BrowserWindow } = require('electron')
let mainWindow = null // 主窗口
// app代表electron的引用,BrowserWindow代表窗体
// 当app准本就绪之后创建一个800*600的窗体
app.whenReady().then(() => {
// 创建窗体
mainWindow = new BrowserWindow({
width: 800,
height: 800,
webPreferences: {
// 为了使用node
nodeIntegration: true,
contextIsolation: false,
},
})
// 加载渲染进程文件
mainWindow.loadFile('index.html')
// 监听窗口关闭回调,减少资源占用
mainWindow.on('closed', () => {
mainWindow = null
})
})

这时在运行npm start就可以正常运行了。

案例2 在渲染进程中打开新窗口

当我们知道了Electron有主进程和渲染进程后,我们还要知道一件事😶,就是ElectronAPI方法和模块也是分为可以在主进程和渲染进程中使用的,具体参考文档👀。那如果我们想在渲染进程中使用主进程中的模块方法时,可以使用Electron Remote解决🍠。

  • 安装@electron/remote
    1
    npm install --save @electron/remote
  • index.htmlbody中增加如下代码
    1
    2
    3
    4
    5
    <body>
    <button id="btn_open">
    打开新窗口
    </button>
    </body>
  • 创建新窗口文件newOpen.html 内容如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    </head>
    <body>
    <div>新窗口</div>
    </body>
    </html>

  • render->index.js中增加如下代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var btn_open = this.document.querySelector('#btn_open')
    const { BrowserWindow } = require('@electron/remote')
    btn1.onclick = ()=>{
    var newWin = new BrowserWindow({
    width: 500,height: 500,
    webPreferences:{
    nodeIntegration:true,
    contextIsolation: false
    }
    })
    newWin.loadFile('newOpen.html')
    newWin.on('close',()=>{
    newWin = null
    })
    }
  • 在主进程中加载渲染进程的下方一定要加入如下内容否则是无法使用remote
    1
    2
    3
    // 开始 为了在渲染线程使用remote
    require('@electron/remote/main').initialize()
    require('@electron/remote/main').enable(mainWindow.webContents)
案例3 创建菜单以及快捷键

我们可以看到在Electron的左上角有一些菜单,那我们如何在Electron中自定义这些菜单呢。

  • 在根目录中新建menu.js 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    const {Menu,BrowserWindow} = require('electron')
    var template = [{
    label: '菜单1',
    submenu:[{
    label: '子菜单1', // 菜单名称
    accelerator: 'ctrl+n', //快捷键
    click: () =>{
    为菜单增加点击事件
    var win = new BrowserWindow({
    width: 500,
    height: 500,
    webPreferences:{
    nodeIntegration: true,
    contextIsolation: false
    }
    })
    win.loadFile('newOpen.html')
    win.on('closed',()=>{
    win = null
    })
    }
    }]
    },{
    label: '菜单2',
    submenu:[{
    label: '子菜单2'
    }]
    }]
    // 构建菜单
    var m = Menu.buildFromTemplate(template)
    // 设置菜单
    Menu.setApplicationMenu(m)
  • 在主进程中引用该文件

    1
    require('./menu.js') //渲染菜单
    案例4 打开调试面板

    当我们在没有自定义导航菜单的时候可以通过上方导航的View->Toggle DevTools 🐇打开控制台方便调试,那如果我们自定义菜单之后应该怎么打开调试控制台呢🙋‍♂️?。

  • 在主进程中增加如下代码,在程序启动的时候自动打开控制台

1
mainWindow.webContents.openDevTools()
案例5 创建右键菜单

右键菜单是在渲染进程中进行编辑的所以我们要在渲染进程中编写如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// render->index.js
const {Menu, getCurrentWindow } = require('@electron/remote')
// 创建右键菜单内容,与导航菜单逻辑相同
var rightTemplate = [{
label: '右键菜单1',
},{
label:'右键菜单2'
}]
var m = Menu.buildFromTemplate(rightTemplate)
// 监听鼠标右键事件
window.addEventListener('contextmenu', function(e){
// 阻止默认事件
e.preventDefault()
//把菜单模板添加到右键菜单
m.popup({
window: getCurrentWindow()
})
})
案例6 通过链接打开浏览器,嵌入网页,打开子窗口

通过链接打开浏览器

  • index.htmlbody中增加a标签
    1
    <a id="aHref" href="https://drinkwd.github.io/">打开博客</a>
  • 如果什么都不写这时运行程序,该网址会在窗口内打开🎨,我们想要的时候通过浏览器来打开该网址在render->index.js中增加如下代码👇
    1
    2
    3
    4
    5
    6
    7
    const {shell} = require('electron')
    var aHref = this.document.querySelector('#aHref')
    aHref.onclick = function(e){
    e.preventDefault()
    var href = this.getAttribute('href')
    shell.openExternal(href)
    }
  • 这样在运行的时候就会通过默认浏览器打开该链接🛹

嵌入网页

  • 在主进程中添加如下代码
    1
    2
    3
    4
    5
    6
    7
    8
    // 实例化View
    const view = new BrowserView()
    // 主窗口中引用类似iframe
    mainWindow.setBrowserView(view)
    // 设置位置与宽高
    view.setBounds({ x: 0, y: 50, width: 500, height: 600 })
    // 加载内嵌网页地址
    view.webContents.loadURL('https://electronjs.org')
  • 执行npm start就会在指定位置显示出你引入的网页

创建子窗口,传值(与网页的传值相同)

  • 我们之前提到过在渲染进程中使用remote打开一个窗口🧶,实际上就是一个新窗口,但是创建子窗口需要使用window.open来实现🎣

  • index.html增加打开子窗口按钮元素

    1
    2
    3
    <button id="sonWindow">打开子窗口</button>
    <!--赋值-->
    <div id="mytext"></div>
  • render->index.js中打开子窗口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var sonWindow = this.document.querySelector('#sonWindow')
    sonWindow.onclick = function(e){
    e.preventDefault()
    // 打开子窗口
    window.open("./test.html")
    }
    // 监听子窗口传过来的值
    window.addEventListener('message',(msg)=>{
    console.log(msg)
    var mytext= document.querySelector('#mytext')
    mytext.innerHTML = msg.data
    })
    1
    2
    3
    4
    5
    <!--test.html-->
    <body>
    <h1>子窗口</h1>
    <button id="popbtn">向父窗口传递信息</button>
    </body>
    1
    2
    3
    4
    5
    6
    // render->index.js
    var popbtn = this.document.querySelector('#popbtn')
    popbtn.onclick = function(){
    window.opener.postMessage('我是子窗口传递过来的信息')
    }

案例7 打开各种对话框与桌面通知,在渲染进程实现
  • index.html中增加如下代码👇
    1
    2
    3
    4
    5
    6
    7
    <body>
    <button id="opendialog">打开文件框</button>
    <button id="savedialog">保存文件对话框</button>
    <button id="messagedialog">消息对话框</button>
    <button id="postpush">桌面消息通知</button>
    <script src="render/demo5.js"></script>
    </body>
  • render->index.js中进行实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    var opendialog = this.document.querySelector('#opendialog')
    var savedialog = this.document.querySelector('#savedialog')
    var messagedialog = this.document.querySelector('#messagedialog')
    var postpush = this.document.querySelector('#postpush')
    const {dialog} = require('@electron/remote')
    // 打开文件框
    opendialog.onclick = function(){
    // 选择文件
    dialog.showOpenDialog({
    title:'请选择图片', // 左上角标题
    defaultPath:'file.txt', //默认路径
    filters:[{ // 过滤
    name:'text',
    extensions:['txt'] //过滤文件后缀 保留的
    }],
    buttonLabel:'打开文本' //自定义打开按钮
    }).then((res)=>{
    console.log(res.filePaths[0]) //获取文件信息
    }).catch(err=>{
    console.log(err) // 错误信息
    })
    }
    // 保存文件
    savedialog.onclick = function(){
    dialog.showSaveDialog({
    title:'保存文件', // 左上角标题
    }).then((res)=>{
    console.log(res) //获取文件信息
    }).catch(err=>{
    console.log(err) // 错误信息
    })
    }
    // 消息对话框
    messagedialog.onclick = function(){
    dialog.showMessageBox({
    type:'warning',
    title:'消息对话框',
    message:'消息对话框内容',
    buttons:['按钮1','按钮2']
    }).then((res)=>{
    // 点击按钮之后的回调函数
    console.log(res)
    })
    }
    // 桌面通知
    postpush.onclick = function(){
    let option = {
    title:'通知标题',
    body: '通知内容'
    }
    new window.Notification(option.title,option)
    }
案例8 断网检测(应该是html5的功能),复制文字,注册全局快捷键
  • 断网检测:在render->index.js中监听该事件如果触发相应事件进行相应操作
1
2
3
4
5
6
7
// 断网之后再次链接会显示该对话框
window.addEventListener('online', () => {
alert('链接成功')
})
window.addEventListener('offline', () => {
alert('断网了,请检查网络连接')
})
  • 复制文字 引用clipboard模块,因为这个模块主进程和渲染进程中都有所以就不需要用到remote
    1
    2
    3
    4
    5
    <body>
    <h1 id="content">想要复制的内容</h1>
    <button id="btnCopy">复制</button>
    <script src="render/demo7.js"></script>
    </body>
1
2
3
4
5
6
7
8
9
// render->index.js
//const { clipboard } = require("electron") 两种引用方式都可以
const { clipboard } = require("@electron/remote")
var content = document.querySelector('#content')
var btnCopy = document.querySelector('#btnCopy')
btnCopy.onclick=function(){
clipboard.writeText(content.innerHTML)
alert('复制成功')
}
  • 注册全局快捷键: 引用globalShortcut模块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // index.js 主进程文件
    const {app, BrowserWindow } = require('electron')
    let mainWindow = null // 主窗口
    // app代表electron的引用,BrowserWindow代表窗体
    // 当app准本就绪之后创建一个800*600的窗体
    app.whenReady().then(() => {
    // 创建窗体
    mainWindow = new BrowserWindow({
    width: 800,
    height: 800,
    webPreferences: {
    // 为了使用node
    nodeIntegration: true,
    contextIsolation: false,
    },
    })
    // 加载渲染进程文件
    mainWindow.loadFile('index.html')
    // 快捷键打开网址,内部
    globalShortcut.register('ctrl+e',()=>{
    mainWindow.loadURL('http://www.baidu.com')
    })
    let isRegister = globalShortcut.isRegistered('ctrl+e')?'success': 'fail'
    console.log('--->',isRegister)
    // 监听窗口关闭回调,减少资源占用
    mainWindow.on('closed', () => {
    mainWindow = null
    })
    })
    app.on('will-quit',function(){
    // 注销全局快捷键
    globalShortcut.unregister('ctrl+e')
    // 注销全局快捷键
    // globalShortcut.unregisterAll()
    })

Vue项目中集成Electron

通过上面的案例我们已经简单了解了Electron的基本用法和使用😝,但是我们在实际的工作当中大多都是使用vue或者react进行项目开发🍤,下面就说一下如果在vue项目中集成Electron🎈

  • 创建一个vue的项目,通过vue-cli脚手架搭建就可以,这里就不过多叙述了,前端开发肯定都会这一步📞。

  • 引用vue-cli-plugin-electron-builder插件

    1
    2
    3
    # 这里使用vue add 不使用npm install的原因是因为vue add会对脚手架有一些修改
    # 会自动帮助我们生成主进程文件,scripts脚本安装相关依赖。
    vue add electron-builder

  • 这时只需要运行 npm run electron:serve就会将前端项目运行至Electron中生成一个桌面程序🤣。

如果你的启动非常慢可以将background.js中的这段代码注释👇

1
2
3
4
5
6
7
8
9
// 安装vue开发者插件
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS3_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}

Electron打包

第一次打包会很慢很慢,也可能会超时,解决方案可以参考这篇或者文章2
设electron淘宝镜像在终端中敲入

1
npm set ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/

打开C盘,在你当前用户下搜索 .npmrc 文件,用记事本打开,看看是否设置成功。

1
npm run electron:build
  • 如果你很顺利的话就会在根目录看到dist_electron文件夹里面就会有绿色版的exe和安装包🤔(没有的话就是打包失败了)

  • 但是默认的安装体验是非常差的我们可以通过vue.config.js来设置安装过程的步骤具体😛参考文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// vue.config.js
module.exports = {
pluginOptions: {
electronBuilder: {
builderOptions: {
win:{
target: 'nsis' // 安装方式
},
nsis:{
oneClick: false, 是否一键安装
allowToChangeInstallationDirectory: true // 允许更改安装目录
}
// options placed here will be merged with default configuration and passed to electron-builder
},
nodeIntegration: true,
contextIsolation: !true
}
}
};

如果在页面中使用了history路由可能在打包之后发现router-view中是空的

1
2
3
解决方法就是在 App.vue 载入的时候,给 mounted 这个钩子里面手动跳转到你想要的首页页面的路径去就可以了。

个人用的是 this.$router.push("/") ,打包后 router-view 的部分就显示正常了。

主进程与渲染进程通信

  • 用到两个模块 ipcMainipcRender
  • 在渲染进程中发送事件给主进程,监听主进程发过来的事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <template>
    <div class="loginContainer">
    <div class="systemLogoBox">
    <img class="systemLogo" src="@/assets/img/logo.png" @click="sendMessage" />
    </div>
    </div>
    </template>
    <script setup>
    import {onMounted} from 'vue'
    import { ipcRenderer } from 'electron'
    const sendMessage = () => {
    // 向主进程发送事件类似$emit
    ipcRenderer.invoke("hello")
    }
    onMounted(() => {
    ipcRenderer.on('helloClick', () => {
    console.log('我收到了点击事件')
    })
    })
    </script>
  • 主进程中监听渲染进程中发送的事件,发送事件给渲染进程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // background.js增加如下代码
    import {ipcMain,Notification} from 'electron'
    // 监听渲染进程发送的事件
    ipcMain.handle('hello', () => {
    // 创建桌面通知
    const n = new Notification({
    title: '通知信息',
    body: "你收到了一条消息"
    })
    n.show()
    n.on('click', () => {
    console.log(win)
    // 点击桌面通知的时候发送事件给到渲染进程
    win.webContents.send('helloClick')
    })
    })
    如果在开发的过程中发现了一些问题可以先看看官网的常见问题处理特别注意nodejsElectron的版本

总结

Electron的入门还是比较简单的,但是在实际工作当中可能会出现调用dll文件或者与硬件进行交互的时候可能就会消耗一定的精力😥,如果我在后期遇到了类似的工作,也会总结起来与大家进行分享滴🎈

参考链接

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!谢谢