Project background

In order to provide an easy-to-use desktop streaming tool for some teachers, we plan to launch a Windows desktop application called Live Companion, and the 1.0 version of Live Companion will be delivered quickly in the Spring Festival of 2020. The main functions of livestreaming partners are as follows:

  • Compatible with login of various sites
  • Push the live streaming
  • Live even wheat
  • Host interactive chat

It has been more than one year since the launch of the project. Here, we briefly share our experience in developing Electron for more than one year, and mainly share some points needing attention during the transformation from Web development to Electron development, hoping to bring some benefits to everyone.

Technology selection

Before making the streaming tool, we investigated the internal desktop of Douyin Livestreaming – Livestreaming partner, and selected the same Mediasdk as Douyin Livestreaming in the underlying streaming SDK, and finally determined the technology selection of Bytelive livestreaming partner:

GUI framework UI framework internationalization The application package Push the current SDK Use the platform
Electron React react-intl electron-builder Mediasdk Windows

The code structure

The current directory structure of live partners is as follows:

  • app: Business code
    • assestStatic resources such as images and SVG
    • components: Business components
    • containers: A component one level above components, usually composed of multiple components
    • pages: represents the container for each BrowserWindow
    • locale: Internationalization related language configuration
    • mainThe module called in the main process
      • lib: MediasDK, RTC, etc. Modules called in the main process
      • window: BrowserWindow related creation and process communication
    • reducers: story related
    • typings: Ts type definition
    • utils: utility class
    • package.json: PKG of the app directory to install dependencies that need to be invoked in the main process
    • The HTML file: HTML loaded by BrowserWindow
    • main.development.ts: Project entry file used to start the entire ELECTRON project
  • builder-config: electron builder Related configurations, including local and online packing configurations
  • config: WebPack configuration
  • script: Script used for packaging
  • test: jest test
  • package.json: PKG of the root directory, where front-end dependencies are installed to avoid node_modules appearing in packaged files

As you can see from the code structure, writing Electron has a lot in common with Web development:

  1. You can use Webpack to package React components, package.json, etc.
  2. In most cases, the UI and logic on a page can continue to be used in Web development

To sum up, from Web development to Electron, the threshold of early start is low.

Of course, there are certainly differences between Web development and Electron development, which is the focus of this discussion.

A link to the Electron official documentation will be attached to the Electron terminology covered in the share for reference

Also Electron official glossary

Differences between Electron development and Web development

Operation mechanism

  • In Web development, we develop pages that run in a Tab in the browser:

For a one-page application (SPA), an HTML file is generally used as the entry file. After the front-end resources are packaged, the corresponding bundle.js is added to the HTML. When users access the APPLICATION through a browser, the HTML file is downloaded and the corresponding resources are loaded.

At the same time, each TAB loaded page of the browser runs in a separate process.

  • When developing Electron, our page ran in a window in a desktop application:

On the Electron Web page, you can introduce Nodejs native modules such as FS and PATH into your code to use

Here we introduce the concept of Windows (corresponding to BrowserWindow in Electron). Electron application consists of one or more Windows. In Electron, each window corresponds to an HTML file, which is similar to Web development.

But we still need a higher level entry file where we organize the logic for displaying the various Windows in the application.

In other words, for Electron, the “access through the browser” step is actually handled by the developer.

Imagine having a browser, and here are three questions to ponder:

  • Who specifies the page you want to visit
  • Who handles the operation of the browser itself, opening and closing tabs, and exiting the browser
  • Who is responsible for page Tab rendering content

In Web development, we usually only need to care about the UI and interaction part of the front page, and we don’t need to care about the operations such as window closing.

In Electron application, we need to display a page to the user when the user clicks on it. When the application contains multiple Windows, we also need to manage the explicit and implicit logic of each window. The closing, display and position of the window need to be perceived by the developer.

In Electron, each window corresponds to a rendering process, and a main process manages these rendering processes, so we generally regard the main process as such a manager.

For the above problem, let’s look at the concept of Electron again:

  • Who specifies the window you want to open –> main process
  • Who handles application logic, open close window, exit application -> main process
  • Who is responsible for rendering content in the window –> render process

Different Windows work together with the main process, which calls the shots, to form a complete Electron application:

  • For example, in the login window, after the login interface is invoked successfully, hide the login window and display the subsequent live broadcast list window
  • In the broadcast page, a Dialog needs to pop up to prompt the user

Interactions between BrowserWindows, data communication, require the corresponding renderer to send events to the main process, which then sends events to the specified renderer.

Main process and renderer process

To put it simply, the development of single-page application SPA is an HTML corresponding to an application, while the development of Electron can be multiple single-page applications. The division of labor is well handled through inter-process communication to form an overall application.

Electron life cycle

This is the entry file for the project. In this file, we handle some initialization of BrowserWindow, register some necessary event listeners on the main process, monitor data reporting, etc… Most importantly of all, we handle the Ready event in the Electron app life cycle.

Electron life cycle

  • Application startup

When Electron completes its basic initialization, it emits a ready event, at which point the developer needs to handle the custom initialization logic and present the UI to the user.

