基于React与Typescript构建Electron应用

发布时间:2021-07-19 23:17
最后更新:2021-07-19 23:17
所属分类:
前端 React

Electron 可以利用前端技术构建跨平台的 GUI 应用,但是 Electron 主要提供的是使用基础 HTML 5 技术来展现 UI 界面的,而且后端技术也是因为基于 Node.js,而限定在了 Javascript 上。这就对计划采用 React 搭配 Typescript 开发 GUI 应用相当的不友好,也就需要更多的配置环节。这里将通过对构建和配置过程进行记录来为基于 React 和 Typescript 开发 GUI 应用提供一条通道。

构建目标及原则

由于 Electron 是分为渲染线程和主线程两套代码的,所以对于代码的处理也是需要有两套。但是总结起来,项目整体的构建目标主要有以下几点。

  1. 渲染线程和主线程均采用 Typescript 编写。
  2. 渲染线程基于 React 框架编写。
  3. 在开发过程中,渲染线程与主线程的代码要能够做到热重载。
  4. 开发与发布的过程要尽可能的自动化,减少命令键入的次数。

另外为了简化项目整体的构建和配置复杂性,在寻找解决方案的时候,将遵照以下原则来完成拣选。

  1. 不使用 Eject 来做项目的详细配置。
  2. 尽可能使用create-react-app工具自身提供的功能完成配置。
  3. 尽可能使用更少的辅助库。
  4. 在 Windows、macOS、Linux 上都能够得到一致的配置方法和运行效果。

构建过程

因为整个应用项目中,渲染线程的部分占据的比例非常大,所以项目的构建过程先从前端部分开始。

1
npx create-react-app electron-app --template typescript

首先使用create-react-app工具利用typescript模版构建一个使用 Typescript 开发 React 应用的前端项目。然后我们就需要进入到已经创建好的项目目录中,继续添加要使用的其他依赖库。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 进入到项目目录中
cd electron-app

# 安装项目中所需要用到的功能库
# 安装React配置工具以及跨平台的环境变量设置工具
npm install -D react-app-rewired customize-cra cross-env

# 安装前端路由库
npm install react-router react-router-dom

# MobX状态管理库可以根据需要换成其他状态管理库,例如Redux
npm install mobx mobx-react-lite mobx-state-tree

# 安装Ajax请求处理工具,SWR为自动化远程数据缓存
npm install axios swr

# 安装前端样式库,这里采用了Fluent UI,可根据需要更换
npm install @fluentui/react

# 安装Electron
npm install -D electron

完成这些内容的安装以后,实际上就可以开始一个前端项目的开发了。但是如果要让项目运行在 Electron 中,还有一些工作要做。

由于位于渲染线程的React APP需要调用electron的一些内容,所以需要修改一下项目根目录中的config-overrides.js来使项目的编译目标改为electron-renderer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const { override } = require('customize-cra');

function addRendererTarget(config){
    config.target = 'electron-renderer';
    return config;
}

module.exports = override(
    addRendererTarget
);
这样一来,React APP将无法在浏览器中正常运行,加入这个改动以后,就不要再用浏览器调试React APP了,下文中使用BROWSER=none的环境变量也是为了防止CRA自动打开浏览器。

因为项目中已经安装了 Typescript 库,所以可以直接在 Electron 的主线程中使用 Typescript,当然前提只需要配置好 Typescript 的编译。在这个项目中,设计应用的渲染线程代码位于src目录中,主线程的代码位于src-main目录中,其中主线程代码所在的目录可以根据需要修改。

src这个目录被 CRA 的配置占据了,而且修改起来不是太容易所以就保留了。

渲染线程和主线程的代码分目录放的原因主要有以下几点:

  1. 渲染线程和主线程所使用的技术和库不同,使用独立目录存放可以从视觉和习惯上进行区分。
  2. 渲染线程和主线程在使用tsc进行编译的时候可以采用不同的配置。

因为开发的过程中需要热重载技术,而 React 中的热重载技术是由 Webpack DevServer 提供的,所以在开发过程中,要实现 Electron 中的热重载,也必须让 Electron 加载 Webpack DevServer 提供的地址http://localhost:3000,打包发布的时候再去加载打包目录中的index.html。这样一来就需要区分开发环境和生产环境了。主线程代码的实时编译是由tsc提供的,所以除 Webpack DevServer 以外,还需要再运行一个tsc实例。而且这个tsc实例需要配置一些与 React 应用不同的内容,为了方便,可以直接把 React 的tsconfig.json复制一份修改一下。这里仅举出一部分的关键设置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
    "compilerOptions": {
        "target": "es5",
        "lib": ["dom.iterable", "esnext"],
        "allowJs": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "noFallthroughCasesInSwitch": true,
        "module": "CommonJS",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "outDir": "./build"
    },
    "include": ["src-main/**/*"]
}

在这里,我们把这个用于编译主线程的 Typescript 配置命名为tsconfig.main.json,然后与之前已有的tsconfig.json放置在一起。

这样一来,要启动整个应用的调试就需要同时运行 Webpack DevServer,也就是npm start命令,还有electron,和用于编译主线程代码的tsc。为了达到这个目的,就需要更多的工具来支持了,在这里我们需要安装一个库:concurrently。

1
2
# 安装concurrently
npm install -D concurrently

