preface

Today, we bring you a self-developed application framework called SUGAR-ELECTRON based on the Electron desktop development platform, hoping to improve the stability of Electron application and help development teams reduce development and maintenance costs.

The author has been using Electron for desktop application for 3 years, during which he has encountered many large and small pits. But in conclusion, the biggest problem is application stability and development efficiency. With this framework, we expect to be able to optimize our applications in both areas.

Project source code address: github.com/SugarTurboS…

If you have any questions, you can scan the code to join the wechat group chat

About application stability

We know that the Electron application has three basic modules.

  • The main process
  • Rendering process
  • Interprocess communication

Since we belong to the multi-window (multi-render process) application, we will write the common service module of the window to the main process module, which has hidden trouble for the stability of the whole program.

In Electron, the main process controls the entire program life cycle and also manages the renderers it creates. Once the code of the main process has a problem, the following can happen.

  • An uncaught exception crashes the main process, causing the application to exit.
  • The main process is blocked, which directly causes all renderers to block, and the UI is blocked and unresponsive.

So, in sugar-electron, we introduced the concept of a Service process, hoping to migrate the code that the business originally wrote in the main process into the Service process (essentially the renderer process) so that a crash caused by that code would not cause the entire program to crash. The process manager of the main process can restart the process and restore the state before the crash when the Service crashes, thus improving the stability and availability of the whole program.

About Development efficiency

Electron belongs to the desktop development platform to provide desktop application development capabilities framework, easy to get started. However, the framework itself lacks conventions, so using Electron for application development, the system modules will be divided into all kinds of strange divisions, and the code will be written in a variety of ways, which will significantly increase the learning cost and reduce the efficiency of developers. Sugar-electron was developed as agreed to reduce team collaboration costs and increase efficiency.

features

  • Built-in interprocess communication module, support request response, publish and subscribe
  • Built-in interprocess state sharing module, support state synchronization change, state change monitoring
  • Built-in process management module to support centralized process module management
  • Built-in configuration management module, support development, testing, production environment configuration switch
  • A built-in plug-in module supports a highly extensible plug-in mechanism
  • The framework has low invasiveness and low cost of project access and transformation
  • Incremental development

Design principles

Everything is designed around the rendering process. The main process only acts as a daemon for process management (creation, deletion, exception monitoring) and scheduling (process communication, state function bridge).

The main process does not handle business logic. The benefits of this design are:

  1. This prevents the main process from crashing without catching exceptions and causing the application to exit
  2. Avoid main process blocking, causing all render process blocking, resulting in UI blocking no response

All business modules in sugar-electron are renderers. We know that there is no direct access between processes, but to make the calls between processes as convenient as those between modules with the same thread, sugar-Electron provides the following three modules:

  1. Interprocess communication module
  2. Interprocess state sharing module
  3. Process management module

Third, in order to ensure that the framework’s core is lean, stable and efficient, the framework’s ability to expand is crucial. For this reason, SUGAR-Electron provides a custom plug-in mechanism to expand the framework’s ability, promote business logic reuse and even the formation of an ecosystem.

The framework logical view is as follows:

The SUGAR-electron is designed based on a microkernel-like architecture, as shown below:

The core of its framework has seven modules:

  • The base process class BaseWindow
  • Service process class Service
  • Processes manage windowCenter
  • Interprocess communication IPC
  • State sharing stores between processes
  • Configuration Center Config
  • Plug-ins manage plugins

The installation

npm i sugar-electron
Copy the code

The scaffold

npm i sugar-electron-cli -g

sugar-electron-cli init
Copy the code

The core function

Base process class — BaseWindow

The basic process class BaseWindow is based on the secondary encapsulation of BrowserWindow. The SUGAR-electron aggregates all the core modules of the framework using BaseWindow as the carrier.

For example

Create the renderer process using BrowserWindow

Const {BrowserWindow} = require('electron') let win = new BrowserWindow({width: 800, height: 600, show: false }); win.on('ready-to-show', () => {}) win.loadURL('https://github.com');Copy the code

Create the renderer process using BaseWindow

Const {BaseWindow} = require('sugar-electron'); // In the main process const {BaseWindow} = require('sugar-electron'); Let win = new BaseWindow('winA', {url: 'https://github.com' // leght: 400, GHT: 600, show: false }); win.on('ready-to-show', () => {}) const browserWindowInstance = winA.open();Copy the code

