Используйте Webpack/React для упаковки и сборки приложений Electron.

внешний интерфейс Electron
Используйте Webpack/React для упаковки и сборки приложений Electron.

предисловие

Electron — это кроссплатформенный фреймворк для создания настольных приложений, позволяющий нам использовать HTML/CSS/JS для создания кроссплатформенных настольных приложений. С развитием большого внешнего интерфейса, когда мы разрабатываем веб-интерфейс, мы обычно будем использовать инструменты построения, такие как Webpack и фреймворки MVVM, такие как React, для помощи в разработке. То же самое относится и к разработке Electron, поэтому в этой статье будет рассказано, как использовать Webpack/React для упаковки и сборки всего приложения Electron, а также использовать Electron-builder для сборки приложения. На самом деле сообщество предоставляет множество шаблонов и шаблонов для Electron Webpack, таких какelectron-forge,electron-react-boilerplateИ так далее, но благодаря моим собственным исследованиям и конструированию (воспроизведению колеса) я могу глубже понять интерфейсную систему упаковки и конструирования.

содержание

  1. Об Электроне
  2. Электронная установка
  3. структурный дизайн
  4. Используйте веб-пакет для упаковки основных процессов и процессов рендеринга.
  5. Создавайте приложения с помощью электронного конструктора
  6. Поддержка модуля С++
  7. Интеграция Redux + React-маршрутизатор
  8. Интеграция вспомогательных средств разработки Devtron
  9. Суммировать
  10. Ссылаться на

Об Электроне

Electron — это фреймворк для создания собственных кроссплатформенных настольных приложений с использованием интерфейсных веб-технологий (HTML/CSS/JavaScript/React и т. д.), который можно рассматривать как комбинацию Chromium, Node.js и Native API.

Chromium является открытым исходным кодом Google и эквивалентен урезанной версии браузера Chrome и отвечает за визуализацию веб-интерфейса в Electron. Chromium позволяет разработчикам писать код веб-интерфейса без учета совместимости браузера.

Node.js — это легкая и эффективная среда выполнения JavaScript, основанная на управляемой событиями неблокирующей модели ввода-вывода. В Electron он отвечает за вызов базового API системы для работы с собственным графическим интерфейсом и выполнения кода JavaScript основного потока, а такие модули, как utils и fs, обычно используемые в Node.js, также могут использоваться непосредственно в Electron.

Нативные API — это функции графического интерфейса, предоставляемые системой, такие как системные уведомления, системные меню, открытие диалоговых окон системных папок и т. д. Electron обеспечивает поддержку функций операционной системы для приложений путем интеграции собственных API.

В отличие от традиционных веб-сайтов, Electron основан на модели процесса «ведущий-ведомый».Каждое приложение Electron имеет один и только один основной процесс (Main Process) и один или несколько процессов рендеринга (Renderer Process), соответствующих нескольким веб-страницам. Кроме того, он также включает в себя другие процессы, такие как процесс GUP, процесс расширения и так далее.

Основной процесс отвечает за создание окон, координацию межпроцессного взаимодействия, регистрацию и распределение событий и так далее. Процесс рендеринга отвечает за рендеринг страниц пользовательского интерфейса и реализацию интерактивной логики. Однако в рамках этой модели процесса легко создать проблему с единой точкой отказа, то есть сбой или блокировка основного процесса приведет к тому, что все приложение не сможет ответить.

Электронная установка

Проблема, встречающаяся в процессе установки электрона, может заключаться в том, что время ожидания сети (злая стена) происходит при загрузке электронного пакета, вызывая неудачную установку.

Решение, естественно, используется, здесь мы можем открыть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);
}

и получить путь загрузкиgetArtifactRemoteURLметод