In main.development.ts, browserWindows such as login page and push stream page are created when the ready event of app is triggered. In addition, when the ready-to-show event of login window is triggered, the login window of live partner is displayed to users through show method.

  • Application to exit

The livestream partner currently has six BrowserWindows. When we want to exit the app, we will destroy all current BrowserWindow instances, which will trigger the window-all-close event and exit the app completely using the electron.app.quit method.

const { app, BrowserWindow } = require('electron') const path = require('path') function createWindow () { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js') } }) win.loadFile('index.html') } app.whenReady().then(() => { createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) }) app.on('window-all-closed', () => { if (process.platform ! == 'darwin') { app.quit() } })Copy the code

Example of the official Electron entry file

Experience optimization

BrowserWindow is pre-created

Note that each BrowerWindow takes up a certain amount of resources after it is created, so you need to minimize the number of BrowserWindows as much as possible in a single window.

BrowserWindow is like a TAB page in Chrome. In Electron, each window corresponds to an instance of BrowserWindow, which has its own rendering process.

When creating the window, we can specify the size, color, visibility and other properties of the window, refer to the BrowserWindow document. After creating the window, we load the specified HTML using the loadURL method to load our front-end page.

Ideally, the process for opening a new window is new BrowserWindow-> Load js-> perform initialization logic. Closing the window destroys the instance via close. In reality, creating a New BrowserWindow each time is expensive. Users often have to wait excruciatingly long from creation until the page is fully interactive.

In actual situations, we usually create BrowserWindow in advance, and show it directly when it needs to be used. When clicking close, we hide it in the background with hide method, so that it can pop up directly next time and reduce the user’s waiting time. Here is a simple strategy of exchanging space for time. There are also many points of discussion about the interaction optimization of Electron and how to catch up with the native interaction experience.

When we show the Electron interface using show-hide mode instead of creation-close, here are a few differences from Web page development:

  • Pre-createdBrowserWindowThe initialization logic may be executed ahead of time
  • BrowserWindowWhen hide, the React component is not destroyed

Here we construct a scene. After logging in, we enter the window of live broadcast list. After clicking on a broadcast room, the broadcast page of the broadcast room will be displayed

When we initialize the host page, we usually specify an empty array in useEffect to ensure that the whole component is initialized once when loading, such as fetching data from redux, requesting interface, and updating the corresponding view.

  useEffect(() => {
    init()
  }, [])
Copy the code

Initialization of the page in BrowerWindow

We usually create BrowserWindow ahead of time to optimize the user’s interaction experience, and after that, init logic will be triggered, which means the init logic will be executed before the click, which is not expected.

And BrowserWindow hide, if you don’t do anything special, the React component of the current page won’t be destroyed, so the init logic won’t be reexecuted the first time we click and the second time we click.

There are two ways to resolve such a case:

  • UseEffect listens for a variable, and when the value changes, init logic is performed
  • Through process communication, the main process sends initialization events to the renderer of the broadcast page

If we use the first method of listening for variables, our code can be modified to:

  useEffect(() => {
    init(activityId)
  }, [activityId])
Copy the code

This way, we update the activityId value stored in redux when we click live on the list page, and the broadcast page detects the change in activityId, ensuring that the init logic is executed correctly every time we switch the activityId.

The downside of this approach is that we use the value activityId as the trigger for the initialization logic of the page, which means that we need to know where this value might trigger the change logic to prevent the execution of the initialization logic that does not meet the expectation.

If we use the second method of initialization, which is through process communication, the code modification is a little more complicated.

First we need to add listeners to the front page, usually through the ipcrenderer. on method. IpcRenderer can receive events sent by the main process.

  1. List window after click throughipcRenderer.sendMethod sends the click event to the main process and carries it with itactivityIdparameter
  2. The main process goes through the broadcast windowBrowserWindowThe instancewebContents.sendMethods to sendSHOW_MAIN_WINDOWEvent, transparent transmissionactivityIdparameter
  3. The render process of the broadcast window passesipcRenderer.onListening to theSHOW_MAIN_WINDOWEvent that reads the activityId and executes the initialization process for the page

In this way, we can change the code on the broadcast page to:

  const handleInit = (data)=>{
      const { activityId } = data
      init(activityId)
  }  

  useEffect(() => {
    ipcRenderer.on(EVENT.SHOW_MAIN_WINDOW, handleInit)
    return ()=>{
        ipcRenderer.removeListener(EVENT.SHOW_MAIN_WINDOW, handleInit)
    }
  }, [])
Copy the code

In this way, we can ensure that the initialization of each broadcast page is sensed and controlled by the main process. In the case of complex page logic, it is recommended to use events to control the initialization, so as to avoid some redundant logic such as whether initialization is needed or not.

Of course, if we are using a process to communicate, we need to be aware that the latest values in useState and redux cannot be read in hook. If our init logic needs some additional parameters in the page, we recommend using useRef to store this variable. Read via ref.current during init.

Total knot

When we developed Electron, the UI level was basically able to reuse the Web development experience, and BrowserWindow was essentially loaded HTML.

To summarize, in Electron we need to develop:

  • The Web page
  • Management of various Windows
  • Application related: menu bar, lifecycle (opening and closing applications, etc.)