The Service process class — Service

In real business development, we need a process to host the functions of the common modules of the business process, and services are born for this purpose. A Service instance is actually a renderer, but the developer can create a renderer simply by passing in the startup entry JS file, which, like BaseWindow, aggregates all the core modules of the framework.

For example

/ / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- main process -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- const service = new service (' the service 'path. Join (__dirname, 'app.js'), true); Service. On ('success', function () {console.log('service started successfully '); }); Service. On ('fail', function () {console.log('service process failed to start '); }); Service. On ('crashed', function () {console.log(' crashed'); // corresponding to webcontents.on ('crashed')}); Service. On ('closed', function () {console.log('service process closed'); // corresponding to browserWindow.on('closed')});Copy the code

Process communication — IPC

As a core module of interprocess communication, IPC supports three communication modes:

  1. Request response (between renderers)
  2. Publish subscribe (between renderers)
  3. The main process communicates with the renderer

Request and response

Logical view:

For example

// service const {ipc} = require('sugar-electron'); A1 ipc. Response ('service-1', (json, cb) => {console.log(json); // {name: 'winA'} cb('service-1 response '); }); // Render process winA const {ipc, windowCenter} = require('sugar-electron'); const r1 = await windowCenter.service.request('service-1', { name: 'winA' }); console.log(r1); // const r2 = await ipc. Request ('service', 'service-1', {name: 'winA'}); console.log(r2); / / service - 1 responseCopy the code

abnormal

Status code 1 Instruction 2
1 Unable to find process
2 The process registration service could not be found
3 timeout

Release subscription

Logical view:

For example

// service const {ipc} = require('sugar-electron'); SetInterval (() => {ipc. Publisher ('service-publisher', {name: 'publisher'}); }, 1000); // winA const { ipc, windowCenter } = require('sugar-electron'); / / subscribe const unsubscribe = windowCenter. Service. The subscribe (' service - publisher, (json) = > {the console. The log (json); // {name: 'publish message'}}); // const unsubscribe = ipc. Subscribe ('service', service-publisher', (json) => {console.log(json); // Const unsubscribe = ipc. Subscribe ('service', service-publisher', (json) => {console.log(json); // {name: 'publish message'}}); // Unsubscribe (); / / equivalent windowCenter. Service. Unsubscribe (' service - publisher, cb);Copy the code

Communication between the main and renderer processes (process name “main”, reserved for the main process)

All business modules are completed by each rendering process, so basically there is no function to communicate with the main process, but it does not rule out scenes where the main process communicates with the rendering process. Therefore, the SUGAR -electron process communication module supports communication interfaces with the main process.

For example

// Const {ipc} = require('sugar-electron'); ipc.response('test', (data, cb) => { console.log(data); // I am render process cb(' I am main process ')}); // winA const { windowCenter } = require('sugar-electron'); Const res = windowCenter. Main. Request (' test ', 'I am the rendering process'); console.log(res); // const res = ipc.request('main', 'test', 'I'm rendering '); console.log(res); // I am the main processCopy the code

Process Management – Windows Center

All the business modules at SUGAR-Electron are renderers. We know that processes are not directly accessible, so there is a process management module.

All renderer processes can find the corresponding renderer process in windowCenter according to the unique key corresponding to the process name, so that calls between processes are as convenient as direct calls between thread modules.

For example

Set window B setSize(400, 400) after initialization of winB webContents

// Const {BaseWindow, Service, windowCenter} = require('sugar-electron'); / / set the Windows default Settings. For details please refer to the Electron BrowserWindow document BaseWindow. SetDefaultOption ({show: false}); // winA const winA = new BaseWindow('winA', { url: `file://${__dirname}/indexA.html` }); // winB const winB = new BaseWindow('winB', { url: `file://${__dirname}/indexB.html` }); // Create a winA window instance windowcenter.wina.open (); // equivalent to wina.open ();Copy the code
// winA const { windowCenter } = require('sugar-electron'); const winB = windowCenter.winB; // Create winB window instance await winb.open (); // Ready-to-show const unsubscribe = winb. subscribe('ready-to-show', () => {// Unsubscribe unsubscribe(); // set winB size[400, 400] const r1 = await winb. setSize(400, 400); // get winB size[400, 400] const r2 = await winb.getSize (); console.log(r1, r2); });Copy the code

== Note: Server process handles can also be obtained using Windows Center

State sharing between processes — store

The SUGAR-electron is a multi-process architecture design. In a service system, multiple service processes cannot avoid sharing state. Since the memory between processes is independent from each other, the SUGAR-electron framework integrates the process status sharing module.

The process status sharing module is divided into two parts:

  • The main process declares shared state data
  • The renderer sets up, gets shared state data, and subscribes to state changes

For example

State const {store} = require('sugar-electron'); // State const {store} = require('sugar-electron'); Store.createstore ({state: {name: 'I'm a store'}, modules: {moduleA: {state: {name:' I'm a moduleA'}}, moduleB: {state: {name: 'I'm moduleB'}, modules: {moduleC: {state: {name:' I'm moduleC'}}}}}});Copy the code
Const {store} = require('sugar-electron'); // Render process A, subscribe state change const {store} = require('sugar-electron'); console.log(store.state.name); Const unsubscribe = store.subscribe((data) => {console.log(store.state.name); // Change state unsubscribe(); // Unsubscribe}); // moduleA const moduleA = store.getModule('moduleA'); console.log(moduleA.state.name); Subscribe ((data) => {console.log(moduleA.state.name); // I am moduleA const unsubscribeA = moduleA.subscribe((data) => {console.log(moduleA.state.name); // Change moduleA unsubscribeA(); // Unsubscribe});Copy the code
// Render process B, set state const {store} = require('sugar-electron'); Await store.setState({'name': 'change state'}); // moduleA const moduleA = store.getModule('moduleA'); Await modulea.setState ({'name': 'change moduleA'});Copy the code

Configuration – config

The SUGAR-electron provides multiple environment configurations that can be switched based on environment variables, and the build environment configuration is loaded by default.

Config | - config. Base. Js based configuration | - config. / / js / / production configuration | - config. Test. The js / / test configuration - environment variable env = test | - config. Dev. Js / / Development configuration -- environment variable env=devCopy the code

Flow chart:

For example

// Const {config} = require('sugar-electron'); global.config = config.setOption({ appName: 'sugar-electron', configPath: path.join(__dirname, 'config') }); Const {config} = require('sugar-electron'); // Render process const {config} = require('sugar-electron'); console.log(config);Copy the code

== Note: ==

  • Json {“env”: “environment variable “, “config”:” configuration “}
  • By default, the sugar-electron is automatically initialized according to the root directory config

Plug-in – plugins

A good framework is inseparable from the framework’s scalability and business reuse. Developers use the Plugins module to customize and configure plug-ins.

== To use a plug-in, there are three steps: ==

  1. Custom encapsulation
  2. Config directory configuration problem plugins.js configuration plug-in installation
  3. The use of plug-in

The plug-in package

Adpter const axios = require('axios'); const apis = { FETCH_DATA_1: { url: '/XXXXXXX1', method: 'POST'}} module.exports = {/** * @ctx [object] framework context object {config, ipc, store, windowCenter, plugins} @params [object] configuration parameters */ install(CTX, BaseServer = ctx.config.baseserver; params = {}) {const baseServer = ctx.config.baseserver; return { async callAPI(action, option) { const { method, url } = apis[action]; Try {// Obtain the user ID from the process status sharing SDK const token = ctx.store.state.token; Const res = await axios({method, url: '${baseServer}${url}', data: option, timeout: params.timeout // configure timeout by plug-in}); If (action === 'LOGOUT') {// Use the interprocess communication module to tell the main process to exit ctx.ipc.sendtomain ('LOGOUT'); } return res; } catch (error) { throw error; } } } } }Copy the code

Plug-in installation

Configure the plug-in installation in the configuration center directory plugins.js

Config | - config. Base. Js based configuration | - config. / / js / / production configuration | - config. Test. The js / / test configuration - environment variable env = test | - config. Dev. Js / / Develop configuration -- environment variable env = dev | - plugins. Js / / plug-in configuration fileCopy the code
// 2, setup plugin const path = require('path'); Exports.adpter = {// If the root path plugins directory has a corresponding plug-in name, there is no need to configure path or package path: path.join(__dirname, '.. // Plugins /adpter'), // Plugins: 'adpter', // Plugins: 'adpter', // Plugins: 'adpter', // Plugins: Params: {timeout: 20000} // Pass the plugin argument};Copy the code

The plug-in USES

WinA const {plugins} = require('sugar-electron'); const res = await plugins.adpter.callAPI('FETCH_DATA_1', {});Copy the code

Automatically initialize the core module

As anyone who has used egg should know, the egg base function module is automatically initialized based on the corresponding directory. Sugar-electron also provides the ability to automatically initialize by directory. The core module is automatically initialized by passing in configuration parameters using the framework startup interface start

For example

const { start } = require('sugar-electron'); Start ({appName: 'application name ', basePath:' start directory ', configPath: 'configuration center directory ', // Optional, default basePath + './config' storePath: BasePath + './store' windowCenterPath: 'configuration center directory ', // Optional, default basePath + './windowCenter' pluginsPath: BasePath + './plugins'})Copy the code

Matters needing attention

1. Since the SUGAR-electron core module will automatically determine the main process or render process environment and automatically choose to load modules in different environments, if webpack is used, the code of both environments will be packed in, and exceptions may occur.

Therefore, if you use Webpack packaging, introduce the SUGAR-electron as follows:

// Main process const {ipc, store... } = require('sugar-electron/main') // render process const {ipc, store,... } = require('sugar-electron/render')Copy the code

API

start

Framework start interface, automatic mount config, Store, windowCenter, plugins module

Main process API

/** * Start sugar * @param {object} options Start parameters * @param {string} options.appName Application name * @param {string} options.basePath ConfigPath Configuration directory, default basePath + './config' * @param {string} options.storePath Shared directory of process status, Default basePath + './store' * @param {string} options.windowCenterPath Window center directory, Default basePath + './windowCenter' * @param {string} pluginsPath pluginsPath directory, default basePath + './plugins' */ start(opions)Copy the code

Using an example

/ / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- main process -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- start ({appName: string, application name, basePath % appData % directory: String, startup directory configPath: string, configuration directory storePath: string, process status shared directory windowCenterPath: string, window center directory pluginsPath: String plugin directory});Copy the code

BaseWindow

* @param {string} name * @param {object} option */ new BaseWindow(name, option);Copy the code

Main process API

SetDefaultOptions [class method] sets the window default configuration

/** * @param {object} option reference electron BrowserWindow */ setDefaultOptions(option)Copy the code

Open [instance method] creates an instance of BrowserWindow

/** * @param {object} option reference electron BrowserWindow * @return {BrowserWindow} */ open(option)Copy the code

GetInstance [instance method]

/**
 * @return {browserWindow}
 */
getInstance(option)
Copy the code

Publisher [instance method] publishes notifications to the current window, refer to the IPC module

/** * @param {object} param parameter * @return {browserWindow} */ Publisher (eventName, param)Copy the code

Using an example

/ / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- main process -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- const {BaseWindow} = the require (' sugar - electron); BaseWindow.setDefaultOptions({ width: 600, height: 800, show: false, ... }); const winA = new BaseWindow('winA', { url: 'https://github.com' }); const instance = winA.open({... }); // create window instance === wina.getInstance (); // trueCopy the code

Service

Main process API

@param {string} name Service process name * @param {string} path Startup path (absolute path) * @param {Boolean} devTool Specifies whether to enable the debugging tool. Default false */ new Service(name = ", path = ", openDevTool = false);Copy the code

Using an example

/ / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- main process -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- const service = new service (' the service 'path. Join (__dirname, 'app.js'), true); Service. On ('success', function () {console.log('service started successfully '); }); Service. On ('fail', function () {console.log('service process failed to start '); }); Service. On ('crashed', function () {console.log(' crashed'); // corresponding to webcontents.on ('crashed')}); Service. On ('closed', function () {console.log('service process closed'); // corresponding to browserWindow.on('closed')});Copy the code

windowCenter

Main process, renderer API

WindowCenter: object process set key= process name value= process instance, default {}Copy the code

Using an example

/ / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- main process -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- const service = new service (' the service 'path. Join (__dirname, 'app.js'), true); const winA = new BaseWindow('winA', {}); const winB = new BaseWindow('winB', {}); windowCenter['service'] === service; // true windowCenter['winA'] === winA; // true windowCenter['winB'] === winB; // true windowCenter['winA'].open(); WindowCenter ['winA'].on('ready-to-show', () => {windowCenter['winA'].setFullscreen(true); }); / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- rendering process -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / rendering process interface call is actually a process of main process via the ipc channel interface call, Async () => {await windowCenter['winA'].open(); // Create winA window instance, Asynchronous Promise calls windowCenter['winA']. Subscribe ('ready-to-show', async () => { await windowCenter['winA'].setFullscreen(true); }); }) ()Copy the code

ipc

Main process API Response Response

@param {string} eventName @param {function} callback Request ('main', eventName, param) */ Response (eventName, callback)Copy the code

Renderer API

SetDefaultRequestTimeout Sets the response timeout period

setDefaultRequestTimeout(timeout = 0);
Copy the code

Request the request

/** * @param {string} toId process ID * @param {string} eventName eventName * @param {any} data request parameter * @param {number} Request (toId, eventName, data, timeout) * * @return Return Promise object */ request(toId, eventName, data, timeout)Copy the code

The response response

@param {string} eventName * @param {function} callback callback */ response(eventName, callback)Copy the code

Unresponse Unlogs the response service

/** * @param {string} eventName * @param {function} callback */ unresponse(eventName, callback)Copy the code

Publisher publish

/** * @param {string} eventName eventName * @param {any} param parameter */ Publisher (eventName, param)Copy the code

Subscribe subscribe

/** * @param {string} toId process ID * @param {string} eventName eventName * @param {function} callback */ subscribe(toId, eventName, callback)Copy the code

Unsubscribe Unsubscribe

/** * @param {string} toId process ID * @param {string} eventName eventName * @param {function} callback */ unsubscribe(toId, eventName, callback)Copy the code

Using an example

// ---------------------winA--------------------- const { ipc } = require('sugar-electron'); A1 ipc.response('get-data', (json, cb) => {console.log(json); // {name: 'winB'} cb('winA response '); }); // ---------------------winB--------------------- const { ipc, windowCenter } = require('sugar-electron'); const btn1 = document.querySelector('#btn1'); const { winA } = windowCenter; btn1.onclick = () => { const r1 = await winA.request('get-data', { name: 'winB' }); console.log(r1); // winA response // equivalent const r2 = await ipc. Request ('get-data', 'get-data', {name: 'winB'}); console.log(r2); WinA response}Copy the code

store

Main process API

CreateStore Initializes state

/**
 * @param {object} store
 */
createStore(store)
Copy the code

Renderer API

SetState sets the state

/** * Single value set * @param {string} key * @param {any} value * @return Return Promise object */ setState(key, Value) /** * batch set * @param {object} state * @return Return Promise object */ setState(state)Copy the code

Subscribe to be notified of value changes in the current Module

/** * @param {function} cb subscribe * @return {function}Copy the code

Unsubscribe unsubscribe

/** * @param {funtion} cb */ subscribe(cb)Copy the code

GetModule acquisition module

/** * get module * @param {string} moduleName module * @return {object} module * The subscribe: subscription; Unsubscribe: unsubscribe from a subscription; GetModule: Gets submodules of the current module; getModules */ getModule(moduleName)Copy the code

GetModules gets all modules

/**
 * @return {array} [module, module, module]
 */
getModules()
Copy the code

Using an example

// Main process -- initializes state
const { store } = require('sugar-electron');
store.createStore({
    state: {
        name: 'I am a store'
    },
    modules: {
        moduleA: {
            state: {
                name: 'I am moduleA'}},moduleB: {
            state: {
                name: 'I am moduleB'
            },
            modules: {
                moduleC: {
                    state: {
                        name: 'I am moduleC'}}}}}});// Render process
const { store } = require('sugar-electron');
store.state.name; / / I am a store

// Subscribe to update messages
const unsubscribe = store.subscribe((data) = > {
    console.log('Update:', data); // Update: {name: 'I am store1'}
});
await store.setState({
    'name': 'I am store1
});
unsubscribe(); // Unsubscribe
 
// moduleA
const moduleA = store.getModule('moduleA');
moduleA.state.name; / / I'm moduleA
const unsubscribeA = moduleA.subscribe((data) = > {
    console.log('Update:', data); // Update: {name: 'I am moduleA1'}
});
await moduleA.setState({
    'name': 'I am moduleA1'
});
moduleA.unsubscribe(cb); // Unsubscribe

Copy the code