This repository has been archived on 2025-01-09. You can view files and clone it, but cannot push or open issues or pull requests.
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00
2025-01-09 18:30:49 +08:00

Electron 入门

WARNING: 文章部分内容已经过时,仅适用于 Electron ^11.2.1

技术架构

Electron = Chromium + Node.js + Native APIs

工作流程

启动APP -> 主进程创建window -> win加载界面 -> xxx行为

主进程

  • 只有主进程才可以进行GUI的API操作
  • 只有一个

渲染进程

  • 可以有多个
  • 和主进程通信完成原生API操作

环境搭建

  1. 使用官方 Quick Start
$ git clone https://github.com/electron/electron-quick-start
$ cd electron-quick-start 
$ npm install 
// 01 创建一个窗口
// 02 让窗口加载了一个界面这个界面就是用Web技术实现 运行在渲染进程
function createWindow () {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  // and load the index.html of the app.
  mainWindow.loadFile('index.html')

  // Open the DevTools.
  // mainWindow.webContents.openDevTools()
  // ...
}
  1. 手动创建项目
// main.js
const {app, BrowserWindow} = require('electron') // 导包

// 当 app 启动之后执行窗口创建等操作
app.whenReady().then(() => {
	const mainWin = new BrowserWindow({
		width: 600,
		height: 400
	})
	
	// 在当前窗口中加载指定界面让它显示具体的内容
	mainWin.loadFile('index.html')
	
	mainWin.on('close', () => {
		// ...
	})
})

app.on('window-all-closed', () => {
	// ...
	app.quit()
})
<!-- index.html -->
<!doctype html>
<head>
	<title>这个窗口标题</title>
</head>

<body>
	<h1>嘿!这是一段文字!</h1>
</body>
// package.json
{
	"name": "xxxx", // 项目的名字
	"version": "x.x.x", // 项目的版本
	"description": "", // 描述
	"main": "main.js", // 入口文件
	"scripts": {
		"start": "electron ." // npm start
	},
	"keywords": [], // 关键词?
	"author": "", // 作者
	"license": "ISC", // 协议
	"devDependencies": {
		"electron": "^11.2.1" // 开发依赖
	}
}

生命周期

ready: app初始化完成

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

did-finsh-load: 导航完成时出发

window-all-closed: 所有窗口都被关闭时触发

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

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

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

closed: 当窗口关闭时触发,此时删除窗口引用

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

function createWindow() {
    let mainWin = new BrowserWindow({
        width: 600,
        height: 400
    })

    mainWin.loadFile('index.html')

    mainWin.webContents.on('did-finish-load', () => {
        console.log('3 - did finish load')
    })

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

    mainWin.on('close', () => {
        console.log('8 - win closed') // 多窗口时最后触发
        mainWin = null // 删除引用,释放空间
    })
}

app.on('ready', () => {
    console.log('1 - ready')
    createWindow()
})

app.on('window-all-closed', () => {
    console.log('4 - window all closed')
    app.quit()
})

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

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

app.on('quit', () => {
    console.log('7 - quit')
})

窗口大小

function createWindow() {
    let mainWin = new BrowserWindow({
        x: 100, // 相对于屏幕左上角
        y: 100,
        show: false, // 防止首页白屏
        width: 800,
        height: 400,
        maxHeight: 600, // 最大值
        maxWidth: 1000,
        minHeight: 200, // 最小值
        minWidth: 300,
        resizable: false // 固定大小,禁止缩放
    })

    // 防止首页白屏,否则不显示
    mainWin.on('ready-to-show', () => {
        mainWin.show()
    })
    
    // ...
}

窗口标题及环境

// main.js
function createWindow() {
    let mainWin = new BrowserWindow({
        width: 800,
        height: 600,
        show: false,
        title: "标题", // 要在网页里的标题去除
        icon: "w.png", // 设置图标
        frame: true, // 是否显示标题栏
        transparent: false, // 透明窗体
        autoHideMenuBar: true, // 隐藏菜单
        webPreferences: {
            nodeIntegration: true, // Node集成环境
            enableRemoteModule: true, // 开启远程模块 remote
        }
    })
    
    // ...
}
// index.js
const { remote } = require('electron')

