preface

Electron is a framework for cross-platform desktop application creation that allows us to create cross-platform desktop applications using HTML/CSS/JS. With the development of big front-end, when we develop Web UI, we are accustomed to use build tools such as Webpack and MVVM frameworks such as React to assist development. The same is true when developing Electron, so this article will introduce how to use Webpack/React to package and build the entire Electron application, and use the Electron Builder to build the App. In fact, the community provides many scaffolding and templates for Electron Webpack, such as Electron – Forge and Electron – React-Boilerplate, etc., but by exploring and building (repeating the wheel) by ourselves, we can gain a deeper understanding of the front-end packaging construction system.

directory

  1. Introduction of Electron
  2. Electron installation
  3. The structure design
  4. Use Webpack to package the main and renderer processes
  5. Build the application using electron builder
  6. C++ module support
  7. Redux + React-Router integration
  8. Devtron AIDS in development tool integration
  9. conclusion
  10. reference

Introduction of Electron

Electron is to use Web front-end technology (HTML/CSS/JavaScript/React, etc.) to create a Native cross-platform desktop application framework, it can be thought of as Chromium, Node. Js, the combination of Native APIs.

Node.js is a JavaScript runtime that is lightweight and efficient based on an event-driven, non-blocking I/O model. In Electron, I am responsible for calling the system’s underlying API to operate the native GUI and execute the main thread JavaScript code, and utils, FS and other modules commonly used in Node.js can also be directly used in Electron.

Native APIs are GUI functions provided by the system, such as system notifications, system menus, opening system folder dialogs, etc. Electron integrates Native APIs to support operating system functions for applications.

Unlike traditional Web sites, Electron is based on a master-slave Process model, where each Electron application has only one Main Process and one or more Renderer processes for multiple Web pages. In addition, there are GUP processes, extension processes, and other processes.

Electron installation

The biggest problem encountered during the installation of Electron may be the network timeout (evil wall) during the downloading of the Electron packet, resulting in the unsuccessful installation.

node_modules/@electron/get/dist/cjs/artifact-utils.js
mirrorVar

