Introduction
electron is developed by Github , is an open source library with Html, css, JavaScript to build desktop applications , can be packaged for Mac, Windows, Linux system under the application .
electron is a runtime environment, including Node and Chromium, can be understood as the web application running in the node environment
structure
electron is mainly divided into the main process and the rendering process, the relationship is as follows
The process in which electron runs the script identified in the main
field of package.json
is called the master process.
Create web pages in the master process to display user pages, an electron has one and only one master process.
electron uses Chromium to display web pages, each running in its own
Quick Start
Next, let the code speak for itself, the thundering hello world
Create a folder and run npm init -y
to generate the package.json
file, download the electron
module and add the development dependencies
mkdir electron_hello && cd electron_hello && npm init -y && npm i electron -D
The download speed is too slow or failed, please try to use cnpm, the installation method is as follows
# cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
# electron
cnpm i electron -D
Create index.js, and write the following
const {app, BrowserWindow} = require('electron')
let win
function createWindow () { win = new BrowserWindow({ width: 800, height: 600 })
win.loadFile('./index.html')
win.webContents.openDevTools()
win.on('closed', () => {
win = null
})
}
app.on('ready', createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (win === null) {
createWindow()
}
})
Create index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1 id="h1">Hello World!</h1>
We are using node
<script>
document.write(process.versions.node)
</script>
Chrome
<script>
document.write(process.versions.chrome)
</script>,
and Electron
<script>
document.write(process.versions.electron)
</script>
</body>
</html>
Finally, modify the main field in packge.json
and add the start command.
{
...
main:'index.js',
scripts:{
"start": "electron ."
}
}
After executing npm run start
, our application will pop up.
debug
We know electron has two processes, the main process and the rendering process, how do we need to debug them during development? The old lady eats persimmons, let’s pick the soft one!
rendering process
BrowserWindow is used to create and control the browser window, and we call the API on its instance.
win = new BrowserWindow({width: 800, height: 600})
win.webContents.openDevTools()
The debugging is the same as Chrome.
main process
Debugging with VSCode
Open the project using VSCode and click the debug button
Click on the drop-down box after debugging
Select Add Configuration and choose node
This turns on the default debugging configuration, which looks like this
What? Yours isn’t, and if it isn’t, just copy and replace the following with your configuration
That’s pretty much it, so copy the second item from configurations
into your configurations
configuration, the first one is for debugging node
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "",
"program": "${workspaceFolder}/main.js"
},
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args" : ["."]
}
]
}
You can see ${workspaceFolder}
, which is a variable about VSCode, used to indicate the path of the currently opened folder
After modifying the configuration, let’s debug the panel and select what we just configured
Mark the places in the code that need to be debugged and click on the little green triangles for a happy debugging session!
process communication
In Electron, GUI-related modules (such as dialog, menu, etc.) are only available in the main process, not in the rendering process. In order to use them in the rendering process, the ipc module is necessary to send inter-process messages to the main process.
Hello, brothers
ipcMain and ipcRenderer are two good friends, through these two modules can realize the process communication.
ipcMain is used in the main process to handle synchronous and asynchronous messages sent by the rendering process (web page)
The ipcRenderer is used in the rendering process to send synchronous or asynchronous messages to the main process, and can also be used to receive reply messages from the main process.
The communication between the above two modules can be understood as ,Next, we look at their specific use of methods
main process
const {ipcMain} = require('electron')
ipcMain.on('something', (event, data) => {
event.sender.send('something1', '')
})
rendering process
const { ipcRenderer} = require('electron')
ipcRenderer.send('something', '')
ipcRenderer.on('something1', (event, data) => {
console.log(data)
})
The above code uses asynchronous transfer messages. electron also provides an API for synchronous transfer.
Sending a synchronization message will block the entire rendering process, and you should avoid using this approach – unless you know what you’re doing.
Never use ipc to pass large amounts of data, there will be major performance issues that can seriously get your whole app stuck.
With the remote module, you can call methods of the main process object without having to explicitly send interprocess messages.
const { dialog } = require('electron').remote
dialog.showMessageBox({type: 'info', message: ''})
webContents, which is responsible for rendering and controlling the web page, is a property of the BrowserWindow object, and we use the send method to send asynchronous messages to the renderer process.
main process
const {app, BrowserWindow} = require('electron')
let win
app.on('ready', () => {
win = new BrowserWindow({width: 800, height: 600})
win.loadURL('./index.html')
win.webContents.on('did-finish-load', () => {
win.webContents.send('something', 'data')
})
})
rendering process
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>
require('electron').ipcRenderer.on('something', (event, message) => {
console.log(message)
})
</script>
</body>
</html>
Rendering process data sharing
More often than not, we use HTML5 API implementations such as localStorage, sessionStorage, etc., which can also be implemented using electron’s IPC mechanism
main process
global.sharedObject = {
someProperty: 'default value'
}
rendering process
first page
require('electron').remote.getGlobal('sharedObject').someProperty = 'new value'
Second page
console.log(require('electron').remote.getGlobal('sharedObject').someProperty) // new value
Summarize
All of the above four methods can realize the communication between the main process and the rendering process, you can find that using the remote
module is the simplest, and the rendering process can use the electron
module directly in its code.
Common Modules
Shortcuts and menus
Local Shortcuts
It is only triggered when the application is focused, the main code is as follows
The official documentation is not working.
const { Menu, MenuItem } = require('electron')
const menu = new Menu()
menu.append(new MenuItem({
label: '',
submenu: [
{
label: '',
accelerator: 'CmdOrCtrl+P',
click: () => {
}
}
]
}))
Menu.setApplicationMenu(menu)
Global Shortcuts
Registers the global, which is triggered regardless of whether the app is focused or not, using globalShortcut
to do so, with the following main code, the
const {globalShortcut, dialog} = require('electron')
app.on('read', () => {
globalShortcut.register('CmdOrCtrl+1', () => {
dialog.showMessageBox({
type: 'info',
message: ''
})
})
})
The following is displayed, using the dialog
module
context menu (computing)
The context menu is the menu that appears when we right-click, and is set inside the rendering process.
The main code is as follows
const remote = require('electron').remote;
const Menu = remote.Menu;
const MenuItem = remote.MenuItem;
var menu = new Menu();
menu.append(new MenuItem({ label: 'MenuItem1', click: function() { console.log('item 1 clicked'); } }));
menu.append(new MenuItem({ type: 'separator' }));
menu.append(new MenuItem({ label: 'MenuItem2', type: 'checkbox', checked: true }));
window.addEventListener('contextmenu', (e) => {
e.preventDefault();
menu.popup(remote.getCurrentWindow());
}, false);
application menu (computing)
Above we created a local shortcut through the Menu
module, the application menu is also using the Menu
module, we see that it looks like the same, in fact, the implementation is similar, we use the creation of templates here to achieve the main code is as follows:
const {app, Menu} = require('electron')
const template = [
{
label: 'action',
submenu: [{
label: 'copy',
accelerator: 'CmdOrCtrl+C',
role: 'copy'
}, {
label: 'paset',
accelerator: 'CmdOrCtrl+V',
role: 'paste'
}, {
label: 'reload',
accelerator: 'CmdOrCtrl+R',
click: function (item, focusedWindow) {
if (focusedWindow) {
// on reload, start fresh and close any old
// open secondary windows
if (focusedWindow.id === 1) {
BrowserWindow.getAllWindows().forEach(function (win) {
if (win.id > 1) {
win.close()
}
})
}
focusedWindow.reload()
}
}
}]
},
{
label: 'reload',
submenu: [
{
label: 'youtube',
accelerator: 'CmdOrCtrl+P',
click: () => { console.log('time to print stuff') }
},
{
type: 'separator'
},
{
label: 'google',
}
]
}
]
if (process.platform === 'darwin') {
const name = electron.app.getName()
template.unshift({
label: name,
submenu: [{
label: `about ${name}`,
role: 'about'
}, {
type: 'separator'
}, {
label: 'services',
role: 'services',
submenu: []
}, {
type: 'separator'
}, {
label: ` ${name}`,
accelerator: 'Command+H',
role: 'hide'
}, {
label: 'hide other',
accelerator: 'Command+Alt+H',
role: 'hideothers'
}, {
label: 'more',
role: 'unhide'
}, {
type: 'separator'
}, {
label: 'esc',
accelerator: 'Command+Q',
click: function () {
app.quit()
}
}]
})
}
app.on('read', () => {
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
})
system tray
Primary Code
const { app, Tray } = require('electron')
let tray;
app.on('ready', () => {
tray = new Tray(__dirname + '/build/icon.png');
const contextMenu = Menu.buildFromTemplate([
{label: 'show', type: 'radio', click: () => {win.show()}},
{label: 'hidden', type: 'radio', click: () => {win.hide()}},
])
// tray.on('click', () => {
// win.isVisible() ? win.hide() : win.show()
// })
tray.setToolTip('This is my application.')
tray.setContextMenu(contextMenu)
})
Open page
shell
When the main process opens a page, it calls the default browser to open the page, using the shell
module
const { app,shell } = require('electron')
app.on('ready', () => {
shell.openExternal('github.com')
})
Once the app is launched it opens the github
page using the default browser
window.open
Used during the rendering process, the current application opens the page
window.open('github.com')
webview
Use webview
to display external web content in a separate frame and process.
Just use it directly in index.html
<webview id="foo" src="https://www.github.com/" style="display:inline-flex; width:640px; height:480px"></webview>
stencil
electron-forge
Electron-forge includes out-of-the-box templates for vue
, react
, Angular
and more.
npm i -g electron-forge
electron-forge init my-app template=react
cd my-app
npm run start
The directory structure is as follows
.compilerc
It is the configuration file of electron-compile
, similar to .babelrc
.
electron-compile is a conversion rule that supports compiling js and css in an application.
electron-react-boilerplate
If you don’t want any tools and want to simply start building from a template, react-electron template can be understood.
electron-react-boilerplate is a dual package.json
configuration project with the following directory
electron-vue
electron-vue makes full use of vue-cli as a scaffolding tool, along with webpack, electron-packager, or electron-builder with vue-loader, and some of the most commonly used plugins, such as vue-router, vuex, and so on.
The directory structure is as follows
More templates
For more templates, please visit
Pack
How to package our developed application into .app
or .exe
executable, this involves the important part of packaging, here use electron-quick-start project for packaging
Currently, there are two main packaging tools electron-packager and electron-builder.
Mac package window installer need to download the wine
brew install wine
If there are missing components, just follow the error message to download them
electron-packager
electron-packager packages your electron into a runnable file (.app, .exe, etc)
Execute npm i electron-packager -D
to install
electron-packager .
Quick Pack
Packaging Details
electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> --out=out --icon=assets/app.ico --asar --overwrite --ignore=.git
sourcedir project entry directory according to package.json- appname Package name
platform Build the platform to includedarwin, linux, mas, win32 all
- arch Build Architecture Contains
ia32,x64,armv7l,arm64
- out Packed address
- icon Packaging icon
asar Whether to generate app.asar, otherwise it’s your own source code.- overwrite overwrites the last package
- ignore Files that are not packaged
The first time the package needs to download the binary package will take a little longer, later go to the cache is much faster.
The packaged directory is
where electron-quick-start
can be opened and run directly
cache address
- Mac
~/Library/Caches/electron/
- Windows
$LOCALAPPDATA/electron/Cache
or~/AppData/Local/electron/Cache/
- Linux
$XDG_CACHE_HOME
or~/.cache/electron/
electron-builder
electron-builder is a complete solution for macos, windows, linux electron app, it can provide packaging and building related functions. It also provides out-of-the-box support for “automatic updates”.
npm i electron-builder -D
downloading
electron-builder
ask for a doggy bag (at a restaurant)
The following appears
Again, it will take a little longer to pack for the first time, so don’t fret and wait for the good news.
It will be packaged by default in the dist
directory, containing the following
mac
The file has is packaged by default using the asar method
electron-builder --dir
Only generates package files, useful for testing
pit pit pit
The first time to pack will be slower, if you and I hand and directly quit, and again when packing, congratulations, an error. The error message is as follows
• electron-builder version=20.28.2
• loaded configuration file=package.json ("build" field)
• description is missed in the package.json appPackageFile=/Users/shisan/Documents/self/you-app/package.json
• writing effective config file=dist/builder-effective-config.yaml
• no native production dependencies
• packaging platform=darwin arch=x64 electron=2.0.7 appOutDir=dist/mac
• cannot unpack electron zip file, will be re-downloaded error=zip: not a valid zip file
⨯ zip: not a valid zip file
Error: /Users/shisan/Documents/self/you-app/node_modules/app-builder-bin/mac/app-builder exited with code 1
at ChildProcess.childProcess.once.code (/Users/shisan/Documents/self/you-app/node_modules/builder-util/src/util.ts:254:14)
at Object.onceWrapper (events.js:272:13)
at ChildProcess.emit (events.js:180:13)
at maybeClose (internal/child_process.js:936:16)
at Process.ChildProcess._handle.onexit (internal/child_process.js:220:5)
From previous event:
at unpack (/Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/out/electron/ElectronFramework.js:191:18)
at Object.prepareApplicationStageDirectory (/Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/electron/ElectronFramework.ts:148:50)
at /Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/platformPackager.ts:179:21
at Generator.next (<anonymous>)
From previous event:
at MacPackager.doPack (/Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/platformPackager.ts:166:165)
at /Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/macPackager.ts:88:63
at Generator.next (<anonymous>)
From previous event:
at MacPackager.pack (/Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/macPackager.ts:80:95)
at /Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/packager.ts:376:24
at Generator.next (<anonymous>)
at xfs.stat (/Users/shisan/Documents/self/you-app/node_modules/fs-extra-p/node_modules/fs-extra/lib/mkdirs/mkdirs.js:56:16)
at /Users/shisan/Documents/self/you-app/node_modules/graceful-fs/polyfills.js:287:18
at FSReqWrap.oncomplete (fs.js:171:5)
From previous event:
at Packager.doBuild (/Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/packager.ts:344:39)
at /Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/packager.ts:314:57
at Generator.next (<anonymous>)
at /Users/shisan/Documents/self/you-app/node_modules/graceful-fs/graceful-fs.js:99:16
at /Users/shisan/Documents/self/you-app/node_modules/graceful-fs/graceful-fs.js:43:10
at FSReqWrap.oncomplete (fs.js:153:20)
From previous event:
at Packager._build (/Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/packager.ts:285:133)
at /Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/packager.ts:281:23
at Generator.next (<anonymous>)
From previous event:
at Packager.build (/Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/packager.ts:238:14)
at build (/Users/shisan/Documents/self/you-app/node_modules/app-builder-lib/src/index.ts:58:28)
at build (/Users/shisan/Documents/self/you-app/node_modules/electron-builder/src/builder.ts:227:10)
at then (/Users/shisan/Documents/self/you-app/node_modules/electron-builder/src/cli/cli.ts:42:48)
at runCallback (timers.js:763:18)
at tryOnImmediate (timers.js:734:5)
at processImmediate (timers.js:716:5)
From previous event:
at Object.args [as handler] (/Users/shisan/Documents/self/you-app/node_modules/electron-builder/src/cli/cli.ts:42:48)
at Object.runCommand (/Users/shisan/Documents/self/you-app/node_modules/yargs/lib/command.js:237:44)
at Object.parseArgs [as _parseArgs] (/Users/shisan/Documents/self/you-app/node_modules/yargs/yargs.js:1085:24)
at Object.get [as argv] (/Users/shisan/Documents/self/you-app/node_modules/yargs/yargs.js:1000:21)
at Object.<anonymous> (/Users/shisan/Documents/self/you-app/node_modules/electron-builder/src/cli/cli.ts:25:28)
at Module._compile (internal/modules/cjs/loader.js:654:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:665:10)
at Module.load (internal/modules/cjs/loader.js:566:32)
at tryModuleLoad (internal/modules/cjs/loader.js:506:12)
at Function.Module._load (internal/modules/cjs/loader.js:498:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:695:10)
at startup (internal/bootstrap/node.js:201:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:516:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] dist: `electron-builder --mac --x64`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] dist script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/shisan/.npm/_logs/2018-08-22T06_28_55_102Z-debug.log
✖ Error building interactive interface
The problem is that when downloading the .zip package, the operation is interrupted, and all future executions of the package, can not find the file (or the mutilated file) will report an error, you need to manually clear the cache Cache path in the ~/Library/Caches/electron/
Common Parameters
electron-builder
The configuration file is written in the build
field in package.json
"build": {
"appId": "com.example.app",
"productName": "测试",
"directories": {
"buildResources": "build",
"output": "dist"
},
"mac": {
"category": "public.app-category.developer-tools",
"target": ["dmg", "zip"],
"icon": "build/icon.icns"
},
"dmg": {
"background": "build/background.tiff or build/background.png",
"title": "标题",
"icon": "build/icon.icns"
},
"win": {
"target": ["nsis","zip"]
}
}
Use electron-package or electron-builder for packaging, there is no need for asar to package manually, these two tools are already integrated, the following content just to do to understand the
asar
Introduction
asar is a tar-like archive format that combines multiple files into a single file. Electron can read the contents of any file from it without having to decompress the entire file.
use
Above we see that when we use electron-package
for packaging, we can add --asar
parameter, its function is to package our source code into asar format, if not used, it is our source code.
Still using the electron-quick-start
template, packaged using electron-package
Select the application in the packaged directory and right-click
Open the paths in turn Contents -> Resources -> app
And you can see our original file.
Use electron-package . --asar
for packaging, and check again to see that the source files in the app have been packaged into a asar
file
Common commands
- Pack
asar pack|p <dir> <ouput>
as if asar ./ app.asar
- Unzip
asar extract|e <archive> <output>
as if asar e app.asar ./
In fact, even with asar packaging, our source code is still exposed, and can still be easily decrypted using asar decompression. Using webpack to compress and obfuscate the code is the way to go.
automatic update
We use electron-builder and electron-updater to do automatic updates to the application, not electron’s autoUpdater
process
Creating Certificates
Regardless of the method used for automatic updates, code signing is essential, and we tested automatic updates by creating a local certificate
Find Keychain Access in Other
Make a strong name, choose for the certificate type, and click on Create
Find it in your keychain when you’re done creating it
Right-click and select Change Trust Settings to set the trust to
code signature
No code signature
code signature
Setting environment variables
Execute sudo vi ~/.bash_profile
, add variables
export CSC_NAME="electron_update"
Execute source ~/.bash_profile
to reload the variables file
Execute echo $CSC_NAME
to see if the variable has taken effect, if not, execute the above command to reload the file.
If you still can’t get the variable after reloading several times, just exit the terminal and log in again.
Packing Code
Packaged using electron-builder
, the project uses electron-quick-start
We’ll start by adding the packaged build
configuration, and in package.json
add the
"build": {
"appId": "org.electron.com",
"publish": [
{
"provider": "generic",
"url": "http://172.28.86.36:8080/"
}
],
"productName": "231",
"directories": {
"output": "dist"
},
"mac": {
"target": [
"dmg",
"zip"
]
},
"win": {
"target": [
"nsis",
"zip"
]
},
"dmg": {
"backgroundColor": "red",
"title": "made",
"contents": [
{
"x": 400,
"y": 128,
"type": "link",
"path": "/Applications"
}
]
}
}
Earlier, we talked about the meaning of each parameter in the build
field, so we won’t go into it here. Here we have added the publish
field, which configures our auto-update information, and the url is the server address.
For servers we can use the http-server module to quickly set up a static server.
After executing the package, the output
Look mainly at the latest-mac.yml
file
You can see the version
field, which is used to indicate the current version of the application, so where does this field come from?
That’s right it’s the version
field in our package.json
file
automatic update
Write the following code in the master process
const {app, BrowserWindow, ipcMain} = require('electron')
const { autoUpdater } = require('electron-updater')
const feedURL = `http://172.28.82.40:8080/`
let mainWindow
function createWindow () {
mainWindow = new BrowserWindow({width: 800, height: 600})
mainWindow.loadFile('index.html')
mainWindow.on('closed', function () {
mainWindow = null
})
}
app.on('ready', createWindow)
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', function () {
if (mainWindow === null) {
createWindow()
}
})
ipcMain.on('update', (e, arg) => {
checkForUpdate()
})
const checkForUpdate = () => {
autoUpdater.setFeedURL(feedURL)
autoUpdater.on('error', message => {
sendUpdateMessage('err', message)
})
autoUpdater.on('checking-for-update', message => {
sendUpdateMessage('checking-for-update', message);
})
//
autoUpdater.on('download-progress', function(progressObj) {
sendUpdateMessage('downloadProgress', progressObj);
});
autoUpdater.on('update-downloaded', function(event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
ipcMain.on('updateNow', (e, arg) => {
autoUpdater.quitAndInstall();
});
sendUpdateMessage('isUpdateNow');
});
autoUpdater.checkForUpdates();
}
function sendUpdateMessage(message, data) {
mainWindow.webContents.send('message', { message, data });
}
Then change the code of our rendering process
const {ipcRenderer} = require ('electron');
const button = document.querySelector('#button')
const ul = document.querySelector('ul')
button.onclick = () => {
ipcRenderer.send('update')
}
ipcRenderer.on('message', (event, {message,data }) => {
let li = document.createElement('li')
li.innerHTML = message + " <br>data:" + JSON.stringify(data) +"<hr>";
ul.appendChild(li)
if (message === 'isUpdateNow') {
if (confirm('?')) {
ipcRenderer.send('updateNow');
}
}
})
page code
Here the button click event is used to trigger the update operation.
Just pack it up and install this 1.0.3
version.
Then change the version
field in the file at package.json
to 1.0.5
and change the index.html one to 1.0.5
to package it.
Create a folder somewhere random like test
Move the packaged files to test
and execute http-server ./
to build the service
When we click on the button, we can update the application as follows
After clicking ok and waiting for a few moments, the latest version of the app was opened after the update
The above completes the automatic update of electron.