async function getArtifactRemoteURL(details) {
    const opts = details.mirrorOptions || {};
    let base = mirrorVar('mirror', opts, BASE_URL); // ELECTRON_MIRROR 环境变量
    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环境变量,并将{{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}`;
}

Видно, что для указания зеркала можно определить множество переменных окружения, таких как ELECTRON_MIRROR, ELECTRON_CUSTOM_DIR и т.д., которые собственно и указаны в официальной документации

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

For instance, to usethe China CDN mirror:

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

Поэтому при загрузке Electron вам нужно всего лишь добавить две переменные окружения, чтобы решить проблему тайм-аута сети (стены).

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

После установки электрона можно попробовать написать простейшее приложение для электрона Структура проекта следующая

project
  |__index.js     # 主进程
  |__index.html   # 渲染进程
  |__package.json # 

соответствующий основной процессindex.jsчасть

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

let window = null;

function createWindow() {
  if (window) return;
  window = new electron.BrowserWindow({
    webPreferences: {
      nodeIntegration: true // 允许渲染进程中使用node模块
    },
    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)

Соответствующий процесс рендерингаindex.htmlчасть

<!DOCTYPE>
<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>

Кpackage.jsonДобавить команду запуска в

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

npm run startЗапустите, одна из простейших электронных приложений разработки завершена.

Структура проекта

Проекты Electron обычно состоят из основного процесса и процесса рендеринга.Основной процесс используется для реализации серверной части приложения.Как правило, C++ или rust используются для реализации основных функций и загружаются в основной процесс в виде плагинов Node. (например, Feishu, Feichat от ByteDance). Основной процесс реализован с использованием ржавчины), часть JavaScript похожа на слой клея, используемого для соединения Electron и сторонних плагинов, а процесс рендеринга заключается в реализации рисования веб-интерфейса и некоторая логика взаимодействия с пользовательским интерфейсом. Основной процесс и процесс рендеринга разрабатываются независимо, а для связи между процессами используется IPC, поэтому основной процесс и процесс рендеринга упаковываются отдельно, то есть два набора конфигураций webpack. среды разработки из производственной среды, также требуются два набора конфигураций веб-пакетов. Кроме того, при разработке электронных приложений потребуется несколько окон, поэтому процесс рендеринга упакован с несколькими страницами.Общая структура выглядит следующим образом.

project
  |__src
     |__main                                          # 主进程代码 
        |__index.ts
        |__other
     |__renderer                                      # 渲染进程代码
        |__index                                      # 一个窗口/页面
           |__index.tsx
           |__index.scss
        |__other   
  |__dist                                             # webpack打包后产物
  |__native                                           # C++代码
  |__release                                          # electron-builder打包后产物
  |__resources                                        # 资源文件
  |__babel.config.js                                  # babel配置
  |__tsconfig.json                                    # typescript配置
  |__webpack.base.config.js                           # 基础webpack配置
  |__webpack.main.dev.js                              # 主进程开发模式webpack配置
  |__webpack.main.prod.js                             # 主进程生产模式webpack配置
  |__webpack.renderer.dev.js                          # 渲染进程开发模式webpack配置
  |__webpack.renderer.prod.js                         # 渲染进程生产模式webpack配置

Процесс упаковки и сборки на самом деле относительно прост: используйте webpack для упаковки основного процесса и процесса рендеринга, и, наконец, используйте electronic-builder для упаковки и сборки упакованного кода и, наконец, для сборки приложения.Для многооконной обработки каждый каталог в процессе рендеринга представляет собой окно (страницу) и помечается в записи записи веб-пакета.dist/${name}В каталоге, когда основной процесс загружается, он загружается в соответствии с именем, указанным в записи веб-пакета.

Используйте веб-пакет для упаковки основных процессов и процессов рендеринга.

Сначала установите веб-пакет

npm install --save-dev webpack webpack-cli webpack-merge

установить реагировать

npm install --save react react-dom

установить машинопись

npm install --save-dev typescript

И установите пакет соответствующего типа

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

написать соответствующий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"
  ]
}

Написать базовую конфигурацию веб-пакетаwebpack.base.config.js, и основной процесс, и процесс рендеринга должны использовать эту конфигурацию веб-пакета.

const path = require('path');
// 基础的webpack配置
module.exports = {
  module: {
    rules: [
      // ts,tsx,js,jsx处理
      {
        test: /\.[tj]sx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader', // babel-loader处理jsx或tsx文件
          options: { cacheDirectory: true }
        }
      },
      // C++模块 .node文件处理
      {
        test: /\.node$/,
        exclude: /node_modules/,
        use: 'node-loader' // node-loader处理.node文件,用于处理C++模块
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '.node'],
    alias: {
      '~native': path.resolve(__dirname, 'native'), // 别名,方便import
      '~resources': path.resolve(__dirname, 'resources') // 别名,方便import
      
    }
  },
  devtool: 'source-map',
  plugins: []
};

Установитьbabel-loaderОбрабатывать файлы jsx или tsx,node-loaderОбрабатывать файлы .node

npm install --save-dev babel-loader node-loader

установить соответствующийПлагин Бабель

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

и установкабабель пресеты

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

написать соответствующийbabel.config.jsКонфигурацию, код в режиме разработки и режиме производства нужно обрабатывать отдельно в конфигурации, то есть с использованием разных плагинов

const devEnvs = ['development', 'production'];
const devPlugins = []; // TODO 开发模式

const prodPlugins = [ // 生产模式
  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编译目标,electron版本
        }
      }],
      require('@babel/preset-typescript'), // typescript支持
      [require('@babel/preset-react'), {development, throwIfNamespace: false}] // react支持
    ],
    plugins: [
      [require('@babel/plugin-proposal-optional-chaining'), {loose: false}], // 可选链插件
      [require('@babel/plugin-proposal-decorators'), {legacy: true}], // 装饰器插件
      require('@babel/plugin-syntax-dynamic-import'), // 动态导入插件
      require('@babel/plugin-proposal-class-properties'), // 类属性插件
      ...(development ? devPlugins : prodPlugins) // 区分开发环境
    ]
  };
};

Конфигурация упаковки веб-пакета основного процесса

Когда основной процесс запакован, необходимо толькоsrc/mainВсе файлы ts упакованы вdist/mainДалее стоит отметить, что основной процесс соответствует нодовому проекту, если использовать webpack для упаковки напрямую, то он будетnode_modulesМодули также упакованы, поэтому здесь мы используемwebpack-node-externalsплагин для исключенияnode_modulesмодуль

npm install --save-dev webpack-node-externals

Соответствующая конфигурация веб-пакета в режиме разработкиwebpack.main.dev.config.jsследующим образом

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', // 开发模式
  target: 'node',
  entry: path.join(__dirname, 'src/main/index.ts'),
  output: {
    path: path.join(__dirname, 'dist/main'),
    filename: 'main.dev.js' // 开发模式文件名为main.dev.js
  },
  externals: [nodeExternals()], // 排除Node模块
  plugins: [
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'development'
    })
  ],
  node: {
    __dirname: false,
    __filename: false
  }
});

Режим производства аналогичен режиму разработки, поэтому он соответствует конфигурации веб-пакета.webpack.main.prod.config.jsследующим образом

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', // 生产模式
  output: {
    path: path.join(__dirname, 'dist/main'),
    filename: 'main.prod.js' // 生产模式文件名为main.prod.js
  },
  plugins: [
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'production'
    })
  ]
});

Конфигурация упаковки процесса рендеринга

Упаковка процесса рендеринга - это процесс упаковки обычного внешнего проекта. Учитывая требование нескольких окон в электронном проекте, процесс рендеринга упакован на нескольких страницах. Упакованная структура процесса рендеринга выглядит следующим образом.

dist
  |__renderer # 渲染进程
     |__page1 # 页面1
        |__index.html
        |__index.prod.js
        |__index.style.css
     |__page2 # 页面2
        |__index.html
        |__index.prod.js
        |__index.style.css
производственный режим

Давайте сначала посмотрим на упаковку в режиме производства, установим соответствующие плагины и загрузчик, используйте здесьhtml-webpack-pluginПлагин генерирует HTML-шаблоны и должен создавать файл .html для каждой страницы.

npm install --save-dev mini-css-extract-plugin html-webpack-plugin

css-loader,sass-loader,style-loaderстили обработки,url-loader,file-loaderработа с изображениями и шрифтами,resolve-url-loaderОбрабатывать файлы scssurl()Проблема относительного пути в

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

Поскольку вы используете scss для написания стилей, вам необходимо установитьnode-sassМешок

npm install --save-dev node-sass

Установитьnode-sassНа самом деле ям довольно много.Обычная установка часто сталкивается с проблемой тайм-аута сети загрузки (и по вине стены), и общее решение - полагаться на зеркалирование.

Добавлено во время установки--sass-binary-siteпараметры, как показано ниже

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

Соответствующая конфигурация веб-пакета для производственного режимаwebpack.renderer.prod.config.jsследующим образом

// 渲染进程prod环境webpack配置
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'), // 页面入口
};
// 对每一个入口生成一个.html文件
const htmlWebpackPlugin = Object.keys(entry).map(name => new HtmlWebpackPlugin({
  inject: 'body',
  scriptLoading: 'defer',
  template: path.join(__dirname, 'resources/template/template.html'), // template.html是一个很简单的html模版
  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' // 输出则是每一个入口对应一个文件夹
  },
  module: { 
    rules: [ // 文件处理规则
      // 处理全局.css文件
      {
        test: /\.global\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: { publicPath: './' }
          },
          {
            loader: 'css-loader',
            options: { sourceMap: true }
          },
          {loader: 'resolve-url-loader'}, // 解决样式文件中的相对路径问题
        ]
      },
      // 一般样式文件,使用css模块
      {
        test: /^((?!\.global).)*\.css$/,
        use: [
          { loader: MiniCssExtractPlugin.loader },
          {
            loader: 'css-loader',
            options: {
              modules: { localIdentName: '[name]__[local]__[hash:base64:5]' },
              sourceMap: true
            }
          },
          {loader: 'resolve-url-loader'},
        ]
      },
      // 处理scss全局样式
      {
        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 }
          }
        ]
      },
      // 处理一般sass样式,依然使用css模块
      {
        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 }
          }
        ]
      },
      // 处理字体文件 WOFF
      {
        test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
        use: {
          loader: 'url-loader',
          options: { limit: 10000, mimetype: 'application/font-woff' }
        }
      },
      // 处理字体文件 WOFF2
      {
        test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
        use: {
          loader: 'url-loader',
          options: { limit: 10000, mimetype: 'application/font-woff' }
        }
      },
      // 处理字体文件 TTF
      {
        test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
        use: {
          loader: 'url-loader',
          options: {  limit: 10000, mimetype: 'application/octet-stream' }
        }
      },
      // 处理字体文件 EOT
      {
        test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        use: 'file-loader'
      },
      // 处理svg文件 SVG
      {
        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
        use: {
          loader: 'url-loader',
          options: { limit: 10000, mimetype: 'image/svg+xml' }
        }
      },
      // 处理图片
      {
        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
  ]
});

На данный момент конфигурация упаковки основного процесса и конфигурация упаковки производственного режима процесса рендеринга завершены, и результаты упаковки производственной среды проекта могут быть непосредственно протестированы здесь.

первымpackage.jsonДобавьте соответствующую команду запуска вbuild-mainупаковать основной процесс,build-rendererпроцесс рендеринга пакета,buildОсновной процесс и процесс рендеринга упакованы параллельно,start-mainЗапустите проект Электрон

{
  ...
  "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"
  },
  ...
}

используется в сценарияхcross-env, как следует из названия, обеспечивает кроссплатформенную поддержку переменных среды, аconcurrentlyДля параллельного запуска команд установите следующим образом

npm install --save-dev cross-env concurrently

Можно попробовать написать небольшой пример для проверки результата упаковки, основной процессsrc/main/index.ts

import { BrowserWindow, app } from 'electron';
import path from "path";
// 加载html,目前只对生产模式进行加载
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;
// 创建窗口
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', () => mainWindow = null);
  mainWindow.webContents.on('crashed', () => console.error('crash'));
}
app.on('ready', () => { createMainWindow() });
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});
app.on('activate', () => { createMainWindow() })

Главная страница процесса рендерингаsrc/renderer/index/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';

// @ts-ignore
import style from './index.scss'; // typescript不支持css模块,所以这么写编译器会不识别,建议加个@ts-ignore

function App() {
  return (
    <div className={style.app}>
      <h3>Hello world</h3>
      <button>+ Import</button>
    </div>
  )
}

ReactDOM.render(<App/>, document.getElementById('app'));

использоватьbuildКоманда для параллельной упаковки кода основного процесса и процесса визуализации

npm run build

Упакованный результат показан ниже, поэтому путь основного процесса при загрузке html-файла../renderer/${name}/index.html использоватьnpm run start-mainкоманда для запуска проекта.

режим разработки

В режиме разработки процесса рендеринга необходимо реализовать горячую загрузку модулей, которые используются здесьreact-hot-loaderpackage, и если вам нужно запустить службу webpack, вам также необходимо установитьwebpack-dev-serverМешок.

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

Измените конфигурацию babel и добавьте следующие плагины в среду разработки.

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

Измените файл записи процесса рендеринга, то есть вrenderПри оценке текущей среды и упаковкиReactHotContainer

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')
);

Соответствующая конфигурация веб-пакета для режима разработкиwebpack.renderer.prod.config.js

// 渲染进程dev环境下的webpack配置
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')),
};
// 生成html模版
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' // 开发模式下
    }
  },

  output: { publicPath, filename: '[name].dev.js' },

  module: {
    rules: [
      // 处理全局css样式
      { 
        test: /\.global\.css$/,
        use: [
          {loader: 'style-loader'},
          {
            loader: 'css-loader',
            options: {sourceMap: true}
          },
          {loader: 'resolve-url-loader'},
        ]
      },
      // 处理css样式,使用css模块
      { 
        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'}
        ]
      },
      // 处理全局scss样式
      {
        test: /\.global\.(scss|sass)$/,
        use: [
          {loader: 'style-loader'},
          {
            loader: 'css-loader',
            options: {sourceMap: true}
          },
          {loader: 'resolve-url-loader'},
          {loader: 'sass-loader'}
        ]
      },
      // 处理scss样式,使用css模块
      {
        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'}
        ]
      },
      // 处理图片
      {
        test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
        use: {
          loader: 'url-loader',
          options: { limit: 5000 }
        }
      },
      // 处理字体 WOFF
      {
        test: /\.woff(\?v=\d+\.\d+\/\d+)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 5000,
            mimetype: 'application/font-woff'
          }
        }
      },
      // 处理字体 WOFF2
      {
        test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 5000,
            mimetype: 'application/font-woff'
          }
        }
      },
      // 处理字体 TTF
      {
        test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 5000,
            mimetype: 'application/octet-stream'
          }
        }
      },
      // 处理字体 EOT
      {
        test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        use: 'file-loader'
      },
      // 处理SVG
      {
        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 5000,
            mimetype: 'image/svg+xml'
          }
        }
      }
    ]
  },

  plugins: [
    // webpack 模块热重载
    new webpack.HotModuleReplacementPlugin({
      multiStep: false
    }),
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'development'
    }),
    new webpack.LoaderOptionsPlugin({
      debug: true
    }),
    ...htmlWebpackPlugin
  ],
  // webpack服务,打包后的页面路径为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': '*'},
    contentBase: path.join(__dirname, 'dist'),
    watchOptions: {
      aggregateTimeout: 300,
      ignored: /node_modules/,
      poll: 100
    },
    historyApiFallback: {
      verbose: true,
      disableDotRule: false
    }
  }
});

Кpackage.jsonДобавьте команду запуска вdev-mainУпакуйте основной процесс и запустите проект Electron в режиме разработки,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"
  },
  ...
}

Здесь процесс рендеринга может обновить код с помощью горячей загрузки модуля, но основной процесс не может, и файл .html, загруженный основным процессом, должен быть загружен после того, как процесс рендеринга упакован, поэтому изменитеwebpack.renderer.dev.config.jsКонфигурация, добавить логику упаковки и запуска основного процесса после упаковки процесса рендеринга

...,
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));
    }
},
...

изменить основной процессloadHtmlфункция, режим разработки черезurlзагрузить соответствующую страницу

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;
  }
  // 开发模式
  window.loadURL(`http://localhost:8080/dist/${name}.html`).catch(console.error);
}

npm run devЗапуск в режиме разработки выглядит следующим образом

упаковать несколько окон,rendererновый каталогuserInfoКаталог представляет собой окно с информацией о пользователе и добавляется в файл конфигурации в режиме разработки и режиме производства, т.е.webpack.renderer.dev.config.jsа такжеwebpack.renderer.prod.configв вводной записи.

webpack.renderer.dev.config.jsчасть

...
const entry = {
  index: hot.concat(require.resolve('./src/renderer/index/index.tsx')), // 主页面
  userInfo: hot.concat(require.resolve('./src/renderer/userInfo/index.tsx')) // userInfo页面
};
...

webpack.renderer.prod.config.jsчасть

...
const entry = {
  index: path.join(__dirname, 'src/renderer/index/index.tsx'), // 主页面
  userInfo: path.join(__dirname, 'src/renderer/userInfo/index.tsx') // userInfo页面
};
...

Основной процесс реализуетuserInfoЛогика создания окна

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', () => userInfoWidget = null);
  userInfoWidget.webContents.on('crashed', () => console.error('crash'));
}

