react+electron+ant-design+sqlite3实现一个桌面应用

0x00 写在前面

  最近帮隔壁实验室写个简单的展示页面,想着自己学了一下nodejs与react,刚好可以用来练练手。于是使用sqlite3数据库,用nodejs来写后端,react写前端的前后端分离方法,完成了一个小demo。这时候又要求用electron改成桌面版的应用,这简单啊,把react打包出来的index.html放到electron下做入口文件不就OK了。但是又涉及到跟对方的Java环境下的jar包通信的问题,对方的思路是通过前端点击按钮在数据库里存入一个字段,jar包所在环境通过不断轮询数据库去获取该字段确定是否启动手机应用拖数据库。前端在点击按钮后不断轮询数据库,看需要的数据是否更新,有数据更新就取回数据,并在前端展示。对于这个方案,我是拒绝的。回来跟师兄讨论了一下,确定了下几种方案。

  • nodejs写后端,react写前端。前端请求后端,由nodejs命令行启动jar。打包react页面放到electron下作为入口文件。
  • nodejs写后端,react写前端。把react页面打包近electron下作为入口文件,由于electron是基于nodejs的,所以在electron里使用socket实现桌面应用与jar包通信。
  • Java写后端,react写前端。把react写的页面打包放进electron下作为入口文件,放入Java环境下。
  • 整合electron与react框架。使用ipcMain与ipcRenderer实现进程间的通信,ipcMain(主进程)用来获取数据,启动jar获取返回,再将数据返回到ipcRenderer(渲染进程)进行渲染。

  前三种方式都是需要启动服务器端的,对方采用sqlite3做数据库,就是希望在不启动服务器的情况下打开应用,启动jar包,展示数据。因此,第四种方式是
最合适的。但是在整合electron与react框架时,出现了很多问题。其中最关键的问题就是整合后react中不能直接使用require引入ipcRenderer。目前网络上查到的解决方案,都是使用window.require引入,同时使用预加载的方式。但是这种方式只适合在不使用进程间通信的情况下使用。下面给出一种我尝试成功的方法,可以使用进程间通信。虽然在开发过程中也出现了很多问题,但是所幸都一一解决了。

0x01 技术路线

技术路线 用途
Electron 构建桌面应用
react 前端页面渲染,展示
ant design React的UI库
sqlite3 一个轻量级的数据库

0x02 环境安装及配置

2.1 安装Nodejs环境

  首先确认已经安装了Nodejs环境,如果没有安装请自行在搜索相关文章进行安装,这里不做详细讲解。

2.2 新建项目,打开cmd

1
2
3
mkdir my-react
cd my-react
npm init//默认配置就好

2.3 安装依赖

1
2
3
4
npm install –save-dev electron electron-prebuilt electron-reload electron-packager
npm install –save-dev babel babelify babel-preset-es2015 babel-preset-react babel-plugin-transform-es2015-spread
npm install –save-dev browserify watchify
npm install –-save react react-dom react-router-dom

  推荐ant design按需加载,因此这里还需要安装babel-plugin-import。

1
2
npm install antd --save
npm install –save-dev babel-plugin-import

  具体配置和使用请参考ant design官网.

2.4 配置.babel

  在根目录下新建一个.babelrc文件,内容如下:

1
2
3
4
5
6
7
8
9
{
"presets": [
"es2015",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
}

  这两项用来告知babel转换ES6和React JSX风格的代码,另外还需转换ES6中的spread语法。

2.5 代码转换

  package.json 文件中配置 watchify,让其可以自动检测本地代码变化,并且自动转换代码。
  scripts 下面配置了三个命令:start、watch、package,分别用于启动 App、检测并转换代码、打包 App。

1
2
3
4
5
"scripts": {
"start": "electron .",
"watch": "watchify app/appEntry.js -t babelify -o public/js/bundle.js --debug --verbose",
"package": "electron-packager ./ DemoApps --overwrite --app-version=1.0.0 --platform=win32 --arch=x64 --out=../DemoApps --electron-version=1.4.13 --app-version=1.2.1 --icon=./public/img/icon/icon.ico"
},

  通过在命令行下执行 npm run xxx ,可以运行上面定义好的命令。我们看到,通过 babelify 将代码转换输出到 public/js/bundle.js 目录下,所以我们发布时只需要这一个转换好的 js 文件即可。

0x03 Electron

  package.json文件中有一个”main”字段,这指明了主进程的入口文件。也就是说,启动Electron后会首先在主进程中加载执行这个js文件。所以我们要在这里创建窗口,并在这里指定页面加载的入口文件(index.html)。

3.1 index.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
'use strict';
const electron = require('electron');
const {app, BrowserWindow, Menu, ipcMain, ipcRenderer} = electron;
var mainWnd = null;
function createMainWnd() {//创建主进程的窗口
mainWnd = new BrowserWindow({//长宽可以自定义设置
width: ,
height: ,
icon: 'public/img/app-icon.png'
});
if (isDevelopment) {
mainWnd.webContents.openDevTools();
}
mainWnd.loadURL(`file://${__dirname}/index.html`);//加载index.html,打开electron将会显示index.html的内容
mainWnd.on('closed', () => {
mainWnd = null;
});
}
app.on('ready', createMainWnd);
app.on('window-all-closed', () => {
app.quit();
});

3.2 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Electron Demo Apps</title>
<link rel="stylesheet" type="text/css" href="public/css/main.css">
</head>
<body>
<div id="root">
</div>
<!--引入转换后的js-->
<script src="public/js/bundle.js"></script>
</body>
</html>

  这里的id为root的div是一个容器,React组件将会渲染到这个div上面;引入的bundle.js是通过babelify转换生成的js文件。

3.3 app/appEntry.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
65
66
67
68
69
'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
import {
BrowserRouter as Router,
Route,
Link,
Switch,
Redirect
} from 'react-router-dom';
import Homepage from './components/Homepage'; // 导入首页组件
import Device from './components/Device'; // 导入设备组件
import { Layout, Menu } from 'antd';
const { Header, Content, Footer } = Layout;
const electron = window.require('electron');
const { ipcRenderer, shell } = electron;
class MainWindow extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<!--这是路由组件-->
<Router>
<Layout className="layout">
{/* <div className="logo" /> */}
<Header>
<div className="logo" />
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={['1']}
style={{ lineHeight: '64px' }}
>
<Menu.Item key="1">
<Link to="/service">服务</Link>
</Menu.Item>
<Menu.Item key="2">
<Link to="/homepage">首页</Link>
</Menu.Item>
</Menu>
</Header>
<Content style={{ padding: '0 50px' }}>
<div style={{ background: '#fff', padding: 24, minHeight: 880 }}>
<Switch>
<Route exact path="/" component={Service}>
</Route>
<Route path="/homepage" component={Homepage} />
<Redirect to="/" />
</Switch>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
Ant Design ©2018 Created by Ant UED
</Footer>
</Layout>
</Router>
);
}
}
let mainWndComponent = ReactDOM.render(
<MainWindow />,
document.querySelector('#root'));

  需要注意的是,这里的ant design要按需引入;同时为了实现路由,要引入react-router-dom的组件;引入nodejs中文件处理的一些模块;引入electron的渲染进程,用于和主进程进行通信。并通过ReactDOM.render 方法将一个 React 组件渲染到了一个 div 上面。

0x04 sqlite3安装

  Electron是基于nodejs的,sqlite3又是一个轻量级数据库。所以这里我们通过在Electron里安装sqlite3的依赖,并引入sqlite3模块来使用它。

  但是在安装过程中会出现很多问题,其中比较多的就是在Electron中对sqlite3的编译问题。下面给出一个测试正确的方法。

  首先是安装环境。先安装python2.7环境,只能是2.7的环境,sqlite3在大于2.7的环境中无法正确编译;其次安装vs2015,只能是2015版本,2017不行。安装完之后一定要安装vs2015所需的C++开发环境,这点很重要,因为要是没有这个东西,会报错,缺少v140的工具集,之所以没有选择vs2017,是由于vs201没有v140的工具集。这一步的安装工作非常非常非常重要!!!!如果环境安装不正确,使用后面的命令将会报错!!!!

  然后使用Microsoft的windows-build-tools,在确认电脑安装了python2.7版本的情况下(python3.0以上版本不支持),以管理员身份运行PowerShell或CMD.exe。使用下面代码安装所有必需的工具和配置。环境没有安装好这一步将会报错!!!!

1
cnpm install -g windows-build-tools

  然后安装sqlite3及相关依赖

1
2
3
4
5
6
7
8
cnpm install sqlite3 --save
cnpm install node-gyp -g
cnpm install nan --save
cnpm install electron-rebuild --save

  最后进行编译。

1
2
3
4
5
.\node_modules\.bin\electron-rebuild.cmd
cd .\node_modules\sqlite3
node-gyp rebuild --target=1.4.13 --arch=x64 --target_platform=win32 --dist-url=https://atom.io/download/electron/ --module_name=node_sqlite3 --module_path=../lib/binding/electron-v1.4-win32-x64

  上面的target是electron版本,可以通过

1
2
electron -v

