electron(doc云文档)(1)通讯及相关

175 阅读5分钟

背景

  • 基于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/remote模块

Electron 进程间通信

  1. 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);
    })
})
  1. 如果 react 使用 webpack 构建时,webpack会识别2中模块,import 和 require ; 在渲染进程中使用node模块时,注意用 window.require

github.com/electron/el…

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');
  1. 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]

扩展

  1. 进程间通信IPC ,有管道,消息队列,共享内存,信号量,信号,socket

  2. 信号,就是给某个特定的进程,(可以指定进程的pid),发送信号,让进程执行对应的操作

如,键盘事件信号,ctol+c,取消进程

如,命令信号 kill -9 pid 杀死进程

主进程通知渲染进程

  1. 用 ipcMain.send ,子进程 ipcRenderer.on

主进程通知主进程

  1. 应用 ipcMain 都是一个基于 Emitter 的实例,所有系统菜单发消息要弹框时,可以用 emit
const { ipcMain } = require('electron')
// menuTemplate.js 中,点击菜单发起
ipcMain.emit('open-settings-window')

// main.js 中监听这个事件
ipcMain.on('open-settings-window')

工具

  1. 需要同时执行多条命令,如启动 react 和 electron,取消时要全部取等

npm script : "concurrently "wait-on http://localhost:3000 && electron ." "cross-env BROWSER=none npm start""

  • concurrently 并行执行
  • wait-on 等待资源
  • cross-env 跨平台设置环境变量
  1. @electron/remote 远程对象
  2. 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

  1. 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])
  • 系统菜单
  1. 先写好系统快捷键,和实现方法(很多功能系统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') }
        },
      ]
    }
  ]
},
  1. 再在 main 主进程中,加载模板
app.on('ready', () => {
    // ...

    // set useMenu
    const menu = Menu.buildFromTemplate(menuTemplate)
    Menu.setApplicationMenu(menu)
    
})
  1. 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 应用引入静态文件

  1. 如 electron 引入 html 中,引入js,可以直接使用 require('./settings.js'),这是 electron 环境中赋予的

小图标

  1. 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",
  1. 使用
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faSearch, faTimes } from '@fortawesome/free-solid-svg-icons'

 <FontAwesomeIcon
    title="搜索"
    size="lg"
    icon={faSearch}
/>

其他参考文章

基于Electron开发桌面应用的技术实践