Процесс рендеринга главного окна использует IPC для связи с основным процессом и отправляет сообщение, чтобы открыть окно с информацией о пользователе.

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

Основной процесс получает сообщение о процессе рендеринга и создаетuserInfoокно

ipcMain.handle('open-user-info-widget', () => {
  createUserInfoWidget();
})

результат операции

Создание приложений с помощью Electron-builder

Electron-builderЕго можно понимать как черный ящик, который может решить упаковку и сборку различных платформ (Mac, Window, Linux) проекта Electron и обеспечить поддержку автоматического обновления. Установка выглядит следующим образом, следует отметить, что электрон-билдер можно установить только наdevDependenciesВниз

npm install --save-dev electron-builder

затем в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"
    }
  },
  ...
}

и кpackage.jsonДобавьте команду запуска вpackageупаковка для нескольких платформ,package-macсобрать пакет платформы Mac,package-winСоберите пакет оконной платформы,package-linuxСоберите пакет платформы Linux

{
  ...,
  "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" 
   }
  ...
}

При выполнении упаковки электрон-билдер загружает электронный пакет, а нормальная загрузка истечет по тайм-ауту (стена снова в беде), что приведет к неудачной упаковке, и решение по-прежнему заключается в использовании зеркалирования.

 ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ npm run package-mac
