electron入坑指南

936 阅读6分钟

概述

Electron核心组成分为三个部分,ChromiumNode.jsNative apis。这三部分共同构成了Electron架构。

Chromium可以理解为浏览器,用于支持htmlcssjs页面构成。Native apis用于支持和操作系统的一些交互操作。Node.js用于操作文件等。

Electron可以看成一个框架,将chromiumNode.js整合到一起,它允许通过web技术构建程序,可以通过Native apis对操作系统进行一切操作。 Electron内部存在不同的进程,一个是主进程,一个是渲染进程,当启动app的时候首先会启动主进程,主进程完成之后就会创建Native UI, 会创建一个或者多个 window,用window来呈现界面也就是web界面。每个window都可以看做一个渲染进程,各进程间相互独立,不同窗口数据可以通过rpc或者ipc通信 主进程可以看做是package.jsonmain属性对应的文件。一个应用只能有一个主进程,只要主进程可以进行GUIAPI操作。主进程可以管理所有的web界面和渲染进程。

安装electron

设置electron淘宝镜像

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

mkdir my-electron-app && cd my-electron-app\
npm init
{\
"scripts": {\
"start": "electron ."\
}\
}

全局安装

npm install -g electron

创建index.html

<!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>\
<h1>Hello World!</h1>\
We are using Node.js <span id="node-version"></span>,\
Chromium <span id="chrome-version"></span>,\
and Electron <span id="electron-version"></span>.\
</body>\
</html>

创建mian.js-主进程

 const { app, BrowserWindow } = require('electron')
 const path = require('path')
 /**app 控制着整个应用程序的事件生命周期, BrowserWindow创建和管理应用程序的窗口 */
 function createWindow() {
     const win = new BrowserWindow({
         /**全屏 */
         //fullscreen: false,
         /**让桌面应用没有边框,这样菜单栏也会消失 */
         //frame: false,
         /**不允许用户改变窗口大小*/
         // resizable: false,
         /**设置窗口宽高 */
         width: 800,
         height: 600,
         /**应用运行时的标题栏图标 */
         //icon: iconPath,
         /**最小宽度 */
         //minWidth: 300,
         /**最小高度 */
         // minHeight: 500,
         /**最大宽度*/
         // maxWidth: 300,
         /**最大高度 */
         //maxHeight: 600,
         /**进行对首选项的设置 */
         webPreferences: {
             preLoad: path.join(__dirname, 'preload.js'),
             /**允许node环境运行 */
             NodeIterator: true,
             /**设置应用在后台正常运行*/
             backgroundThrottling: false,
             /**关闭警告信息*/
             contextIsolation: false,
             /**在主进程的窗口中加入enableRemoteModule: true参数才能够调用remote模块*/
             enableRemoteModule: true
         }
     })
     /**并且为你的应用加载index.html */
     win.loadFile('index.html')
     /**打开开发者工具 */
     //win.webContents.openDevTools()
 }
 /**在electron中,只有app 模块的ready事件被触发才会创建浏览器窗口,我们可以通过app.whenReady()进行监听 */
 app.whenReady().then(() => {
     createWindow()
     /**没有窗口打开就打开一个窗口 activate */
     app.on('activate', function () {
         if (BrowserWindow.getAllWindows().length === 0) createWindow()
     })
 })
 /**关闭所有窗口时退出应用(window-all-closed/app.quit) */
 app.on('window-all-closed', function () {
     if (ProcessingInstruction.platform !== 'darwin') app.quit()
 })

创建preload.js

/**输出Electron的版本号和它的依赖项到你的web页面上。 */
window.addEventListener('DOMContentLoaded',() => {
    const replaceText = (selector,text) =>{
        const element = document.getElementById(selector)
        if(element) element.innerText = text
    }
    for(const dependency of ['chrome','node','electron']){
        replaceText(`${dependency}-version`,process.versions[dependency])
    }
})

打包

npm install --save-dev @electron-forge/cli\
npx electron-forge import

分发

npm run make
  • 安装过程中因为electron安装使用的是cnpm 安装,导致出现最后打包报错,建议安装使用统一的,如果慢先安装electron淘宝镜像

生命周期

1. ready: app初始化完成

app.on('ready', () => {
    const mainWin = new BrowserWindow({
        width: 800,
        height: 400
    })
})

2. dom-ready: 一个窗口中的文本文件加载完成

