在找工作的伙伴,可以看看这里:双越老师联合几位博主(包括我)搞了一个前端面试网站 面试派 ------ 常见面试题 + 大厂面试流程 + 面试技巧。做一个真正专业的前端面试网站,旨在解决前端面试资料碎片化、老旧化、非专业化等一系列问题,网站开源免费且持续更新题库
前言
electron
本质上由 Chromium
和 node.js
二者构成,我们会用它来写桌面端应用,写法上还是写 node.js
,其实就是 js
,node
赋予了 js
能够和操作系统打交道的能力,写法上还是 html
,js
,css
三把斧,所以我们在 electron
中写 vue
,react
也是可以的,因为框架最终还是静态文件。
目前像 vscode
,github
桌面端,postman
,WhatsApp
桌面端,Skype
,微信
桌面端,新版 QQ
,飞书
桌面端,Figma
等知名商业软件都是 electron
应用,这些应用利用了 electron
的跨平台特性,使得同一套代码能在 windows
,macOS
和 Linux
上运行,不过 electron
饱受诟病的是它的内存占用以及应用体积很大。
其实
vscode
你是可以点击 帮助 ,然后进入 开发者模式 ,你会发现打开了一个F12
窗口,很神奇,这么看vscode
就像是浏览器一样。
electron
和 tauri
该如何抉择
electron
和 tauri
是目前最流行的前端开发桌面端框架,先看下各自优缺点。
electron
优缺点
优点
- 无需考虑兼容性。
- 学习成本低,可能你看完本文就能上手了。
- 社区生态支持完善。
缺点
- 体积大。
- 仅支持
windows
,mac
,Linux
,不支持安卓
和ios
。 - 开发语言仅支持
js
,html
,css
,当然也可以使用vue
,react
等前端框架,不支持其他语言。
electron
的体积很大,应用会自己内嵌一个Chromium
和Node.js
,内嵌后就不像tauri
一样考虑不同浏览器下的api
差异性。
tauri
优缺点
优点
- 体积小。
- 可以使用不同的编程语言开发,不仅仅支持
js
,html
,css
,还支持kotlin
,swift
,rust
,C#(.net)
更多语言。 - 支持
windows
,mac
,linux
还支持安卓
和ios
。
缺点
- 新兴框架生态不够完善。
- 在
linux
上兼容性不好。 - 学习成本高,需要有
rust
功底。
tauri
体积比较小,这是有代价的,tauri
在windows
用的是 原生**webview**
,在macOS
用的是webkit
,不同浏览器上样式以及api
兼容性会有所不同,比如requestIdleCallback
在Safari
上就不支持。本质上,tauri
就是调用宿主系统的浏览器,问题就在于宿主浏览器五花八门,很容易出问题。
总结
要是看完你还是在选择上犹豫,那我建议你还是无脑选择 electron
,大厂都在用,跟着他们的脚步准没错。其实小型应用用 tauri
挺好,比如你单纯想实现一个剪贴板软件小工具,用 electron
打底也要 250+ M
,用 tauri
可能就 30+ M
,大型应用还是建议用 electron
。
常用模块
app
app 模块控制应用程序的生命周期
app.whenReady().then
应用初始化完成后执行特定操作,基本等效 app.on('ready', callback)
app.quit()
尝试关闭所有窗口并退出应用程序,此方法会先触发 before-quit
事件,若所有窗口成功关闭,则触发 will-quit
事件,随后应用程序终止
app.exit()
立即终止应用程序,并可指定退出代码,与 app.quit
不同,此方法不会触发 before-quit
和 will-quit
app.relaunch()
在当前实例退出时重新启动应用程序,可指定新的启动参数或可执行路径
BrowserWindow
BrowserWindow 模块可用于创建和控制浏览器窗口
new BrowserWindow()
创建一个新的浏览器窗口实例,参数可用于设置窗口的宽度,高度,是否显示菜单栏等属性
win.loadURL()
在窗口中加载指定的 url
win.show()
显示窗口
win.hide()
隐藏窗口
win.webContents()
获取窗口的 webContents
对象,用于与页面内容交互
ipcMain 和 ipcRenderer
这两个模块实现了主进程和渲染进程之间的通信机制
ipcMain 模块在主进程中使用,用于监听和处理从渲染进程发送过来的消息
ipcRenderer 模块在渲染进程中使用,用于发送消息到主进程或接受主进程的回复
shell
shell 模块提供与桌面集成相关的功能,允许应用程序使用系统默认的方式管理文件和 URL,该模块可以在主进程和非沙盒的渲染进程中使用
shell.showItemInFolder()
在文件管理器中显示指定的文件,并尽可能选中该文件
shell.openPath()
以桌面默认方式打开给定的文件,返回一个 promise
,如果操作失败,promise
将解析为包含错误信息的字符串,否则解析为一个空字符串
shell.openExternal()
使用默认的浏览器打开指定的 url
shell.trashItem()
将指定的文件或目录移动至回收站,返回一个 promise
,如果操作失败,promise
将解析为包含错误信息的字符串,否则解析为一个空字符串
dialog
dialog 模块提供了与用户进行文件和消息交互的原生系统对话框
dialog.showOpenDialog()
显示打开文件对话框
dialog.showSaveDialog()
显示保存文件对话框
dialog.showMessageBox()
显示消息框
clipboard
clipboard 模块用于与系统剪贴板交互,允许读写文件,图像等内容
clipboard.readText()
读取剪贴板中文本内容,可选参数指定要读取的剪贴板类型,比如 selection
clipboard.writeText()
将文本写入剪贴板
clipboard.readImage()
读取剪贴板中的图像内容,返回 NativeImage
对象
clipboard.writeImage()
将图像写入剪贴板
clipboard.clear ()
清空剪贴板内容
session
session 模块管理 electron 应用的会话设置,包括缓存,cookie,代理设置等
session.defaultSession
获取默认会话对象
session.fromPartition()
根据分区名称获取会话对象
session.cookies
访问和操作会话的 cookie
session.clearCache()
清除会话缓存
session.setProxy()
设置代理配置
menu 和 MenuItem
Menu 模块用于创建应用程序的菜单,MenuItem 模块用于创建菜单选项
Menu.setApplicationMenu()
设置应用程序的菜单
Menu.buildFromTemplate()
从模板数组中构建菜单
net
net 模块提供了一组 API,用于发起 HTTPS 请求,类似于 Node.js 的 http 模块,但是提供了更高级的功能
net.request()
创建 ClientRequest
实例用于发起 HTTP(S)
请求,options
可以是 URL
字符串或配置对象
ClientRequest 对象的方法
- request.write() 向请求主体写入数据
- request.end() 结束请求,发送剩余的数据
- request.abort() 终止请求
protocol
protocol 模块允许开发者拦截和自定义协议的行为,例如 http(s) ,甚至可以实现自定义协议
protocol.registerSchemesAsPrivileged()
注册自定义协议为特权协议,已启用 CORS
,安全等
protocol.registerFileProtocol()
注册一个协议以处理文件请求
protocol.registerHttpProtocol()
注册一个协议以处理 HTTP
请求
protocol.unregisterProtocol()
注销已注册的协议
contextBridge
contextBridge 模块用于在渲染进程的隔离上下文中创建一个安全的,双向的,同步的桥梁,使预加载脚本可以安全地将功能暴露给网页
contextBridge.exposeInMainWorld(apiKey, api)
将预加载脚本中的 api
暴露给浏览器
apiKey
在window
对象上挂在 api
的键名
api
要暴露的 api
对象
进程间通信 IPC
进程之间的通信简称就是 ipc
。eletron
的进程分为两种,主进程和渲染进程,eletron
项目就是一个 主进程,渲染进程可以有多个,一个主进程控制多个渲染进程,一个窗口就是一个渲染进程
electron
项目一定会有个 main.js
,这个 文件 就是来控制 主进程的
一般渲染进程的就是 render.js
,我们只需要在 html
文件中引入 render.js
即可
主进程的环境就是 node
,我们写不了浏览器的 api
,渲染进程就是 浏览器环境,同样的,这里也用不了 node
的模块。比如 window
是浏览器的 api
,主进程拿不到这个 api
,__dirname
是 node
的 api
,渲染进程就拿不到了。这么看,二者就是独立的,但是二者必须要通信,比如我现在希望 窗口可以展示我的 electron
版本,这个版本数据必须是从主进程才能拿到,实际上需要二者通信的案例比比皆是。
预加载脚本 preload.js
主进程和渲染进程的通信需要一个中间人----预加载脚本 preload.js
,通常这个文件和main.js
一样,放在根目录下面
preload.js
本质上还是在浏览器环境,所以你这里的 log
只能在窗口的控制台才能看到,preload.js
特殊点,它可以访问一部分 node
的 api
,如图
预加载脚本可以在浏览器环境中,能够使用一部分 node
api
也是蛮合理的, 想要让 preload.js
生效,我们需要给 主进程的 BrowserWindow
添加一个配置项
js
function createWindow () {
const win = new BrowserWindow({
// ***
webPreference: {
preload: path.resolve(__dirname, './preload.js') // 记得引入 path 模块
}
})
}
主进程,预加载脚本,渲染进程的执行顺序一定是 从左往右
预加载脚本是可以将部分 node
api
暴露出来给到 渲染进程中使用,比如下面
js
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
version: process.versions
})
我们就可以在 render.js
中拿到 这个 myAPI
,myAPI
下面就是 version
这个 key
,其实这个暴露到了 window
上面
渲染向主进程通信(单)ipcRenderer.send, ipcMain.on
这里举一个🌰,我希望在窗口中的 input 框中写入内容到操作系统,这里肯定是需要渲染进程向主进程通信的,主进程代表 node
,可以与操作系统打交道
先把 render.js
实现好
js
const btn = document.getElementById('btn')
const input = document.getElementById('input')
btn.onclick = () => {
myAPI.saveFile(input.value)
}
需要 preload.js
引入一个 模块 ipcRenderer
js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
saveFile: () => {
ipcRenderer.send('file-save', data) // 参数一:事件名,参数二:callback
}
})
主进程需要订阅来自 preload.js
发布的事件
js
const { app, BrowserWindow, ipcMain } = require('electron')
function createWindow () {
const win = new BrowserWindow({
// ***
})
ipcMain.on('file-save', writeFile)
}
function writeFile (_, data) { // 第一个参数为 event,不需要就用 _ 占位
// 调用 fs 模块写入文件
}
主进程的事件 callback
,默认有两个参数,一个 事件,一个传进来的参数
渲染向主进程通信(双)ipcRenderer.invoke, ipcMain.handle
这里是双向通信,为了体现双向,这里依旧是上面的🌰,我现在希望在窗口可以读取出操作系统的文件内容
preload.js
js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
async readFile () {
let res = await ipcRenderer.invoke('file-read') // 第二个参数就是传入的参数
}
})
main.js
js
const { app, BrowserWindow, ipcMain } = require('electron')
function createWindow () {
const win = new BrowserWindow({
// ***
})
ipcMain.handle('file-read', readFile)
}
function readFile () {
// 调用 js 模块读取文件内容
return fs.readFileSync('/****').toString() // 正常读取是 buffer,需要转成 字符串
}
这里双向就是因为 ipcRenderer.invoke
的事件发给了 ipcMain.handle
订阅后,handle
会将返回值给到 ipcRenderer.invoke
,给个结果一定是个 promise
,所以需要用 await
接受,否则会是 pending
态的 promise
,当然你也可以把 promise
写到 render.js
中
主进程向渲染进程通信(单)webContents.send, ipcRenderer.on
这个其实 就是 渲染向主进程通信(单)的反写
main.js
js
function createWindow () {
win.webContents.send('message', 'Hello World')
}
preload.js
js
contextBridge.exposeInMainWorld('myAPI',{
getMessage: (data) => {
return ipcRenderer.on('message', data)
}
})
渲染进程之间通信
渲染进程之间是不能直接通信的,所以需要借助 主进程 作为中间人来进行通信
election-store
electron
的存储方案总的来看就是下面三种
-
web 浏览器 api
electron
使用了Chromium
来渲染,因此可以使用cookie
,localStorage
,sessionStorage
,indexDB
等来实现本地存储,这个方案适用于存储少量数据或临时数据 -
文件系统操作
利用
node.js
的文件系统模块 -
数据库技术
electron
支持多种数据库技术,比如SQLite
和LevelDB
,以实现更高效的大量结构化数据
而 electron-store
是一个较为特殊的方式,他是一个专门用于 electron
应用设计的轻量级数据持久化库,使用简单,依赖少,他属于 electron
但又基于node
的文件读取,它会将信息存到一个 json
中,而且提供了类似 浏览器 api
方式的读写方式
js
const { app, BrowserWindow } = require('electron')
const Store = require('electron-store')
const store = new Store()
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreference: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady('unicorn', '独角兽')
console.log(store.get('unicorn')) // 输出:独角兽
store.set('foo.bar', true)
console.log(store.get('foo')) // 输出 { bar: true
由上代码可以看出,electron-store
的使用还是非常无脑的,无需你写 sql
语句,你还能存入信息,写法上就像是 浏览器 的缓存
electron-store
既然本质上还是 node.js
的读写,所以它是不能使用在 渲染进程的,否则程序会白屏
存入的 json
文件,默认情况下是在 app.getPath('userData')
目录下的 config.json
文件中
windows macos 差异性
通常来讲,一个应用会有很多个窗口,在 windows
下,我们的行为通常是关闭最后一个 窗口 就会直接关闭应用,而 macos
下,关闭最后一个 窗口,应用的进程仍然保留,对此,我们这里的差异性我们也需要实现
js
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
第一个部分就是当所有窗口关闭时,并且操作系统不是 darwin
,这个 darwin
就是 macos
,所以就是 windows
, 就直接退出 应用
第二个部分就是当应用被激活时,若没有了窗口,那么就会去创建窗口
打包方式
eletron
的打包有四种工具,但是最推荐的也是比较繁琐的工具就是 electron-builder
,最推荐是因为相比其他方式打包出的体积小一点,繁琐是因为它可以进行很多自定义的设置
打包是需要我们对 package.json
进行配置的
json
{
"scripts": {
"start": "electron .",
"build": "electron-build" // 打包指令,生成安装包
},
"build": {
"appId": "", // 程序的唯一标识符
"win": { // 代表打包到 windows 平台
"icon": "", // 应用图标
"target": [
{
"target": "nsis", // 使用 NSIS 作为安装程序格式,其实就是 exe 文件,微软还有中 msi 安装格式
"arch": ["x64"] // 生成 64 位安装包
}
]
},
"nsis": {
"oneClick": false, // 设置为 false 使安装程序显示安装向导界面,而不是一键安装
"perMachine": true, // 允许每台机器安装一次,而不是每个用户都安装
"allowToChangeInstallationDirectory": true // 允许用户在安装过程中选择安装目录
}
}
}
json 文件不支持 注释,这里仅仅给大家看含义
打包的产物会放到 dist
文件夹目录下面,你可能会发现没写几行 js
的 eletron
打包成应用也有 70+ M
的安装包,这其实就包含了 node
环境和 Chromium
环境,当你安装完毕发现大小又变大了,可能会有 250+ M
electron-vite
我们一般不会去真的去写 html
,js
,css
,一般都是用 vue
,react
,其中 electron-vite
就是让我们用 vue/react + electron
的形式去开发
electron-vite
如何支持 Vue
和 React
-
渲染进程支持
electron-vite
为渲染进程提供了对vue/react
等现代前端框架的支持 -
模块热替换(HMR)
在开发过程中,
electron-vite
为渲染进程提供了快速的模块热替换功能,这样修改vue
,react
组价时,无需重启应用即可实时看到更改效果
帮助开发
nodemon
我们修改 js
文件时,若发现需要手动重启才能生效,可以试着用 nodemon
这个插件
nodemon.json
若修改的是 html
文件,我们希望可以立即生效,那么就需要配置 nodemon.json
文件
json
{
"ignore": [
"node_modules",
"dist"
],
"restartable": "r", // 项目运行阶段在控制台输入 r 也可以拥有重启的效果
"watch": ["*.*"], // 监视所有文件
"ext": "html,js,css" // 包括 html, js, css 文件
}
最后
其实学习 electron
最重要的就是学习它的 进程间通信(IPC
),这部分搞明白了,其余内容需要用到是直接参考 electron
官网看 api
就行,其实这个东西本质就是发布订阅模式,所以还是非常好理解的,这也就是大家为何喜欢用 electron
去开发应用,主要就是因为学习成本低以及生态完善,应用体积大就是因为每个软件本质上都包含了完整的 Chromium
浏览器引擎 和 Node.js
运行时,这也无可厚非
文章中若出现错误内容还请各位大佬见谅并指正。如果有任何问题或建议,欢迎指出,另外,有不懂之处欢迎在评论区留言。如果觉得文章对你的学习有所帮助,还请 关注、点赞、收藏 一键三连,感谢支持!欢迎关注我的公众号:
Dolphin_Fung