window.addEventListener('DOMContentLoaded', () => {
    // 点击按钮打开新窗口
    const oBtn = document.getElementById('btn')
    oBtn.addEventListener('click', () => {
        // 创建窗口
        let indexMain = new remote.BrowserWindow({
            width: 200,
            height: 200
        })

        indexMain.loadFile('list.html')

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

自定义窗口的实现

<!-- index.html -->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
		/* ... */
    </style>
</head>
<body>
    <div class="menu-container">
        <div class="left">
            <div class="logo"></div>
            <div class="title">Electron 自定义标题栏</div>
        </div>
        <div class="right">
            <div class="function min">
                <!-- ... -->
            </div>
            <div class="function max">
                <!-- ... -->
            </div>
            <div class="function close">
                <!-- ... -->
            </div>
        </div>
    </div>

    <div class="app">
        <h2>主体内容</h2>
    </div>

    <script src="index.js"></script>
</body>
</html>
// index.js
const { remote } = require('electron')

window.addEventListener('DOMContentLoaded', () => {
    // 利用 remote 获取当前窗口对象
    let mainWin = remote.getCurrentWindow()

    // 获取元素添加点击操作的监听
    let aBtns = document.getElementsByClassName('function')

    aBtns[0].addEventListener('click', () => {
        // 最小化
        mainWin.minimize()
    })

    aBtns[1].addEventListener('click', () => {
        // 最大化
        mainWin.isMaximized() ? mainWin.restore() : mainWin.maximize()
    })

    aBtns[2].addEventListener('click', () => {
        // 关闭窗口操作
        mainWin.close()
    })
})

阻止窗口关闭

<!-- index.html -->
<html lang="en">
<head>
	<!-- ... -->
</head>
<body>
	<!-- ... -->
    <div class="dialog">
        <div class="message">
            是否关闭?
        </div>
        <span class="confirm yes"></span>
        <span class="confirm no"></span>
    </div>

    <script src="index.js"></script>
</body>
</html>
// index.js
const { remote } = require('electron')

window.addEventListener('DOMContentLoaded', () => {

    window.onbeforeunload = function () {
        let oBox = document.getElementsByClassName('dialog')[0]
        oBox.style.display = 'block'

        let yBtn = oBox.getElementsByClassName('confirm')[0]
        let nBtn = oBox.getElementsByClassName('confirm')[1]

        yBtn.addEventListener('click', () => {
            mainWin.destroy() // 不用 close 防止死循环
        })

        nBtn.addEventListener('click', () => {
            oBox.style.display = 'none'
        })

        return false
    }
    
    // ...
}

父子及模态窗口

// index.js
window.addEventListener('DOMContentLoaded', () => {
    let oBtn = document.getElementById('btn')
    oBtn.addEventListener('click', () => {
        let subWin = new remote.BrowserWindow({
			// ...
            parent: remote.getCurrentWindow(), // 设置父窗口
            modal: true, // 设置模态窗口
        })
		// ...
    })
})

自定义菜单

Electorn文档 Menu部分

// ...
console.log(process.platform) // 平台判断
// ...
const createWindow = () => {
    let mainWin = new BrowserWindow({
		// ...
    });

    // 定义自己需要的菜单项
    let menuTemp = [
        {
            label: "文件",
            submenu: [
                {
                    label: "打开文件",
                    click() {
                        console.log("打开文件夹")
                    }
                },
                {
                    type: 'separator' // 分隔符
                },
                {
                    label: "关闭文件夹"
                },
                {
                    label: "关于",
                    role: 'about'
                },
            ],
        },
        { label: "编辑" },
    ];

    // 利用上述模板生成一个菜单项
    let menu = Menu.buildFromTemplate(menuTemp);

    // 添加到应用里
    Menu.setApplicationMenu(menu);

    // ...
};

// ...

菜单角色及类型

function createWindow() {
    let mainWin = new BrowserWindow({
		// ...
    })

    // 01 自定义菜单的内容
    let menuTemp = [
        {
            label: '角色',
            submenu: [
                { label: '复制', role: 'copy' },
                { label: '剪切', role: 'cut' },
                { label: '粘贴', role: 'paste' },
                { label: '最小化', role: 'minimize' }
            ]
        },
        {
            label: '类型',
            submenu: [
                { label: '选项1', type: 'checkbox' },
                { label: '选项2', type: 'checkbox' },
                { label: '选项3', type: 'checkbox' },
                { type: 'separator' },
                { label: 'item1', type: 'radio' },
                { label: 'item2', type: 'radio' },
                { type: 'separator' },
                { label: 'windows', type: 'submenu', role: 'windowMenu'}
            ]
        },
        {
            label: '其他',
            submenu: [
                {
                    label: '打开',
                    icon: 'open.png', // 自定义图标
                    accelerator: 'ctrl + o', // 快捷键
                    click() {
                        console.log('open 执行了')
                    }
                }
            ]
        }
    ]

    // 02 依据上述数据创建一个 menu
    let menu = Menu.buildFromTemplate(menuTemp)

    // 03 将上述的菜单添加至 app 中
    Menu.setApplicationMenu(menu)

    // ...

动态创建菜单

<!-- index.html -->
<html lang="en">
<head>
	<!-- ... -->
</head>
<body>
    <button id="addMenu">创建自定义菜单</button>
    <input type="text" placeholder="输入自定义菜单项内容" id="menuCon">
    <button id="addItem">添加菜单项</button>
</body>
</html>
// index.js
const { remote } = require('electron')
const Menu = remote.Menu
const MenuItem = remote.MenuItem

window.addEventListener('DOMContentLoaded', () => {
    // 获取相应的元素
    let addMenu = document.getElementById('addMenu')
    let menuCon = document.getElementById('menuCon')
    let addItem = document.getElementById('addItem')

    // 自定义全局变量存放菜单项
    let menuItem = new Menu()

    // 生成自定义的菜单
    addMenu.addEventListener('click', () => {
        // 创建菜单
        let menuFile = new MenuItem({ label: '文件', type: 'normal' })
        let menuEdit = new MenuItem({ label: '编辑', type: 'normal' })
        let customMenu = new MenuItem({ label: '自定义菜单项', submenu: menuItem })

        // 将创建好的自定义菜单添加至 menu
        let menu = new Menu()
        menu.append(menuFile)
        menu.append(menuEdit)
        menu.append(customMenu)

        // 将 menu 添加至 app 中显示
        Menu.setApplicationMenu(menu)
    })

    // 动态添加菜单项
    addItem.addEventListener('click', () => {
        // 获取当前 input 输入框中的元素
        let con = menuCon.vaule.trim()
        if (con) {
            menuItem.append(new MenuItem({ label: con, type: 'normal' }))
            menuCon.vaule = ''
        }
    })
})

右键菜单的创建

// index.js
const { remote } = require('electron')
const Menu = remote.Menu

let contextTemp = [
    {lable: 'Run Code'},
    {lable: '转到定义'},
    {type: 'separator'},
    {
        lable: '其他功能',
        click() {
            alert('其他功能选项');
        }
    },
]

let menu = Menu.buildFromTemplate(contextTemp)

// 给鼠标添加监听
window.addEventListener('DOMContentLoaded', () => {
    window.addEventListener('contextmenu', (ev) => {
        ev.preventDefault()
        menu.popup({window: remote.getCurrentWindow()})
    }, false)
})

/**
 * 01 创建一个自定义的菜单内容
 * 02 在鼠标右击行为发生后显示出来
 */

主进程与渲染进程通信

// main.js
const createWindow = () => {
    let mainWin = new BrowserWindow({
		// ...
    })

    let temp = [
        {
            label: 'send',
            click() {
                // 主进程主动与渲染进程发送消息
                BrowserWindow.getFocusedWindow().webContents.send('mtp', '来自于主进程的消息')
            }
        }
    ]

    let menu = Menu.buildFromTemplate(temp)
	// ...
}

// ...

// 主进程接收消息操作
ipcMain.on('msg1', (ev, data) => {
    console.log(data)
    ev.sender.send('msg1Re', '来自于主进程的异步消息')
})

ipcMain.on('msg2', (ev, data) => {
    console.log(data)
    ev.returnValue = '来自于主进程的同步消息'
})
// index.js
const { ipcRenderer } = require('electron')

window.onload = () => {
    // 获取元素
    let aBtn = document.getElementsByTagName('button')
    
    // 01 采用异步的 API 在渲染进程中给主进程发送信息
    aBtn[0].addEventListener('click', () => {
        ipcRenderer.send('msg1', '来自于渲染进程的一条异步消息')
    })

    // 02 采用同步的 API 在渲染进程中给主进程发送信息
    aBtn[1].addEventListener('click', () => {
        let val = ipcRenderer.sendSync('msg2', '来自于渲染进程的一条同步消息')
        alert(val)
    })

    // 接收消息的区域
    ipcRenderer.on('msg1Re', (ev, data) => {
        alert(data)
    })

    ipcRenderer.on('mtp', (ev, data) => {
        alert(data)
    })
}

渲染进程间通信

通过本地储存存储

// main.js
// ...

// 定义全局变量存放主窗口 Id
let mainWinId = null

const createWindow = () => {
    let mainWin = new BrowserWindow({
        // ...
    })
    
    mainWinId = mainWin.id
    // ...
}

// 接受其他进程发送的数据,完成后续的逻辑
ipcMain.on('openWin2', () => {
    // 接收到渲染进程中按钮点击信息之后完成窗口2的打开
    let subWin1 = new BrowserWindow({
        // ...
        parent: BrowserWindow.fromId(mainWinId), // 或定义全局变量
        // ...
    })
    // ...
})

// ...
// index.js
window.onload = () => {
    // 先获取元素
    let oBtn = document.getElementById('btn')

    oBtn.addEventListener('click', () => {
        ipcRenderer.send('openWin2')

        // 打开窗口2之后保存数据至本地储存
        localStorage.setItem('name', '啦啦啦啦')
    })
}
// subWin1.js
window.onload = () => {
    let oInput = document.getElementById('txt')
    let val = localStorage.getItem('name')

    oInput.value = val
}

通过主进程

// main.js

// 定义全局变量存放主窗口 Id
let mainWinId = null

const createWindow = () => {
    let mainWin = new BrowserWindow({
        // ...
    })
    
    mainWinId = mainWin.id
    // ...
}

// 接受其他进程发送的数据,完成后续的逻辑
ipcMain.on('openWin2', (_, data) => {
    // 接收到渲染进程中按钮点击信息之后完成窗口2的打开
    let subWin1 = new BrowserWindow({
        // ...
    })
    // ...
    
    // 由 index -> main* -> subWin
    // 转交给进程
    subWin1.webContents.on('did-finish-load', () => {
        subWin1.webContents.send('its', data)
    })
})

// 由 subWin -> main* -> index
ipcMain.on('stm', (_, data) => {
    // 转交给进程
    let mainWin = BrowserWindow.fromId(mainWinId)
    mainWin.webContents.send('mti', data)
})

// ...
// index.js
window.onload = () => {
    // 先获取元素
    let oBtn = document.getElementById('btn')

    // 由 index* -> main -> subWin1
    oBtn.addEventListener('click', () => {
        ipcRenderer.send('openWin2', '来自于 index 进程')
    })

    // 由 subWin1 -> main -> index*
    // 接受消息
    ipcRenderer.on('mti', (_, data) => {
        alert(data)
    })
}
// subWin1.js
window.onload = () => {
	// ...
    // 在 sub 中发送数据给 index.js
    let oBtn = document.getElementById('btn')
	// subWin1* -> main -> index
    oBtn.addEventListener('click', () => {
        ipcRenderer.send('stm', '来自于sub进程')
    })

    // index -> main -> subWin1*
    // 接收数据
    ipcRenderer.on('its', (_, data) => {
        alert(data)
    })
}

Dialog模块

Electron官网 Dialog文档

// index.js
window.onload = () => {
    let oBtn = document.getElementById('btn')
    let oBtnErr = document.getElementById('btnErr')

    oBtn.addEventListener('click', () => {
        remote.dialog.showOpenDialog({
            defaultPath: __dirname, // 默认打开目录
            buttonLabel: '请选择', // 按钮上的文字
            title: '啦啦啦啦', // 对话框标题
            properties: ['openFiles', 'multiSelections'], // 文件类型
            filters: [ // 文件类型过滤
                {'name': '代码文件', extensions: ['js', 'json', 'html']},
                {'name': '图片文件', extensions: ['ico', 'jpeg', 'png']},
                {'name': '媒体类型', extensions: ['avi', 'mp4', 'mp3']},
            ], 
        }).then(ret => {
            alert(ret)
        })
    })

    oBtnErr.addEventListener('click', () => {
        remote.dialog.showErrorBox('自定义标题', '当前错误内容')
    })
}

shell 与 iframe

shell

<!-- index.html -->
<html lang="en">
<head>
    <!-- ... -->
</head>
<body>
    <h2>shell and iframe</h2>
    <!-- 默认行为是在当前页面打开网页 -->
    <a id="openUrl" href="https://cantyonion.site">打开url</a>
    <br><br>
    <button id="openFolder">打开目录</button>
    <script src="index.js"></script>
</body>
</html>
// index.js
window.onload = () => {
    // 1 获取元素
    let oBtn1 = document.getElementById('openUrl')
    let oBtn2 = document.getElementById('openFolder')

    oBtn1.addEventListener('click', (ev) => {
        ev.preventDefault() // 阻止默认行为

        let urlPath = oBtn1.getAttribute('href')

        // 打开外部浏览器
        shell.openExternal(urlPath)
    })

    oBtn2.addEventListener('click', () => {
        // 打开当前目录
        shell.showItemInFolder(path.resolve(__filename))
    })
}

iframe

<!-- index.html -->
<html lang="en">
<head>
	<!-- ... -->
</head>
<body>
    <iframe src="https://cantyonion.site" frameborder="0" id="webview"></iframe>
    <script src="index.js"></script>
</body>
</html>
// index.js
ipcRenderer.on('open', () => {
    let iframe = document.getElementById('webview')
    iframe.src = 'https://cantyonion.site/git/'
})
// main.js
const createWindow = () => {
    let mainWin = new BrowserWindow({
        // ...
    })

    let tmp = [
        {
            label: '菜单',
            submenu: [
                {
                    label: '关于',
                    click() {
                        shell.openExternal('https://cantyonion.site')
                    }
                },
                {
                    label: '打开',
                    click() {
                        BrowserWindow.getFocusedWindow().webContents.send('open')
                    }
                },
            ]
        }
    ]

    let menu = Menu.buildFromTemplate(tmp)
    Menu.setApplicationMenu(menu)
	// ...
}
// ...

消息通知

前往MDN查看详细内容

window.onload = () => {
    let oBtn = document.getElementById('btn')

    oBtn.addEventListener('click', () => {
        let option = {
            title: '啦啦啦', // 通知标题
            body: 'ok,哈哈哈哈', // 信息体
            icon: 'c.png' // 图标
        }

        let mayNotification = new window.Notification(option.title, option)

        mayNotification.onclick = () => {
            alert("点击了消息页卡")
        }
    })
}

全局快捷键

// main.js
// ...
app.on('ready', () => {
    // 注册
    let ret = globalShortcut.register('ctrl + q', () => {
        console.log("快捷键注册成功")
    })

    if (!ret) {
        console.log('注册失败')
    }

    // 判断某个快捷键是否已经注销
    console.log(globalShortcut.isRegistered('ctrl + q'))
})

app.on('will-quit', () => {    
    globalShortcut.unregister('ctrl + q')
    
    // 自动注销使用快捷键
    globalShortcut.unregisterAll()
})

剪切板模块

<!-- index.html -->
<html lang="en">
<head>
    <!-- ... -->
</head>
<body>
    <h2>剪切板</h2>
    <input type="text" placeholder="输入需要复制的内容">
    <button>复制</button>
    <br><br>
    <input type="text" placeholder="将内容粘贴至此处">
    <button>粘贴</button>
    <br><br>
    <button id="clipImg">将图片拷贝至剪切板再粘贴至界面</button>
    <script src="index.js"></script>
</body>
</html>
// index.js
// ...

window.onload = () => {
    // 获取元素
    let aBtn = document.getElementsByTagName('button')
    let aInput = document.getElementsByTagName('input')
    let oBtn = document.getElementById('clipImg')
    let ret = null

    aBtn[0].onclick = () => {
        // 复制内容
        ret = clipboard.writeText(aInput[0].value)
    }

    aBtn[1].onclick = () => {
        // 粘贴内容
        aInput[1].value = clipboard.readText(ret)
    }

    oBtn.onclick = () => {
        // 将图片放置于剪切板的时候要求图片类型属于 nativeImage 类型
        let oImage = nativeImage.createFromPath('c.png')
        clipboard.writeImage(oImage)

        // 将剪切板中的图片作为 DOM 元素显示在界面上
        let oImg = clipboard.readImage()
        let oImgDom = new Image()
        oImgDom.src = oImg.toDataURL() // 转换为base64
        document.body.appendChild(oImgDom)
    }
}
Description
Electron 11
Readme
Languages
JavaScript 66%
HTML 34%