webContents用于控制当前窗口的内容,每个窗口都有一个webContents

mainWin.webContents.on('dom-ready', () => {
    
})

3. did-finsh-load: 导航完成时触发,dom-ready之后

mainWin.webContents.on('did-finsh-load', () => {
    
})

4. window-all-closed: 所有窗口都关闭时触发,如果没有监听会直接退出

app.on('window-all-closed', () => {

})

5. before-quit: 在关闭窗口之前触发

app.on(' before-quit', () => {
    
})

6. will-quit: 在窗口关闭并且应用退出时触发

app.on('will-quit', () => {
    
})

7. quit: 当所有窗口被关闭时触发

app.on('quit', () => {
    
})

8. close: 窗口关闭

mainWin.on('close', () => {
    mainWin = null;
})

进程通信

1. 渲染进程向主进程发送消息,主进程接收消息。

渲染进程发送消息

const { ipcRenderer } = require('electron');

window.onload = function() {
   
    // 采用异步消息向主线程发送消息
    ipcRenderer.send('msg1', '参数字符串')
    // 采用同步方式发送消息
    ipcRenderer.sendSync('msg3', '同步消息')
}

主进程接收消息

const { ipcMain } = require('electron');
ipcMain.on('msg1', (e, data) => {
    console.log(e, data);
})
ipcMain.on('msg3', (e, data) => {
    console.log(e, data);
})

2. 主进程发送消息给渲染进程

主进程发送消息

const { ipcMain } = require('electron');
 /**主线程 到 渲染线程 通过 webContents.send 来发送 --->ipcRenderer.on 来监听 */
    /**
     * webContents.send(channel, ...args)
       channel String
       ...args any[]
    */
setTimeout(() => {
  win.webContents.send('mainMsg', '我是主线程')

}, 1000);
ipcMain.on('msg1', (e, data) => {
    console.log(e, data);
    // e.sender就是ipcMain
    e.sender.send('msg2', '参数字符串')
})

渲染进程接收消息

const { ipcRenderer } = require('electron');

window.onload = function() {
    // 采用异步消息向主线程发送消息
    ipcRenderer.send('msg1', '参数字符串')
    
}

ipcRenderer.on('mainMsg', (e, data) => {
    console.log(e, data)
    document.getElementById('receive').innerText = data

})
ipcRenderer.on('msg2', (ev, data) => {
    console.log(e, data);
})

创建菜单

menu.js

const { Menu, BrowserWindow } = require('electron')
console.log(Menu, BrowserWindow,1)

const template = [
    {
        label: '菜单一',
        /** submenu 代表下一级菜单 */
        submenu: [
            {
                label: '子菜单一',
                click: () => {
                    const newWin = new BrowserWindow({
                        width: 200,
                        height: 200
                    })

                    win.loadFile('./index2.html')
                    newWin.on('close', () => {
                        newWin = null
                    })
                },
                /** 添加快捷键 */
                accelerator: 'ctrl+n'
            },
            { label: '子菜单二' },
            { label: '子菜单三' },
            { label: '子菜单四' },
        ],
    },
    {
        label: '菜单二',
        /** 代表下一级菜单 */
        submenu: [
            { label: '子菜单一' },
            { label: '子菜单二' },
            { label: '子菜单三' },
            { label: '子菜单四' },
        ],
    }
]
console.log(222)

/** 3.从模板中创建菜单 */
const myMenu = Menu.buildFromTemplate(template)

/** 4.设置为应用程序菜单 */
Menu.setApplicationMenu(myMenu)

main.js


const { app, BrowserWindow } = require('electron')
const { ipcMain } = require('electron')

let win = null

