Summary of electron ic desktop service (PIT)

Posted by Jiraiya on Tue, 08 Mar 2022 02:03:53 +0100

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,

  1. One is to configure webpreferences Nodeintegration is false by disabling node js
  2. 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.

address

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

https://github.com/bosscheng/electron-app-template

github

Topics: Javascript webkit v8 bridge