После завершения сборки вы можете увидеть упакованный результат в каталоге выпуска, который представляет собой файл .dmg под Mac.Дважды щелкните, чтобы установить на Mac

Поддержка модуля С++

Когда дело доходит до электронных приложений, может потребоваться поддержка модуля C++, например, некоторые функции реализованы на C++ или вызывают существующие библиотеки C++ или файлы dll. писать доwebpack.base.config.jsиспользуется при настройкеnode-loaderДля обработки файлов .node, но при написании подключаемых модулей C++ под Electron следует учитывать, что движок V8, предоставляемый Electron, может не соответствовать версии движка V8, предоставляемой локально установленным Node, что приводит к проблеме несоответствия версий. Поэтому при разработке собственных модулей C++ может потребоваться вручную скомпилировать модули Electron, чтобы они соответствовали текущей версии V8 Node. Другой способ - использоватьnode-addon-apiпакет илиNanpackage для написания собственных модулей C++ для автоматической адаптации к версии V8 в Electron. Модули Node C++ см. в статье:Загрузить код C++ в JavaScript.

Например, простой модуль вычисления сложения C++, часть C++

#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)

воплощать в жизньnode-gyp rebuildСоздайте файл .node, основной процесс — загрузка файла .node и регистрация вызова IPC.

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

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