require('./menu')
app.on('ready', function () {
    // require('@electron/remote/main').initialize()
    win = new BrowserWindow({
        width: 800,
        height: 600,
        /**进行对首选项的设置 */
        webPreferences: {
            /**是否集成 Nodejs,把之前预加载的js去了,发现也可以运行*/
            nodeIntegration: true,
            contextIsolation: false,
            /**在主进程的窗口中加入enableRemoteModule: true参数才能够调用remote模块*/
            enableRemoteModule: true
        }
    })
    
   
    win.webContents.openDevTools()
    win.loadFile('./main.html')
   
    win.on('close', () => {
        win = null
    })
  

node Api

在主线程创建窗口的时候 webPreferences一定在加上 nodeIntegration: truecontextIsolation: false main.js


const { app, BrowserWindow } = require('electron')
const { ipcMain } = require('electron')

let win = null

app.on('ready', function () {
    // require('@electron/remote/main').initialize()
    win = new BrowserWindow({
        width: 800,
        height: 600,
        /**进行对首选项的设置 */
        webPreferences: {
            /**是否集成 Nodejs,把之前预加载的js去了,发现也可以运行*/
            nodeIntegration: true
        }
    })
    
   
    win.webContents.openDevTools()
    win.loadFile('./main.html')
   
    win.on('close', () => {
        win = null
    })

index.js

const fs = require('fs')
const path = require('path')
const {log} = console

const showContent = document.getElementById('show_file_content')

function readFile(){
    console.log('读取文件')
    fs.readFile(path.join(__dirname,'a.txt'),(err,data)=>{
        if(err){
            throw new Error(err,'读取失败')
        }
        log(data)
        showContent.innerText = data
    })
}

const content = '今天学习electron'
function writeFile(){
    console.log('写入文件')
    fs.writeFile(path.join(__dirname,'b.txt'),content,'utf-8',(err,data)=>{
        if(err){
           throw new Error(err,'写入失败')
        }
       log('写入成功')  
    })
}

index.html

<!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>node获取api</title>
</head>

<body>
    <button onclick="readFile()">读取文件</button>
    <button onclick="writeFile()">写入文件</button>
    <p id="show_file_content">页面内容</p>
    <script src="./index.js"></script>
</body>

</html>

其他

弹窗

错误框

dialog.showErrorBox('警告', '操作有误');
复制代码

消息框

remote.dialog.showMessageBox({
    type: 'info',
    title: '提示信息',
    message: '内容',
    buttons: ['确定', '取消']
  });
复制代码

打开文件

dialog.showOpenDialog(
    {
      properties: ['openFile']
    },
    function(date) {
      console.log(date);
    }
  );
复制代码

保存文件

dialog.showSaveDialog(
    {
      title: 'save file',
      filters: [
        { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
        { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] },
        { name: 'Custom File Type', extensions: ['as'] },
        { name: 'All Files', extensions: ['*'] }
      ]
    },
    filename => {
      console.log(filename);
    }
  );

进度条

mainWindow.setProgressBar(0.3) 
mainWindow.setProgressBar(-1) // 删除进度条

消息通知

可以借助H5的消息通知功能来完成。

const notify = new window.Notification('标题', {
    title: '标题',
    body: '内容',
    icon: './icon.png'
})

notify.onclick = function() {
    console.log('点击了消息')
}

系统托盘

设置系统托盘菜单和图标

const { Menu, Tray, BrowserWindow, app } = require('electron');
const path = require('path');
let iconTray = new Tray(path.join(__dirname, '../static/icon.png'));
let tpl = [
  {
    label: '设置',
    click: function() {
      console.log('setting');
    }
  },
  {
    label: '升级',
    click: function() {
      console.log('update');
    }
  },
  {
    label: '退出',
    click: function() {
      if (process.platform !== 'darwin') {
        app.quit();
      }
    }
  }
];
let trayTemplte = Menu.buildFromTemplate(tpl);
iconTray.setContextMenu(trayTemplte);
iconTray.setToolTip('地图');

快捷键

快捷点都是针对主进程的,快捷键需要在初始化完成之后绑定。销毁前清除。

const { globakShortcut } = require('electron')
app.on('ready', () => {
    const ret = globakShortcut('ctrl + q', () => {
        console.log('触发了快捷键')
    })
    if (!ret) {
        console.log('注册失败')
    }
    // 是否注册
    console.log(globakShortcut.isRegistered('ctrl + q'))
})

快捷键取消需要在will-quite生命周期中来做。

app.on('will-quit', () => {
    // 清除单个
    globakShortcut.unregister('ctrl + q')
    // 清除所有注册
    globakShortcut.unregisterAll()
})

剪切板

剪切板也是不限制线程的模块,可以在任意线程中使用。

const { clipboard } = require('electron')
clipboard.writeText('写入剪切板');
// 读取剪切板
clipboard.readText;
复制代码

将图片copy到剪切板

const { clipboard, nativeImage } = require('electron')
const image = nativeImage.createFromPath('./aaa.png')
clipboard.writeImage(image);

// 渲染到页面
img.src = image.toDataURL()

调试

这块后面还需要学习如何调试