背景
- 基于Electron+七牛云开发桌面在线文档管理
目录结构
- react 打包用 cra 自带的 react-scripts
- electron 代码用单独的 webpack 打包,打包进和 react 文件夹里面
├── assets // electron 一些静态icon的文件,在pkg中有引用
├── build // react 打包后项目
├── dist // electron 打包后项目,里面包含安装包
├── main.js // electron 主入口
├── package.json
├── public
├── settings // 设置页面(web端) 一个单独的页面
├── src // react项目
├── App.css
├── App.js
├── AppWindow.js
├── components
├── hooks
├── index.css
├── index.js
├── logo.svg
├── menuTemplate.js // electron 菜单配置文件
├── reportWebVitals.js
├── setupTests.js
├── test
└── utils
└── webpack.config.js // 单独webpack 打包 electron 代码的配置文件
进程之间的通讯方式
- electron 用 IPC 在进程之间通讯,为什么进程之间还用通讯?不是每个单独的运行吗?
- 因为 electron 中,main process 主进程可以操作更多的系统 api,render process 需要通过通讯的方式告知主进程
// 主进程 main.js
const {app, BrowserWindow, ipcMain} = require('electron')
let mainWindow = new BrowserWindow({
width: 1600,
height: 600,
webPreferences: {
nodeIntegration: true, // 注意声明渲染进程可以使用 node 模块
contextIsolation:false // 注意声明渲染进程可以使用 node 模块
}
})
ipcMain.on('message', (event, arg) => {
console.log("-> arg", arg);
event.reply('reply','hello from main') // event 拿到通知过来的event
})
// 渲染进程 renderer.js
const {ipcRenderer} = require('electron')
window.addEventListener('DOMContentLoaded', function () {
document.getElementById('send').addEventListener('click', () => {
ipcRenderer.send('message', 111)
})
ipcRenderer.on('reply', (event, arg) => {
console.log("-> arg", arg);
})
})
- 如果 react 使用 webpack 构建时,webpack会识别2中模块,import 和 require ; 在渲染进程中使用node模块时,注意用 window.require
const fs = require('fs') // require 会被 webpack 拦截掉,会到 node_module 找,而不是 node 模块
console.dir(fs)
- 修正,使用window.require
const fs = window.require('fs') // webpack 会忽略 window.require
console.dir(fs)
// 如果是 ts 还要声明
declare global {
interface Window {
require: any;
}
}
const electron = window.require('electron');
- const remote = window.require('@electron/remote')
-
远程模块返回的对象代表主进程中的一个对象,称为远程对象;调用远程对象的方法,调用远程函数时,实际上是在发送同步进程间消息。
-
远程对象的生命周期
- Electron 确保只要渲染进程中的远程对象还活着(换句话说,还没有被垃圾回收),主进程中的相应对象就不会被释放。 当远程对象被垃圾回收后,主进程中的相应对象将被取消引用。
-
将回调传递给主进程
- 为了避免死锁,传递给主进程的回调是异步调用的。 您不应该期望主进程获得传递的回调的返回值
- 注意:当通过远程模块访问时,数组和缓冲区是通过 IPC 复制的。 在渲染器进程中修改它们不会在主进程中修改它们,反之亦然。
// 主进程mapNumbers.js
exports.withRendererCallback = (mapper) => {
return [1, 2, 3].map(mapper)
}
exports.withLocalCallback = () => {
return [1, 2, 3].map(x => x + 1)
}
// 渲染进程
const mapNumbers = require('@electron/remote').require('./mapNumbers')
const withRendererCb = mapNumbers.withRendererCallback(x => x + 1)
const withLocalCb = mapNumbers.withLocalCallback()
console.log(withRendererCb, withLocalCb)
// [undefined, undefined, undefined], [2, 3, 4]
扩展
进程间通信IPC ,有管道,消息队列,共享内存,信号量,信号,socket
信号,就是给某个特定的进程,(可以指定进程的pid),发送信号,让进程执行对应的操作
如,键盘事件信号,ctol+c,取消进程
如,命令信号 kill -9 pid 杀死进程
主进程通知渲染进程
- 用 ipcMain.send ,子进程 ipcRenderer.on
主进程通知主进程
- 应用 ipcMain 都是一个基于 Emitter 的实例,所有系统菜单发消息要弹框时,可以用 emit
const { ipcMain } = require('electron')
// menuTemplate.js 中,点击菜单发起
ipcMain.emit('open-settings-window')
// main.js 中监听这个事件
ipcMain.on('open-settings-window')
工具
- 需要同时执行多条命令,如启动 react 和 electron,取消时要全部取等
npm script : "concurrently "wait-on http://localhost:3000 && electron ." "cross-env BROWSER=none npm start""
- concurrently 并行执行
- wait-on 等待资源
- cross-env 跨平台设置环境变量
- @electron/remote 远程对象
- fs 可读流,可写流,转化流
- 转换流,可以在流中,做一些加工转换
const fs = require('fs')
const zlib = require('zlib')
const rs = fs.createReadStream('./helper.js')
const ws = fs.createWriteStream('./1.gz')
rs.pipe(process.stdout)
rs.pipe(zlib.createGzip()).pipe(ws) // 转换流
electron
- menu 菜单
- 分为上下文菜单和系统菜单
// 1. renderer 渲染进程 ./hooks/useContextMenu 添加菜单item
const remote = window.require('@electron/remote')
const { Menu, MenuItem } = remote
useEffect(() => {
const menu = new Menu()
itemArr.forEach(item => {
menu.append(new MenuItem(item)) // item : {label:,value:}
})
const handleContextMenu = (e) => {
// only show the context menu on current dom element or targetSelector contains target
if (document.querySelector(targetSelector).contains(e.target)) {
clickedElement.current = e.target
// 当前窗口弹出
menu.popup({window: remote.getCurrentWindow() })
}
// 添加事件
window.addEventListener('contextmenu', handleContextMenu)
return () => {
window.removeEventListener('contextmenu', handleContextMenu)
}
}, deps)
// 2. page 页面中调用
const clickedItem = useContextMenu([
{
label: '打开',
click: () => {
const parentElement = getParentNode(clickedItem.current, 'file-item')
if (parentElement) {
onFileClick(parentElement.dataset.id)
}
}
}, // 这些就是 menu 里的 item
], '.file-list', [files])
- 系统菜单
- 先写好系统快捷键,和实现方法(很多功能系统electron已经自带)
- 自带的,直接写快捷键,加系统自带的功能即可role: 'minimize'
- 需要自定义的,用主进程发送给当前渲染进程的方式,实现,browserWindow.webContents.send('create-new-file')
// ./src/menuTemplate.js 定义默认的菜单模板
const { app, shell } = require('electron') // shell 可以执行更多操作
let template = [{
label: '文件',
submenu: [{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: (menuItem, browserWindow, event) => {
browserWindow.webContents.send('create-new-file')
}
},{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: (menuItem, browserWindow, event) => {
browserWindow.webContents.send('save-edit-file')
}
},{
label: '搜索',
accelerator: 'CmdOrCtrl+F',
click: (menuItem, browserWindow, event) => {
browserWindow.webContents.send('search-file')
}
},{
label: '导入',
accelerator: 'CmdOrCtrl+O',
click: (menuItem, browserWindow, event) => {
browserWindow.webContents.send('import-file')
}
},
{
label: '窗口',
role: 'window',
submenu: [{
label: '最小化',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
}, {
label: '关闭',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}]
},
{
label: '帮助',
role: 'help',
submenu: [
{
label: '学习更多',
click: () => { shell.openExternal('http://electron.atom.io') }
},
]
}
]
},
- 再在 main 主进程中,加载模板
app.on('ready', () => {
// ...
// set useMenu
const menu = Menu.buildFromTemplate(menuTemplate)
Menu.setApplicationMenu(menu)
})
- renderer 渲染进程中,监听主进程发来的事件
// renderer.js
const { ipcRenderer } = window.require('electron')
useEffect(()=>{
const cb = ()=>{
console.log('hello from main');
}
ipcRenderer.on('create-new-file',cb)
return ()=>{
ipcRenderer.removeListener('create-new-file',cb)
}
})
electron 应用引入静态文件
- 如 electron 引入 html 中,引入js,可以直接使用 require('./settings.js'),这是 electron 环境中赋予的
小图标
- fortawesome 安装
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-brands-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
- 使用
- fontawesome.com/icons/searc… 可以在这边搜索自己想要的图标
- 名称前面加上 fa- 就可以了
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faTimes } from '@fortawesome/free-solid-svg-icons'
<FontAwesomeIcon
title="搜索"
size="lg"
icon={faSearch}
/>