Процесс рендеринга отправляет вызов IPCcalc-valueСообщение получает результат и отображает его на странице

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

Интеграция Redux + React-Router

На данный момент структура проекта в основном построена, а остальное - добавить некоторые основные библиотеки состояний или библиотеки обработки маршрутизации.В проекте используется Redux для управления состоянием, а React-Router обрабатывает маршрутизацию.Установка выглядит следующим образом

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

использоватьHashRouterкак базовый шаблон маршрутизации

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

react-router-domпри условииuseHistoryХуки удобны для получения истории для выполнения операций, связанных с маршрутизацией, таких как переход на страницу маршрутизации.

const history = useHistory();
const onNext = () => history.push('/page2');

Часть Redux можно использоватьuseSelectorа такжеuseDispatchХуки, напрямую выбирают состояние в хранилище и подключают отправку, чтобы избежать проблем с избыточным кодом, вызванных использованием компонентов высокого порядка подключения.

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

результат операции

Интеграция вспомогательных средств разработки Devtron

Devtron — это инструмент отладки Electron, который упрощает проверку, мониторинг и отладку приложений. Вы можете визуализировать зависимости пакетов в основном процессе и процессе рендеринга, отслеживать и проверять сообщения, отправляемые основным процессом и процессом рендеринга друг другу, отображать зарегистрированные события и прослушиватели, а также проверять наличие возможных проблем в приложении.