function mirrorVar(name, options, defaultValue) {
    // Convert camelCase to camel_case for env var reading
    const lowerName = name.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}_${b}`).toLowerCase();
    return (process.env[`NPM_CONFIG_ELECTRON_${lowerName.toUpperCase()}`] ||
        process.env[`npm_config_electron_${lowerName}`] ||
        process.env[`npm_package_config_electron_${lowerName}`] ||
        process.env[`ELECTRON_${lowerName.toUpperCase()}`] ||
        options[name] ||
        defaultValue);
}
Copy the code

And get the download path getArtifactRemoteURL method

async function getArtifactRemoteURL(details) {
    const opts = details.mirrorOptions || {};
    let base = mirrorVar('mirror', opts, BASE_URL); // ELECTRON_MIRROR environment variable
    if (details.version.includes('nightly')) {
        const nightlyDeprecated = mirrorVar('nightly_mirror', opts, ' ');
        if (nightlyDeprecated) {
            base = nightlyDeprecated;
            console.warn(`nightly_mirror is deprecated, please use nightlyMirror`);
        }
        else {
            base = mirrorVar('nightlyMirror', opts, NIGHTLY_BASE_URL); }}const path = mirrorVar('customDir', opts, details.version).replace('{{ version }}', details.version.replace(/^v/.' ')); // ELECTRON_CUSTOM_DIR environment variable and replaces {{version}} with the current version
    const file = mirrorVar('customFilename', opts, getArtifactFileName(details));
    // Allow customized download URL resolution.
    if (opts.resolveAssetURL) {
        const url = await opts.resolveAssetURL(details);
        return url;
    }
    return `${base}${path}/${file}`;
}
Copy the code

As you can see, there are many environment variables that can be defined to specify mirrors, such as ELECTRON_MIRROR, ELECTRON_CUSTOM_DIR, and so on, which are also specified in the official documentation

Mirror

You can use environment variables to override the base URL, the path at which to look for Electron binaries, and the binary filename. The URL used by @electron/get is composed as follows:

url = ELECTRON_MIRROR + ELECTRON_CUSTOM_DIR + '/' + ELECTRON_CUSTOM_FILENAME
Copy the code

For instance, to usethe China CDN mirror:

ELECTRON_MIRROR="https://cdn.npm.taobao.org/dist/electron/"
ELECTRON_CUSTOM_DIR="{{ version }}"
Copy the code

Therefore, only two environment variables need to be added to solve the network timeout (wall) problem when downloading Electron

ELECTRON_MIRROR="https://cdn.npm.taobao.org/dist/electron/"  ELECTRON_CUSTOM_DIR="{{ version }}" npm install --save-dev electron
Copy the code

After installing electron, you can try to write the simplest electron application. The project structure is as follows

project
  |__index.js     # main process
  |__index.html   # render process
  |__package.json # 
Copy the code

The corresponding index.js section of the main process

const electron = require('electron');
const { app } = electron;

let window = null;

function createWindow() {
  if (window) return;
  window = new electron.BrowserWindow({
    webPreferences: {
      nodeIntegration: true // Allow node modules to be used in renderers
    },
    backgroundColor: '# 333544'.minWidth: 450.minHeight: 350.height: 350.width: 450
  });
  window.loadFile('./index.html').catch(console.error);
  window.on('close', () = >window = null);
  window.webContents.on('crashed', () = >console.error('crash'));
}
app.on('ready', () => createWindow());
app.on('window-all-closed', () = > {if(process.platform ! = ='darwin') { app.quit(); }}); app.on('activate', createWindow)

Copy the code

The corresponding renderer process index.html section


      
<html lang="zh">
<head><title></title></head>
<style>
    .box {color: white;font-size: 20px;text-align: center; }</style>
<body>
<div class="box">Hello world</div>
</body>
</html>
Copy the code

Add a run command to package.json

{
  ...,
  "main": "index.js",
  "script": {
     "start": "electron ."
  },
  ...
}
Copy the code

npm run startRun, a simplest electron application is developed.

The project structure

The Electron project usually consists of the main process and the renderer process. The main process is used to implement the application back end. The main process usually uses C++ or rust to implement core functions and is loaded into the main process in the form of Node plug-in (such as bytedance’s flybook and flychat’s main process uses rust). The JavaScript part is like a layer of glue that connects Electron to third-party plug-ins, and the render process implements the drawing of the Web UI and some UI interaction logic. The main and renderer processes are developed independently and communicate with each other using IPC, so the main and renderer processes are packaged separately, namely two sets of WebPack configurations, as well as two sets of Webpack configurations to distinguish the development and production environments. In addition, there will be a requirement of multi-windows in developing electron application, so the rendering process is packaged with multi-pages. The overall structure is as follows.

project
  |__src
     |__main                                          # Main process code
        |__index.ts
        |__other
     |__renderer                                      # render process code
        |__index                                      # a window/page
           |__index.tsx
           |__index.scss
        |__other   
  |__dist                                             # webPack after packaging
  |__native                                           # c + + code
  |__release                                          # electron- Builder after packaging
  |__resources                                        # resource file
  |__babel.config.js                                  # Babel configuration
  |__tsconfig.json                                    # typescript configuration
  |__webpack.base.config.js                           Basic WebPack configuration
  |__webpack.main.dev.js                              Main process development mode Webpack configuration
  |__webpack.main.prod.js                             Main process production mode Webpack configuration
  |__webpack.renderer.dev.js                          Render process development mode WebPack configuration
  |__webpack.renderer.prod.js                         Render process production mode WebPack configuration
Copy the code

The packaging and construction process is actually quite simple. Webpack the main process and render process respectively, and finally use electron Builder to package and build the packaged code, and finally build the APP.Multi-window processing, where each directory under the render process represents a window (page) and is indicated in the Webpack Entry entry, packaged separately to when packageddist/${name}In the directory, the main process is loaded according to the name identified by WebPack Entry.

Use Webpack to package the main and renderer processes

First, install webPack

npm install --save-dev webpack webpack-cli webpack-merge
Copy the code

Install the react

npm install --save react react-dom
Copy the code

Install the typescript

npm install --save-dev typescript
Copy the code

And install the corresponding types package

npm install --save-dev @types/node @types/react @types/react-dom @types/electron @types/webpack
Copy the code

Write the corresponding tsconfig.json

{
  "compilerOptions": {
    "experimentalDecorators": true."target": "ES2018"."module": "CommonJS"."lib": [
      "dom"."esnext"]."declaration": true."declarationMap": true."jsx": "react"."strict": true."pretty": true."sourceMap": true."noUnusedLocals": false."noUnusedParameters": false."noImplicitReturns": true."moduleResolution": "Node"."esModuleInterop": true."allowSyntheticDefaultImports": true."allowJs": true."resolveJsonModule": true
  },
  "exclude": [
    "node_modules"."native"."resources"]."include": [
    "src/main"."src/renderer"]}Copy the code

Write the basic WebPack configuration, webpack.base.config.js, which is required by both the main and renderer processes

const path = require('path'); Module. exports = {module: {rules: [// ts, TSX, js, JSX] {test: /\.[tj]sx? $/, exclude: /node_modules/, use: { loader:'babel-loader', // babel-loader handles JSX or TSX files options: {cacheDirectory:true}}}, // C++ module.node file processing {test: /\.node$/,
        exclude: /node_modules/,
        use: 'node-loader'// node-loader handles. Node files for C++ modules}]}, resolve: {extensions: ['.js'.'.jsx'.'.json'.'.ts'.'.tsx'.'.node'].alias: {
      '~native': path.resolve(__dirname, 'native'), // alias to facilitate import'~resources': path.resolve(__dirname, 'resources'// alias to facilitate import}}, devtool:'source-map',
  plugins: []
};
Copy the code

Install babel-loader to process JSX or TSX files, and node-loader to process. Node files

npm install --save-dev babel-loader node-loader
Copy the code

Install the appropriate Babel plug-in

npm install --save-dev @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-optional-chaining @babel/plugin-syntax-dynamic-import @babel/plugin-transform-react-constant-elements @babel/plugin-transform-react-inline-elements
Copy the code

And install Babel presets

npm install --save-dev @babel/preset-env @babel/preset-react @babel/preset-typescript
Copy the code

Write the appropriate babel.config.js configuration, in which the code in development mode and production mode is handled separately, that is, using different plug-ins

const devEnvs = ['development'.'production'];
const devPlugins = []; // TODO development mode

const prodPlugins = [ // Production mode
  require('@babel/plugin-transform-react-constant-elements'),
  require('@babel/plugin-transform-react-inline-elements'),
  require('babel-plugin-transform-react-remove-prop-types')];module.exports = api= > {
  const development = api.env(devEnvs);

  return {
    presets: [[require('@babel/preset-env'), {
        targets: {
          electron: 'v9.0.5' // Babel compile target, electron version}}].require('@babel/preset-typescript'), / / typescript support
      [require('@babel/preset-react'), {development, throwIfNamespace: false}] / / the react support].plugins: [[require('@babel/plugin-proposal-optional-chaining'), {loose: false}].// Optional chain plugins
      [require('@babel/plugin-proposal-decorators'), {legacy: true}].// Decorator plugin
      require('@babel/plugin-syntax-dynamic-import'), // Dynamically import plug-ins
      require('@babel/plugin-proposal-class-properties'), // Class attribute plug-in. (development ? devPlugins : prodPlugins)// Differentiate the development environment]}; };Copy the code

The main process, Webpack, packages the configuration

To package the main process, you only need to package all the TS files in SRC /main into dist/main. It is worth noting that the main process corresponds to the Node project. If you use webpack to package the main process, the modules in node_modules will also be packaged. So use the webpack-node-externals plugin to exclude node_modules

npm install --save-dev webpack-node-externals
Copy the code

Development mode corresponding webpack configuration webpack. Main. Dev. Config. Js as follows

const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const webpackBaseConfig = require('./webpack.base.config');

module.exports = merge.smart(webpackBaseConfig, {
  devtool: 'none'.mode: 'development'.// Development mode
  target: 'node'.entry: path.join(__dirname, 'src/main/index.ts'),
  output: {
    path: path.join(__dirname, 'dist/main'),
    filename: 'main.dev.js' // The development mode file is named main.dev.js
  },
  externals: [nodeExternals()], // Remove the Node module
  plugins: [
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'development'})].node: {
    __dirname: false.__filename: false}});Copy the code

Production mode and development mode, so the corresponding webpack configuration webpack. Main. Prod. Config. Js as follows

const path = require('path');
const merge = require('webpack-merge');
const webpack = require('webpack');
const webpackDevConfig = require('./webpack.main.dev.config');

module.exports = merge.smart(webpackDevConfig, {
  devtool: 'none'.mode: 'production'.// Production mode
  output: {
    path: path.join(__dirname, 'dist/main'),
    filename: 'main.prod.js' // The production mode file name is main.prod.js
  },
  plugins: [
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'production']}}));Copy the code

The renderer packages the configuration

The packaging of the rendering process is the normal packaging process of the front-end project. Considering the requirement of multi-window in the Electron project, the multi-page packaging of the rendering process is carried out. The structure of the packed rendering process is as follows

dist
  |__renderer # render process
     |__page1 Page # 1
        |__index.html
        |__index.prod.js
        |__index.style.css
     |__page2 Page # 2
        |__index.html
        |__index.prod.js
        |__index.style.css
Copy the code
Production mode

In this case, the htML-webpack-plugin plugin is used to generate the HTML template, and an.html file needs to be generated for each page

npm install --save-dev mini-css-extract-plugin html-webpack-plugin
Copy the code

Css-loader, Sas-loader, and style-loader deal with styles, url-loader and file-loader deal with images and fonts, and resolve-url-loader deals with relative path problems in SCSS file URL ()

npm install --save-dev css-loader file-loader sass-loader style-loader url-loader resolve-url-loader
Copy the code

Because SCSS is used for styling, the Node-sass package needs to be installed

npm install --save-dev node-sass
Copy the code

Node-sass installation is a bit of a problem, and you will often encounter network timeout problems (again caused by the wall), usually solved by mirroring.

--sass-binary-site

npm install --save-dev node-sass --sass-binary-site=http://npm.taobao.org/mirrors/node-sass
Copy the code

The corresponding production mode webpack configuration webpack. The renderer. Prod. Config. Js as follows

// Render process prod environment WebPack configuration
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.config');

const entry = {
  index: path.join(__dirname, 'src/renderer/index/index.tsx'), // Page entry
};
// Generate an.html file for each entry
const htmlWebpackPlugin = Object.keys(entry).map(name= > new HtmlWebpackPlugin({
  inject: 'body'.scriptLoading: 'defer'.template: path.join(__dirname, 'resources/template/template.html'), // Template.html is a very simple HTML template
  minify: false.filename: `${name}/index.html`.chunks: [name]
}));

module.exports = merge.smart(webpackBaseConfig, {
  devtool: 'none'.mode: 'production'.target: 'electron-preload',
  entry
  output: {
    path: path.join(__dirname, 'dist/renderer/'),
    publicPath: '.. / '.filename: '[name]/index.prod.js' // Output is a folder for each entry
  },
  module: { 
    rules: [ // File processing rules
      // Handle global.css files
      {
        test: /\.global\.css$/.use: [{loader: MiniCssExtractPlugin.loader,
            options: { publicPath: '/'}}, {loader: 'css-loader'.options: { sourceMap: true}}, {loader: 'resolve-url-loader'}, // Resolve relative path issues in style files]},// General style files, using CSS modules
      {
        test: / ^ ((? ! \.global).) *\.css$/.use: [{loader: MiniCssExtractPlugin.loader },
          {
            loader: 'css-loader'.options: {
              modules: { localIdentName: '[name]__[local]__[hash:base64:5]' },
              sourceMap: true}}, {loader: 'resolve-url-loader'}},],// Handle SCSS global styles
      {
        test: /\.global\.(scss|sass)$/.use: [{loader: MiniCssExtractPlugin.loader },
          {
            loader: 'css-loader'.options: { sourceMap: true.importLoaders: 1}}, {loader: 'resolve-url-loader'},
          {
            loader: 'sass-loader'.options: { sourceMap: true}}},// Handle general SASS style, still use CSS module
      {
        test: / ^ ((? ! \.global).) *\.(scss|sass)$/.use: [{loader: MiniCssExtractPlugin.loader },
          {
            loader: 'css-loader'.options: {
              modules: { localIdentName: '[name]__[local]__[hash:base64:5]' },
              importLoaders: 1.sourceMap: true}}, {loader: 'resolve-url-loader'},
          {
            loader: 'sass-loader'.options: { sourceMap: true}}},// Process the font file WOFF
      {
        test: /\.woff(\? v=\d+\.\d+\.\d+)? $/.use: {
          loader: 'url-loader'.options: { limit: 10000.mimetype: 'application/font-woff'}}},// Process the font file WOFF2
      {
        test: /\.woff2(\? v=\d+\.\d+\.\d+)? $/.use: {
          loader: 'url-loader'.options: { limit: 10000.mimetype: 'application/font-woff'}}},// Process the font file TTF
      {
        test: /\.ttf(\? v=\d+\.\d+\.\d+)? $/.use: {
          loader: 'url-loader'.options: {  limit: 10000.mimetype: 'application/octet-stream'}}},// Process the font file EOT
      {
        test: /\.eot(\? v=\d+\.\d+\.\d+)? $/.use: 'file-loader'
      },
      // Process the SVG file SVG
      {
        test: /\.svg(\? v=\d+\.\d+\.\d+)? $/.use: {
          loader: 'url-loader'.options: { limit: 10000.mimetype: 'image/svg+xml'}}},// Process the image
      {
        test: / \. (? :ico|gif|png|jpg|jpeg|webp)$/.use: {
          loader: 'url-loader'.options: { limit: 5000}}}]},plugins: [
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'production'
    }),
    new MiniCssExtractPlugin({
      filename: '[name]/index.style.css'.publicPath: '.. / '
    }),
    ...htmlWebpackPlugin
  ]
});
Copy the code

At this point, you have completed the packaging configuration for the main process and the packaging configuration for the production mode of the renderer. Here you can directly test the packaging results of the project production environment.

Json, build-main to pack the main process, build-renderer to pack the render process, build main and render process to pack in parallel, and start-main to run the Electron project

{... "main": "dist/main/main.prod.js", "scripts": { "build-main": "cross-env NODE_ENV=production webpack --config webpack.main.prod.config.js", "build-renderer": "cross-env NODE_ENV=production webpack --config webpack.renderer.prod.config.js", "build": "concurrently \"npm run build-main\" \"npm run build-renderer\"", "start-main": "electron ./dist/main/main.prod.js" }, ... }Copy the code

Cross-env is used in scripting, which, as its name implies, provides cross-platform environment variable support, while Concurrently running commands is used, as shown below

npm install --save-dev cross-env concurrently
Copy the code

Try writing a small example to test the packing result, the main process SRC /main/index.ts

import { BrowserWindow, app } from 'electron';
import path from "path";
// Load HTML, currently only for production mode
function loadHtml(window: BrowserWindow, name: string) {
  if (process.env.NODE_ENV === 'production') {
    window.loadFile(path.resolve(__dirname, `.. /renderer/${name}/index.html`)).catch(console.error);
    return;
  }
  // TODO development
}

let mainWindow: BrowserWindow | null = null;
// Create window
function createMainWindow() {
  if (mainWindow) return;
  mainWindow = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true
    },
    backgroundColor: '# 333544',
    minWidth: 450,
    minHeight: 350,
    width: 450,
    height: 350
  });
  loadHtml(mainWindow, 'index');
  mainWindow.on('close'.(a)= > mainWindow = null);
  mainWindow.webContents.on('crashed'.(a)= > console.error('crash'));
}
app.on('ready'.(a)= > { createMainWindow() });
app.on('window-all-closed'.(a)= > {
  if(process.platform ! = ='darwin') { app.quit(); }}); app.on('activate'.(a)= > { createMainWindow() })
Copy the code

Main page rendering process SRC/renderer/index/index. The TSX

import React from 'react'; import ReactDOM from 'react-dom'; // @ts-ignore import style from './index.scss'; // typescript does not support CSS modules, so the compiler will not recognize them. @ts-ignore function App() {return (<div className={style. App}> <h3>Hello world</h3> <button>+ Import</button> </div> ) } ReactDOM.render(<App/>, document.getElementById('app'));Copy the code

Package the main and renderer code in parallel using the build command

npm run build
Copy the code

The packaged result is shown below, so the path of the main process when loading the HTML file is../renderer/${name}/index.html usenpm run start-mainCommand to run the project.

Development mode

The react-hot-loader package is used in renderer development mode, and the Webpack-dev-server package is also installed if the webpack service is required.

npm install --save-dev webpack-dev-server
npm install --save react-hot-loader @hot-loader/react-dom
Copy the code

Modify the Babel configuration and add the following plug-ins in the development environment

const devPlugins = [require('react-hot-loader/babel')];
Copy the code

Modify the render process entry file to judge the current environment and wrap the ReactHotContainer during render

import { AppContainer as ReactHotContainer } from 'react-hot-loader';

const AppContainer = process.env.NODE_ENV === 'development' ? ReactHotContainer : Fragment;

ReactDOM.render(
  <AppContainer>
      <App/>
  </AppContainer>,
  document.getElementById('app')
);
Copy the code

The corresponding development mode webpack configuration webpack. The renderer. Prod. Config. Js

// WebPack configuration in render dev environment
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {spawn} = require('child_process');
const webpackBaseConfig = require('./webpack.base.config');

const port = process.env.PORT || 8080;
const publicPath = `http://localhost:${port}/dist`;