Concurrently 可以同时运行多条命令,正好可以满足我们同时启动 Webpack DevServer、tsc和 Electron 的需要。这时我们就可以把package.jsonscripts一节改变一下样子。

1
2
3
4
5
6
7
8
9
{
    "scripts": {
        "start": "concurrently \"npm:start:renderer\" \"npm:build:main\" \"npm:start:electron\"",
        "start:renderer": "cross-env BROWSER=none react-app-rewired start",
        "build:main": "tsc --project tsconfig.main.json --watch",
        "start:electron": "cross-env NODE_ENV=development electron ."
    },
    "main": "./build/main.js"
}

现在直接运行npm start应该可以同时启动三个进程了,但是又出现了一个新的问题,那就是 Electron 启动以后什么也没有加载。如果打开看一下 DevTools,可以发现 Electron 根本没有加载 DevServer 提供的内容。那是因为 Electron 根本就没有等待 DevServer 启动完毕就直接尝试加载http://localhost:3000这个 URL 了。所以我们还需要再改进一下,这样就需要wait-on库的支持了。

1
2
# 安装wait-on
npm install -D wait-on

wait-on库可以提供在指定条件就绪的时才继续执行下一条命令的功能。所以借助wait-on的支持,package.jsonscripts的样子还可以再改进一下。

1
2
3
4
5
6
7
8
9
{
    "scripts": {
        "start": "concurrently \"npm:start:renderer\" \"npm:build:main\" \"npm:start:electron\"",
        "start:renderer": "cross-env BROWSER=none react-app-rewired start",
        "build:main": "tsc --project tsconfig.main.json --watch",
        "start:electron": "wait-on http://localhost:3000 && cross-env NODE_ENV=development electron ."
    },
    "main": "./build/main.js"
}

现在直接执行npm start可以顺利的打开Electron并让Electron加载DevServer提供的内容了。但是当我们实际编写一些主线程的代码以后会发现,Electron还是维持着编辑之前的代码,要打算让新代码生效,就必须手动重启Electron。这显然也不是我们追求自动化的风格,所以我们就需要再引入一个库了:electronmon

1
2
# 安装Electronmon
npm install -D electronmon

electronmon可以监视与主线程脚本有关的所有变化,并在这些变化发生的时候,自动重启Electron。这功能完美的匹配了我们的需求,于是package.json就修改为了以下这个样子。

1
2
3
4
5
6
7
8
9
{
    "scripts": {
        "start": "concurrently \"npm:start:renderer\" \"npm:build:main\" \"npm:start:electron\"",
        "start:renderer": "cross-env BROWSER=none react-app-rewired start",
        "build:main": "tsc --project tsconfig.main.json --watch",
        "start:electron": "wait-on http://localhost:3000 && cross-env NODE_ENV=development electronmon ."
    },
    "main": "./build/main.js"
}

好了,现在再来启动一下吧,无论渲染线程还是主线程,都会根据变化自动重启加载了。

关于发布

对于Electron应用来说,最简单的发布工具就是electron-builder,但是这个工具并没有什么特殊的,只需要使用默认配置就可以完成三个平台的编译打包。

踩过的一些坑

用Typescript开发使用React作为渲染线程的Electron应用不是没有坑的,这里仅记录一些目前已经遇到的。

各种require() is not defined

在DevTools上出现这种提示,往往是因为BrowserWindow创建的时候webPreferences配置给的不正确。因为React APP和基于Typescript的主线程都需要require(),所以webPreferences的配置需要参考以下示例。

1
2
3
4
5
6
7
8
webPreferences: {
    // 启动nodeIntegration是为了在渲染线程中使用Node.js的API
    nodeIntegration: true,
    nodeIntegrationInWorker: true,
    nodeIntegrationInSubFrames: true,
    enableRemoteModule: true,
    contextIsolation: false
}

另外还有一种情况,就是之前用于customize-craconfig-overrides.js中把Webpack的编译目标设为了electron-renderer,这就导致了Webpack在不支持require的文件中也同样使用了require,从而导致运行的时候报出了require() is not defined错误。这也是在计划使用preload.js,开启了contextIsolation以后出现的错误的根本原因。所以如果计划使用Electron推荐的安全策略,采用preload.js控制对于底层API的暴露,那么可以去掉config-overrides.js中Webpack的target设置,然后将WebPerferences的配置改为以下形式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
webPerferences: {
    // 取消在Renderer线程中使用Node.js API的能力
    nodeIntegration: false,
    // 对于PWA中的Service Worker可以适当放开,也可以继续限制其对于底层API的使用
    nodeIntegrationInWorker: true,
    nodeIntegrationInSubFrames: false,
    enableRemoteModule: false,
    contextInsolation: true,
    preload: path.join(__dirname, 'preload.js')
}

出现了Electron failed to install correctly

这种情况一般在使用淘宝的npm镜像的时候容易发生,而发生这种情况的主要原因是Electron没有完全安装。要解决这个问题可以直接安装一个辅助库来完成Electron的安装。

1
2
3
4
# 安装electron-fix
npm install -D electron-fix
# 执行已经安装的electron-fix即可重新开始Electron的下载。
npx electron-fix start

一个更加迅速的方法

基于以上调试经验,我制作了一个CRA的项目模版cra-template-typescript-electron,可以直接构建一个完整的Electron应用,这个模版可以使用以下命令在本地创建项目。

1
npx create-react-app my-app --template typescript-electron

索引标签
前端
React
Typescript
Electron