background
Follow the previous article
When developing the electron ic desktop, you will encounter various pits. Let's summarize.
- Anti virus software damage inspection
- Prevent debug debugging
- Client crash report
- Improve client startup speed
- Performance monitoring and analysis
- Delay loading module
- Uniform scroll bar style
- browserWindow error listening
- browserWindow a tab to open the default browser
- - jquery, requirejs, meteor, angularjs cannot be used in electron -.
- electron-bridge
- Proxy settings
- System version comparison (mac)
- Multi window management
- Seamless upgrade installation similar to vscode
Anti virus software damage inspection
For some internal core files, you can check whether the file exists through the whitelist mechanism. If it does not exist, report that the software is damaged and exit directly.
const getBinaryFileCheckList = ()=>{ const dir = []; // For example, the network interface package is required. const network = require.resolve("network-interface/package"), dir.push(network); return dir; } const binaryFileCheckList = getBinaryFileCheckList(); <!--inspect--> for (let e = 0; e < binaryFileCheckList.length; e++) { const n = binaryFileCheckList[e]; if (!fs.existsSync(n)) { dialog.showErrorBox("Start failed", "The application file is damaged, which may be caused by anti-virus software. Please download and install again"); // Direct exit electronApp.exit(1); break } }
Prevent debug debugging
You need to check whether there are chrome debugging keywords on argv parameters, such as inspect or debugging.
const runWithDebug = process.argv.find(e => e.includes("--inspect") || e.includes("--inspect-brk") || e.includes("--remote-debugging-port")); if(runWithDebug){ // Exit directly. electronApp.quit() }
Client crash report
Third party plug-ins can be used to assist in client crash reporting
@sentry/electron
Chinese documents
https://www.yuque.com/lizhiyao/dxydance/sentry-javascript-readme-cn
Improve client startup speed
There are several ways to improve the startup speed of the client.
Using V8 to cache data
electorn uses V8 engine to run js. When V8 runs js, it needs to parse and compile before executing the code. Among them, the parsing and compilation process consumes a lot of time, which often leads to performance bottlenecks. The V8 cache function can cache the compiled bytecode, saving the time for the next parsing and compilation.
Compile the plug-in code using V8 compile cache cache
The use of V8 compile cache is very simple. Add a line of code to the code to be cached:
require('v8-compile-cache')
V8 compile cache is cached to the temporary folder by default < OS tmpdir()>/v8-compile-cache-<V8_ Under version >, the file will be cleared after the computer is restarted.
If you want to make the cache permanent, you can use the environment variable process env. V8_ COMPILE_ CACHE_ CACHE_ Dir to specify the cache folder to avoid deletion after computer restart. In addition, if you want different caches corresponding to different versions of the project, you can add the code version number (or other unique identification) to the folder name to ensure that the cache is completely corresponding to the project version. Of course, this also means that multiple versions of the project have multiple caches. In order not to occupy too much disk space, we need to delete the cache of other versions when the program exits.
Performance monitoring and analysis
For the main process, you can use V8 inspect profiler for performance monitoring. Generated Cpupprofile file can be analyzed by Javascript Profiler on devtools. If you use fork and other methods to start the sub process, you can also use the same method to monitor. You only need to set different monitoring ports.
v8-inspect-profiler
Set the startup command, add the parameter -- inspect=${port}, and set the v8 debugging port of the main process.
// package.json { "name": "test", "version": "1.0.0", "main": "main.js", "devDependencies": { "electron": "9.2.1" }, "scripts": { "start": "electron . --inspect=5222" }, "dependencies": { "v8-inspect-profiler": "^0.0.20" } }
Delay loading module
Some dependent modules of the project need to be used only when specific functions are triggered. Therefore, it is not necessary to load immediately when the application starts. You can load it again when the method is called.
Before optimization
// Import module const xxx = require('xxx'); export function share() { ... // Method of executing dependency xxx() }
After optimization
export function share() { // Import module const xxx = require('xxx'); ... // Method of executing dependency xxx() }
Uniform scroll bar style
For window s and macOs systems, the default scroll axis width is different. First obtain the scroll axis width
function getScrollbarWidth() { const div = document.createElement('div'); div.style.visibility = 'hidden'; div.style.width = '100px'; document.body.appendChild(div); const offsetWidth = div.offsetWidth; div.style.overflow = 'scroll'; const childDiv = document.createElement('div'); childDiv.style.width = '100%'; div.appendChild(childDiv); const childOffsetWidth = childDiv.offsetWidth; div.parentNode.removeChild(div); return offsetWidth - childOffsetWidth; }
Then make different settings according to getScrollbarWidth.
For example:
<!-- Scroll slider on scroll bar --> ::-webkit-scrollbar-thumb{ background-color: rgba(180, 180, 180, 0.2); border-radius: 8px; } ::-webkit-scrollbar-thumb:hover{ background-color: rgba(180, 180, 180, 0.5); } <!-- Scroll bar track --> ::-webkit-scrollbar-track { border-radius: 8px; } <!-- Entire scroll bar --> ::-webkit-scrollbar{ width: 8px; height: 8px; }
document.onreadystatechange=(()=>{ if("interactive" === document.readyState){ // processing logic } })
browserWindow error listening
It mainly listens to unhandledrejection and error events
error
window.addEventListener('error',(error)=>{ const message = { message: error.message, source: error.source, lineno: error.lineno, colno: error.colno, stack: error.error && error.error.stack, href: window.location.href }; // Send to the main process through ipcRender for logging. ipcRenderer.send("weblog", n) },false)
unhandledrejection
window.addEventListener('unhandledrejection',(error)=>{ if(!error.reason){ return; } const message = { message: error.reason.message, stack: error.reason.stack, href: window.location.href } <!-- adopt ipcRender Send logs to the master process.--> ipcRenderer.send("weblog", n) },false)
browserWindow a tab to open the default browser
For business, the a tag usually exists on the browserWindow page. At this time, if it is in the electron ic container, it needs to be intercepted and opened through the default browser.
document.addEventListener('click',(event)=>{ const target = event.target; if(target.nodeName === 'A'){ if(event.defaultPrevented){ return; } if(location.hostname){ event.preventDefault(); } if(target.href){ shell.openExternal(target.href); } } },false);
Expose a global method of opening the browser
window.openExternalLink = ((r)=>{ shell.openExternal(r) });
- jquery, requirejs, meteor, angularjs cannot be used in electron -.
Because Electron introduces node in the running environment JS, so there are some additional variables in DOM, such as module, exports and require. This leads to many libraries not working properly, because they also need to add variables with the same name to the running environment.
In two ways,
- One is to configure webpreferences Nodeintegration is false by disabling node js
- Through the electronic bridge JS inside the top delete window require;, delete window. exports;, delete window. module; mode
// In the main process const { BrowserWindow } = require('electron') const win = new BrowserWindow(format@@ webPreferences: { nodeIntegration: false } }) win.show()
<head> <script> window.nodeRequire = require; delete window.require; delete window.exports; delete window.module; </script> <script type="text/javascript" src="jquery.js"></script> </head>
electron-bridge
Inject additional api of electron into browserWindow through bridge
const {ipcRenderer: ipcRenderer, shell: shell, remote: remote, clipboard: clipboard} = require("electron"), <!-- process Inner parameters--> const processStaticValues = _.pick(process, ["arch", "argv", "argv0", "execArgv", "execPath", "helperExecPath", "platform", "type", "version", "versions"]); module.exports = (() => ({ ipcRenderer: ipcRenderer, // ipc renderer shell: shell, // shell remote: remote, // clipboard: clipboard, process: { ...processStaticValues, hang: () => { process.hang() }, crash: () => { process.crash() }, cwd: () => { process.cwd() } } }));
Proxy settings
For proxy settings, there are generally two modes:
- PAC
- HTTP
PAC
Direct input address p rotocol://IP:Port
HTTP
For HTTP mode, there are
- HTTP
- SOCKS4
- SOCKS5
Enter p rotocol://IP:Port
System version comparison (mac)
It is recommended to use semver to.
Multi window management
Electronic windows is recommended to support dynamic creation of windows.
Seamless upgrade installation similar to vscode
General idea: Mount dmg first and find the mount directory. Under mac, it is under / Volumes directory; Delete the app under / Applications and copy the app under / Volumes to the directory of / Applications; Then unload dmg; Just restart the application. This method can achieve the effect similar to seamless update.
With the help of hdiutil
It is mainly divided into six steps:
- which hdiutil
- hdiutil eject [/Volumes/appDisplayName latestVersion]
- hdiutil attach [latest Dmg Path]
- mv [local App Path] [temp dir]
- cp -R [latest app path] [local app path]
- hdiutil eject [/Volumes/appDisplayName latestVersion]
which hdiutil
Check whether the hdiutil executable exists.
hdiutil eject [/Volumes/appDisplayName latestVersion]
Uninstall the files under [/ Volumes/appDisplayName latestVersion].
hdiutil attach [latest Dmg Path]
Install dmg files
mv [local App Path] [temp dir]
Move the old local app directory to tempDir directory.
cp -R [latest app path] [local app path]
Copy all files under the latest app path file to the original app directory.
hdiutil eject [/Volumes/appDisplayName latestVersion]
Uninstall the files under [/ Volumes/appDisplayName latestVersion] again
At every step, if you succeed, you succeed.
Example code.
const path = require("path"); const os = require('os'); const {waitUntil, spawnAsync} = require('../../utils'); const {existsSync} = require('original-fs'); const getMacOSAppPath = () => { const sep = path.sep; const execPathList = process.execPath.split(sep); const index = execPathList.findIndex(t => 'Applications' === t); return execPathList.slice(0, index + 2).join(sep); }; module.exports = (async (app) => { const {appDisplayName} = app.config; const {latestVersion, latestDmgPath} = app.updateInfo; // const macOsAppPath = getMacOSAppPath(); // temp dir const tempDir = path.join(os.tmpdir(), String((new Date).getTime())); const appDisplayNameVolumesDir = path.join('/Volumes', `${appDisplayName} ${latestVersion}`); // const latestAppPath = path.join(appDisplayNameVolumesDir, `${appDisplayName}.app`); // step 1 which hdiutil // /usr/bin/hdiutil try { const hdiutilResult = await spawnAsync('which', ['hdiutil']); if (!hdiutilResult.includes('/bin/hdiutil')) { throw new Error('hdiutil not found'); } } catch (e) { app.logger.warn(e); return { success: false, type: 'dmg-install-failed' } } // step 2 hdiutil eject appDisplayNameVolumesDir try { await spawnAsync("hdiutil", ["eject", appDisplayNameVolumesDir]) } catch (e) { e.customMessage = '[InstallMacOSDmgError] step2 volume exists'; app.logger.warn(e); } finally { const result = await waitUntil(() => !existsSync(latestAppPath), { ms: 300, retryTime: 5 }); if (!result) { app.logger.warn('[InstallMacOSDmgError] step2 volume exists'); return { success: false } } } //step 3 hdiutil attach latestDmgPath try { await spawnAsync('hdiutil', ['attach', latestDmgPath]) } catch (e) { e.customMessage = '[InstallMacOSDmgError] step3 hdiutil attach error'; app.logger.warn(e); } finally { const result = await waitUntil(() => !existsSync(latestAppPath), { ms: 300, retryTime: 5 }); if (!result) { app.logger.warn('[InstallMacOSDmgError] step3 hdiutil attach fail'); return { success: false } } } // step 4 mv try { await spawnAsync('mv', [macOsAppPath, tempDir]); } catch (e) { e.customMessage = '[InstallMacOSDmgError] step4 mv to tmp path error'; app.logger.warn(e); } finally { const result = await waitUntil(() => !existsSync(tempDir), { ms: 300, retryTime: 5 }); if (!result) { app.logger.warn('[InstallMacOSDmgError] step4 cp to tmp path fail'); return { success: false, type: "dmg-install-failed" } } } // step 5 try { await spawnAsync('cp', ['-R', latestAppPath, macOsAppPath]) } catch (e) { e.customMessage = '[InstallMacOSDmgError] step5 cp to app error'; app.logger.warn(e); } finally { const result = await waitUntil(() => !existsSync(macOsAppPath), { ms: 300, retryTime: 5 }); if (!result) { app.logger.warn('[InstallMacOSDmgError] step5 cp to app fail'); await spawnAsync('mv', [tempDir, macOsAppPath]); return { success: false, type: "dmg-install-failed" } } } // step 6 try { await spawnAsync('hdiutil', ['eject', appDisplayNameVolumesDir]) } catch (e) { e.customMessage = '[InstallMacOSDmgError] step6 hdiutil eject fail'; app.logger.warn(e); } return { success: true } });
Project address
To this end, I stripped out the functions required by each client used in the business system and created a new template to facilitate the development of the new business system.
Coding is not easy, welcome star