const hot = [
  'react-hot-loader/patch'.`webpack-dev-server/client? http://localhost:${port}/ `.'webpack/hot/only-dev-server',];const entry = {
  index: hot.concat(require.resolve('./src/renderer/index/index.tsx')),};// Generate HTML templates
const htmlWebpackPlugin = Object.keys(entry).map(name= > new HtmlWebpackPlugin({
  inject: 'body'.scriptLoading: 'defer'.template: path.join(__dirname, 'resources/template/template.html'),
  minify: false.filename: `${name}.html`.chunks: [name]
}));

module.exports = merge.smart(webpackBaseConfig, {
  devtool: 'inline-source-map'.mode: 'development'.target: 'electron-renderer',
  entry,
  resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom' // In development mode}},output: { publicPath, filename: '[name].dev.js' },

  module: {
    rules: [
      // Handle global CSS styles
      { 
        test: /\.global\.css$/.use: [{loader: 'style-loader'},
          {
            loader: 'css-loader'.options: {sourceMap: true}}, {loader: 'resolve-url-loader'}},],// Handle CSS styles, using CSS modules
      { 
        test: / ^ ((? ! \.global).) *\.css$/.use: [{loader: 'style-loader'},
          {
            loader: 'css-loader'.options: {
              modules: { localIdentName: '[name]__[local]__[hash:base64:5]' },
              sourceMap: true.importLoaders: 1}}, {loader: 'resolve-url-loader'}},// Handle global SCSS styles
      {
        test: /\.global\.(scss|sass)$/.use: [{loader: 'style-loader'},
          {
            loader: 'css-loader'.options: {sourceMap: true}}, {loader: 'resolve-url-loader'},
          {loader: 'sass-loader'}},// Handle SCSS styles, using CSS modules
      {
        test: / ^ ((? ! \.global).) *\.(scss|sass)$/.use: [{loader: 'style-loader'},
          {
            loader: 'css-loader'.options: {
              modules: { localIdentName: '[name]__[local]__[hash:base64:5]' },
              sourceMap: true.importLoaders: 1}}, {loader: 'resolve-url-loader'},
          {loader: 'sass-loader'}},// Process the image
      {
        test: / \. (? :ico|gif|png|jpg|jpeg|webp)$/.use: {
          loader: 'url-loader'.options: { limit: 5000}}},// Process the font WOFF
      {
        test: /\.woff(\? v=\d+\.\d+\/\d+)? $/.use: {
          loader: 'url-loader'.options: {
            limit: 5000.mimetype: 'application/font-woff'}}},// Process the font WOFF2
      {
        test: /\.woff2(\? v=\d+\.\d+\.\d+)? $/.use: {
          loader: 'url-loader'.options: {
            limit: 5000.mimetype: 'application/font-woff'}}},// Process the font TTF
      {
        test: /\.ttf(\? v=\d+\.\d+\.\d+)? $/.use: {
          loader: 'url-loader'.options: {
            limit: 5000.mimetype: 'application/octet-stream'}}},// Process the font EOT
      {
        test: /\.eot(\? v=\d+\.\d+\.\d+)? $/.use: 'file-loader'
      },
      / / deal with SVG
      {
        test: /\.svg(\? v=\d+\.\d+\.\d+)? $/.use: {
          loader: 'url-loader'.options: {
            limit: 5000.mimetype: 'image/svg+xml'}}}]},plugins: [
    // The Webpack module is hot reloaded
    new webpack.HotModuleReplacementPlugin({
      multiStep: false
    }),
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'development'
    }),
    new webpack.LoaderOptionsPlugin({
      debug: true
    }),
    ...htmlWebpackPlugin
  ],
  // Webpack service, the packaged page path is http://localhost:${port}/dist/${name}.html
  devServer: {
    port,
    publicPath,
    compress: true.noInfo: false.stats: 'errors-only'.inline: true.lazy: false.hot: true.headers: {'Access-Control-Allow-Origin': The '*'},
    contentBase: path.join(__dirname, 'dist'),
    watchOptions: {
      aggregateTimeout: 300.ignored: /node_modules/.poll: 100
    },
    historyApiFallback: {
      verbose: true.disableDotRule: false}}});Copy the code

Add run commands to package.json, package the main process and run the Electron project in dev-main and package the renderer process in dev-renderer

{... , "start": { ... , "dev-main": "cross-env NODE_ENV=development webpack --config webpack.main.dev.config.js && electron ./dist/main/main.dev.js", "dev-renderer": "cross-env NODE_ENV=development webpack-dev-server --config webpack.renderer.dev.config.js", "dev": "npm run dev-renderer" }, ... }Copy the code

Here rendering process can through update module thermal load code, but the main process can’t, and the main process of loading. The HTML files need to be packaged in rendering process is completed to load, thus modify the webpack. The renderer. Dev. Config. Js configuration, Add logic to package and run the main process after wrapping the renderer process

. , devServer: {before() {console.log('start main process... '); Spawn (' NPM ', ['run', 'dev-main'], {// NPM run dev-main shell: true, env: process.env, stdio: 'inherit' }).on('close', code => process.exit(code)) .on('error', spawnError => console.error(spawnError)); }},...Copy the code

Modify the main process loadHtml function, development mode through the URL to load the corresponding page

function loadHtml(window: BrowserWindow, name: string) {
  if (process.env.NODE_ENV === 'production') {
    window.loadFile(path.resolve(__dirname, `.. /renderer/${name}/index.html`)).catch(console.error);
    return;
  }
  // Development mode
  window.loadURL(`http://localhost:8080/dist/${name}.html`).catch(console.error);
}
Copy the code

NPM run dev runs as follows

renderer
userInfo
webpack.renderer.dev.config.js
webpack.renderer.prod.config

Webpack. The renderer. Dev. Config. Js

. const entry = {index: hot.concat(require.resolve('./src/renderer/index/index.tsx')), / / the main page
  userInfo: hot.concat(require.resolve('./src/renderer/userInfo/index.tsx')) / / the userInfo page}; .Copy the code

Webpack. The renderer. Prod. Config. Js

. const entry = {index: path.join(__dirname, 'src/renderer/index/index.tsx'), / / the main page
  userInfo: path.join(__dirname, 'src/renderer/userInfo/index.tsx') / / the userInfo page}; .Copy the code

The main process implements the creation logic for the userInfo window

function createUserInfoWidget() {
  if (userInfoWidget) return;
  if(! mainWindow)return;
  userInfoWidget = new BrowserWindow({
    parent: mainWindow,
    webPreferences: { nodeIntegration: true },
    backgroundColor: '# 333544',
    minWidth: 250,
    minHeight: 300,
    height: 300,
    width: 250
  });
  loadHtml(userInfoWidget, 'userInfo');
  userInfoWidget.on('close'.(a)= > userInfoWidget = null);
  userInfoWidget.webContents.on('crashed'.(a)= > console.error('crash'));
}
Copy the code

The main window renderer communicates with the main process using IPC to send a message to open the user information window

const onOpen = (a)= > { ipcRenderer.invoke('open-user-info-widget').catch(); };
Copy the code

The main process receives the renderer message and creates the userInfo window

ipcMain.handle('open-user-info-widget'.(a)= > {
  createUserInfoWidget();
})
Copy the code

The results

Build the application using Electron builder

The Electron Builder can be understood as a black box that can solve the packaging and build of the Electron project for various platforms (Mac, Window, Linux) and provide automatic update support. Install the following installation, note that the electron builder can only be installed under devDependencies

npm install --save-dev electron-builder
Copy the code

Then add the build field to package.json

{
  ...,
  "build": {
    "productName": "Electron App",
    "appId": "electron.app",
    "files": [
      "dist/",
      "node_modules/",
      "resources/",
      "native/",
      "package.json"
    ],
    "mac": {
      "category": "public.app-category.developer-tools",
      "target": "dmg",
      "icon": "./resources/icons/app.icns"
    },
    "dmg": {
      "backgroundColor": "#ffffff",
      "icon": "./resources/icons/app.icns",
      "iconSize": 80,
      "title": "Electron App"
    },
    "win": {
      "target": [ "nsis", "msi" ]
    },
    "linux": {
      "icon": "./resources/icons/app.png",
      "target": [ "deb", "rpm", "AppImage" ],
      "category": "Development"
    },
    "directories": {
      "buildResources": "./resources/icons",
      "output": "release"
    }
  },
  ...
}
Copy the code

Package packages multiple platforms. Package-mac builds the MAC platform package, package-win builds the Window platform package, and package-Linux builds the Linux platform package

{
  ...,
  "script": {
    "package": "npm run build && electron-builder build --publish never",
    "package-win": "npm run build && electron-builder build --win --x64",
    "package-linux": "npm run build && electron-builder build --linux",
    "package-mac": "npm run build && electron-builder build --mac" 
   }
  ...
}
Copy the code

In the process of packing, the electron builder will download the electron package. The normal download will time out (the wall has caused trouble again), resulting in the packaging failure. The solution is still to use mirror

 ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ npm run package-mac
Copy the code


C++ module support

When it comes to electron applications, you may need C++ module support, such as some functions implemented in C++, or calling existing C++ libraries or DLL files. Before compiling the webpack.base.config.js configuration, use node-loader to process the. Node file. However, when compiling the C++ plug-in under Electron, note that the V8 engine provided by Electron may be inconsistent with the V8 engine version provided by the locally installed node. Version mismatch may occur during compilation, so you may need to manually compile the Electron module to fit the current V8 version of Node when developing native C++ modules. Another option is to use the node-addon-api or Nan packages to write native C++ modules to automatically adapt to version V8 in Electron. For the node C++ module, see article: loading C++ code into JavaScript.

For example, a simple C++ addition calculation module, C++ part

#include <node_api.h>
#include <napi.h>
using namespace Napi;
Number Add(const CallbackInfo& info) {
    Number a = info[0].As<Number>();
    Number b = info[1].As<Number>();
    double r = a.DoubleValue() + b.DoubleValue();
    return Number::New(info.Env(), r);
}
Object Init(Env env, Object exports) {
    exports.Set("add", Function::New(env, Add));
    return exports;
}
NODE_API_MODULE(addon, Init)
Copy the code

Execute node-gyp rebuild to build the. Node file, the main process loads the. Node file, and registers an IPC call

import { add } from '~build/Release/addon.node';

ipcMain.handle('calc-value'.(event, a, b) = > {
  return add(+a, +b);
})
Copy the code

The renderer process makes an IPC call to send a calc-value message to get the result and render it to the page

const onCalc = (a)= > {
    ipcRenderer.invoke('calc-value', input.a, input.b).then(value= > {
      setResult(value);
    });
};
Copy the code

Redux + React-Router integration

The rest is to add some basic state libraries or route processing libraries. In the project, Redux is used to manage state and React-Router is used to process routes. The installation is as follows

npm install --save redux react-redux react-router react-router-dom history
npm install --save-dev @types/redux @types/react-redux @types/react-router @types/react-router-dom @types/history
Copy the code

Use HashRouter as the underlying routing pattern

const router = (
  <HashRouter>
    <Switch>
      <Route path="/" exact>
        <Page1/>
      </Route>
      <Route path="/page2">
        <Page2/>
      </Route>
    </Switch>
  </HashRouter>
);
Copy the code

React-router-dom provides useHistory Hooks to retrieve history and perform route-related operations, such as redirecting to a routing page

const history = useHistory();
const onNext = (a)= > history.push('/page2');
Copy the code

The Redux section can use useSelector and useDispatch Hooks to directly select state and connection dispatches in store, avoiding redundant code issues with connect higher-order components

const count = useSelector((state: IStoreState) = > state.count);
const dispatch = useDispatch();
const onAdd = (a)= > dispatch({ type: ActionType.ADD });
const onSub = (a)= > dispatch({ type: ActionType.SUB });
Copy the code

The results

Devtron AIDS in development tool integration

Devtron is an Electorn debugging tool for easy checking, monitoring and debugging applications. You can visualize package dependencies in the main and renderer processes, track and inspect messages sent to each other by the main and renderer processes, display registered events and listeners, check for possible problems in the app, and more.

npm install --save-dev devtron
Copy the code

Use as follows

app.whenReady().then((a)= > {
  require('devtron').install();
});
Copy the code

In addition, you can also use the electron Devtools-installer to install devTools extensions, such as the Redux and React extensions commonly used in browsers. It will automatically download the Chrome extension from the Chrome App Store and install it. Large probability will not download (evil wall to trouble again)

npm install --save-dev electron-devtools-installer @types/electron-devtools-installer
Copy the code

use

import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS, REACT_PERF } from 'electron-devtools-installer';

app.whenReady().then((a)= > {
  installExtension([REACT_PERF, REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]).then((a)= > {});
  require('devtron').install();
});
Copy the code

conclusion

In the early stage, I directly used the ready-made React template for development when I contacted electron, but blindly used the community template, which was difficult to find when problems occurred, and the functions provided by the community template did not necessarily meet my own needs. Although I made the wheel repeatedly, I could also learn a lot in the process of making the wheel. The project borrowed from the auto-react-Bolierplate packaging model and optimized it to add features. Subsequent TODO deals with package splitting optimizations and structural optimizations for the rendering and main processes.

reference

Electron document

Electron builder universal configuration

Electron builds cross-platform applications for Mac/Windows/Linux

Load C++ code into JavaScript

GitHub address: github.com/sundial-dre…