使用React构建Electron应用-入门踩坑篇

最近几个晚上都在编写dotflow(一个前端工作流平台),想尝试一次新技术,就选择了React+Electron技术栈。React是一个前端UI渲染框架,结合React全家桶做SPA也是一把好手;Electron是基于Nodejs的一个编写客户端软件的框架,ps:呵呵,万能的js。

一开始React方面看中了阿里的轮子antDesign+umi,然而在经历了三天的尝试之后弃坑逃跑,后面专门留一段说为什么会弃坑。

初始化项目

首先我们需要依赖fb官方推荐的React应用构建脚手架create-react-app。以下简称cra
根据文档说明,我们成功创建了项目之后,本地的目录结构应该是这个样子的
创建之后的目录

这里我的编辑器使用的是vscode,我们可以直接在编辑器里面直接打开终端,运行命令yarn start,运行成功后,浏览器会打开一个页面,像下面图片所示:
运行成功

接下来的步骤很关键,cra的封装性做的非常好,你可以不用修改任何配置文件就可以完成一个React应用的开发,当然我们这里是需要修改很多配置文件的,我们新建一个终端标签页,输入yarn eject根据提示完成之后,就会看到文件目录里多了很多文件,这些文件就是项目所有预置的配置文件。这时候,你还需要执行一次yarn命令,来完成所有npm包的安装。项目初始化已经完成。

集成Electron

第一步初始化项目如果你完成的很顺利的话,我们进入第二步,把Electron集成进去。接下来我们开始大刀阔斧地改动项目文件

two-package-structure

按照ELectron的约定,最佳实践是使用two-package-structure结构来构建应用,我们分别在src目录下分别建立mainrenderer文件夹,然后把之前src目录下的文件全部放入renderer文件夹中,然后在main文件夹下建立我们客户端开发需要的业务文件,这里可以参考我在github上的示例代码,我将客户端业务全部写成多个services,通过全局global暴露出去。在renderer端使用remote形式对services进行调用,具体的不在这里赘述,后面开新篇具体说明原理。

项目根目录新建app文件夹,具体文件参考github源码

修改配置文件

  • package.json
    dependencies参考github上的源码
    script参考如下代码
1
2
3
4
5
6
7
8
9
10
"scripts": {
"postinstall": "cd app && tnpm i && cd .. && npm run rebuild",
"start": "concurrently \"node scripts/start.js\" \"npm run build:electron\"",
"start:electron": "electron app/dist/main",
"build": "node scripts/build.js",
"build:electron": "node scripts/electron.js",
"rebuild": "electron-rebuild ./app",
"pack": "npm run build && npm run build:electron && npm run rebuild && electron-builder",
"test": "node scripts/test.js --env=jsdom"
},
  • config配置文件
    config/paths.js文件里面的几个配置分别作对应的修改,代码如下:
1
2
3
4
5
6
7
function getServedPath(appPackageJson) {
const publicUrl = getPublicUrl(appPackageJson);
const servedUrl = envPublicUrl || (publicUrl
? url.parse(publicUrl).pathname
: './'); //这里修改webpack的publicPath为了让electron打包之后能访问到页面
return ensureSlash(servedUrl, true);
}
1
2
3
4
//修改入口目录文件
appBuild: resolveApp('app/dist'),//更改build的outputPath
appIndexJs: resolveApp('src/renderer/index.js'),
appSrc: resolveApp('src/renderer'),

config文件夹下新建webpack.config.electron.js文件,添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const path = require('path');
const webpack = require('webpack');
const paths = require('./paths');
module.exports = {
target: 'electron-renderer',
entry: {
main: './src/main/index.js'
},
output: {
path: paths.appBuild,
filename: '[name].js'
},
externals(context, request, callback) {
callback(null, request.charAt(0) === '.'
? false
: `require("${request}")`);
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
}
]
},
plugins: [new webpack.DefinePlugin({$dirname: '__dirname'})]
};

这里简单说一下,这个webpack配置文件是打包electron业务代码到app/dist文件夹下

为了让命令执行和cra之前的方式统一,在scripts文件夹下面建立electron.js 这个脚本是执行刚才建立的webpack.config.electron.js的任务的,脚本我已写好,贴上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
'use strict';

// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';

// Makes the script crash on unhandled rejections instead of silently ignoring
// them. In the future, promise rejections that are not handled will terminate
// the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});

// Ensure environment variables are read.
require('../config/env');

const path = require('path');
const chalk = require('chalk');
const webpack = require('webpack');
const config = require('../config/webpack.config.electron');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');

// Create the production build and print the deployment instructions.
function build() {
console.log('正在编译打包electron-renderer...');

let compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) {
return reject(err);
}
const messages = formatWebpackMessages(stats.toJson({}, true));
if (messages.errors.length) {
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(new Error(messages.errors.join('\n\n')));
}
if (process.env.CI && (typeof process.env.CI !== 'string' || process.env.CI.toLowerCase() !== 'false') && messages.warnings.length) {
console.log(chalk.yellow('\nTreating warnings as errors because process.env.CI = true.\nMost CI servers se' +
't it automatically.\n'));
return reject(new Error(messages.warnings.join('\n\n')));
}

return resolve({stats, warnings: messages.warnings});
});
});
}

build().then(({stats, warnings}) => {
if (warnings.length) {
console.log(chalk.yellow('Compiled with warnings.\n'));
console.log(warnings.join('\n\n'));
console.log('\nSearch for the ' + chalk.underline(chalk.yellow('keywords')) + ' to learn more about each warning.');
console.log('To ignore, add ' + chalk.cyan('// eslint-disable-next-line') + ' to the line before.\n');
} else {
console.log(stats.toString({
chunks: false, // 使构建过程更静默无输出
colors: true // 在控制台展示颜色
}))
console.log(chalk.green('✨ 编译 成功.\n'));
}
});

新增开发者工具

项目根目录新增extensions目录,这里面放置electron内置的开发者工具,我在github连接上预置了redux-devtools,由于我的电脑无法编译最新版的react-developer-tools工具,所以我将这个工具调用代码注释了

重新运行

在更改完文件夹,修改完一堆配置文件之后,这时候我们重新在终端中运行yarn start 如果成功运行的话,浏览器会出现一开始打开的界面

我们新建一个终端标签页,运行yarn start:electron,如果成功运行的话,本地会弹出一个应用程序调用,你会看到下面图片上的画面

这时在react和electron的集成构建应用我们就完成了

打包应用

新建终端标签页,执行命令yarn run pack,执行成功后,我们可以看到终端运行情况

此时正在打包中

打包完成后,我们在项目根目录可以看到生成了一个dist文件夹,这里就是构建成功的app文件


安装成功后,测试成功运行

示例文件

electron-react-example

总结

最后说一下我为什么弃坑umi,不是说umi不好,umi是个很优秀的框架,是dva框架作者今年新的目标,主要是umi在打包之后,对file://协议对象解析不好,electron窗口运行又是依赖file://协议的。作者应该也没有精力在这上面。
总之这几天真的是踩了太多的坑。我后面计划从最原始的react全家桶开始搭建,开始慢慢踩坑。这也是我要做dotflow这个应用的原因,为了找到最适合自己的那个点。

参考文章

HyruleTeam wechat
前端手艺工坊