命令来查看。
  还有一点需要注意的是,上面有两个地方用到了electron的版号,一个是’–target’中,这里需要完整的版本号;另一个是module_path中有一个electron-v1.4字段,module_path是编译生成的node_sqlite3.node所在的路径,electron-v1.4-win32-x64是node_sqlite3.node上一级文件名,这里只能用v1.4不能用1.4.13,否则会生成名为electron-v1.4.13-win32-x64的文件夹。在启动项目时,项目会默认查找electron-v1.4-win32-x64文件夹中的node_sqlite3.node文件,会因为找不到该文件而报错。如果使用了electron-v1.4.13,一定要到相应的文件夹中把名字改成electron-v1.4-win32-x64。切记切记!!!

0x05 启动

  首先启动Watchify,主要是让其监控本地文件修改,实时转换生成 public/js/bundle.js 文件。如果appEntry.js转换成bundle.js有错误的话,命令行下也会有提示。

1
npm run watch

  接下来就需要调用start来启动App了。

1
npm run start

0x06 数据库操作与页面渲染

  在这个项目中,我们使用到进程间通信来获取数据,并渲染到页面上。主进程用ipcMain,负责从sqlite3数据库获取数据,再通过与渲染进程ipcrenderer通信,将数据返回给渲染进程,渲染到页面上。

6.1 主进程index.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
ipcMain.on('getServiceMsg', (event, arg) => {//主进程监听一个渲染进程,进行数据库操作
let curpage = arg.curpage;
let sql_num = "SELECT * from Info";
let sql_all = "SELECT * from Info limit 1" ;
let num;
let results = [];
db.all(sql_num, function (err, rows) {
num = rows.length;
});
db.all(sql_all, function (err, rows) {
console.log(rows);
rows.map((row) => {
results.push({
"key": row.id,
"xinghao": row.xinghao,
"bianhao": row.bianhao,
"yuyan": row.yuyan,
"changshang": row.changshang
})
})
results.push({
"count": num,
"curpage": curpage
});
// console.log(results);
event.returnValue = results;//将从数据库取得的数据返回给渲染进程
});
})

  主进程需要对这个字段进行监听。

6.2 渲染进程Homepage.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
export default class Hello extends Component{
constructor(props) {
super(props);
this.state = {
dataSource: [],
total: "",
curpage: 1,
Selected: ""
};
}
componentDidMount() {
this.getMessageData();
}
getMessageData() {
let data = {
"type":"Info",
"curpage":this.state.curpage
}
let res = ipcRenderer.sendSync('getServiceMsg', data);//发送给主进程一个字段:'getServiceMsg',执行数据库查询操作后会获取一个返回值。
let count = res.pop();
this.setState({
dataSource: res[0],
total: count.count
}, () => {
console.log(this.state.dataSource);
});
}
onChange(page) {
this.setState({
curpage: page.current,
}, () => {
this.getMessageData();
});
}
render(){}

0x07 开发过程中出现的一些问题

7.1 css样式的问题

  在用react开发的js文件中引用外部的css文件会报错;antd.css样式不能渲染。这应该是因为babel将appEntry.js转换成bundle.js文件,并没有将css文件转化出来。页面是通过加载index.html进行渲染的,html文件中不能找到css文件。因此,在入口html文件中引入外部css文件。可以解决这个问题。

7.2 文件路径问题

  文件夹app下存放的是用react开发的js文件,在这里面引入图片时使用相对路径会出错。首先,Homepage与Device两个页面放在app文件夹下的components下,在app文件夹中,js文件的根路径就是app文件夹;因此,引入的图片只能放在app文件夹或下一级目录下;其次,经过babel的转化引起,在bundle.js文件中找不到合适的路径。因此,在引入图片时,我使用了绝对路径或者在整个项目的根目录下写一个css文件,在类中引入图片。

7.3 路由的问题

  在打开页面时,首页是空白的,只有通过点击导航栏,才会将页面渲染出来。这个问题的解决请参考我前面的博客。

7.4 window.require问题

  在react中不能直接引入ipcRenderer。这里要使用window.require进行引入,但需要注意的是,打包后的index.html文件为空白,会出现,’window.require is not a function’的报错,使用window.require而不是require来避免electron和browserify的require函数之间的冲突。在浏览器测试的时候会报错,这是因为window.require未在浏览器中定义,只在运行Electron应用程序时起作用。

0x08 打包

  关于electron的打包,目前提供了两种打包方式。这里使用的是electron-package。可以参考我的另外一篇博客electron与vue实践初体验
  这里只给出命令

package.json/scripts
1
electron-packager ./ DemoApps --overwrite --app-version=1.0.0 --platform=win32 --arch=x64 --out=../DemoApps --electron-version=1.4.13 --app-version=1.2.1 --icon=./public/img/icon/icon.ico

  需要注意的是,前面00x4中对sqlite3编译时用到了–arch=x64,所以对应的,这里的arch也要用x64,如果用别的可能会报错。

Miss Me wechat
light