Способ установки

npm install --save-dev devtron

Используйте следующим образом

app.whenReady().then(() => {
  require('devtron').install();
});

Также вы можете использоватьelectron-devtools-installer, используется для установки расширений Devtools, таких как Redux, расширений React, обычно используемых в браузерах и т. д. Он автоматически переходит в магазин приложений Chrome для загрузки расширений Chrome и их установки, но из-за стены существует высокая вероятность того, что они не скачивается (злая стена опять грядет беда)

npm install --save-dev electron-devtools-installer @types/electron-devtools-installer

Как использовать

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

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

Суммировать

В первые дни, когда я связался с электроном, я напрямую использовал готовый шаблон реакции для разработки, но вслепую использовал шаблон сообщества, который было сложно найти при возникновении проблемы, а функции, предоставляемые шаблоном сообщества, не помогали. не обязательно удовлетворять мои собственные потребности.Вы также можете многому научиться в процессе вращения. Проект опирается наelectron-react-bolierplateРежим упаковки , оптимизирует и настраивает некоторые места и добавляет некоторые соответствующие функции. Следующим TODO является проверка оптимизации разделения пакетов и корректировка структурной оптимизации процесса рендеринга и основного процесса.

Ссылаться на

Электронная документация

общая конфигурация электронного построителя

Electron создает кроссплатформенные приложения для Mac/Windows/Linux

Загрузить код C++ в JavaScript

Адрес проекта на GitHub: GitHub.com/солнечные часы-электрические…