@(Technical
)[Electron
|React
|Node.js
|ES6
|Material Design
]
1、概述
近来工作上需要做一款 PC 上的软件,这款软件大体来讲是类似 PPT 的一款课件制作软件。由于我最近几年专注于移动 App 的开发,对 PC 端开发的了解有些滞后。所以我首先需要看看,在 PC 上采用什么框架能够顺利完成我的工作。
我的目标是,在完成这款软件的同时能够顺便学习一下比较流行的技术。在经过前期技术调研后,我明确了实现这款软件所需要的技术条件:
- 不采用 C++ 方面的类库,比如 MFC、Qt、DuiLib 等等;
- 本来想试试 C# 来开发,但 C# 对我来说,需要从头学习,如果学 C# 只为了开发这一款软件,后续再无用武之地,那么对我来说,学习的驱动力不大;
- 之前学习了移动端的开发库
React Native
,所以对React
组件化的开发方式颇有好感,所以想尝试用 React 来开发。
2、技术路线
基于以上几点考虑,我通过搜索了解了 Electron
这个框架,果断采用了下面的技术路线:
Technical | 用途 | 文档官网 |
---|---|---|
Electron | 包装 HTML 页面,为网页提供一个本地运行环境 | http://electron.atom.io/docs/ |
React | 用 React 组件来写页面 | https://facebook.github.io/react/ |
Node.js | 为 Electron 提供运行环境 | https://nodejs.org/en/docs/ |
ES6 | 用 Javascript 最新的 ES6 标准来完成代码 |
http://es6-features.org/#Constants |
Electron
是基于 Node.js 的上层框架,为 HTML 页面提供一个 Native 的壳,并提供本地化的功能——如创建 Native 的 Window、文件选择对话框、MessageBox、Window 菜单、Context 菜单、系统剪切板的访问等等。Electron 支持 Windows/Linux/Mac 系统
,所以我们只需要写一份 Javascript 代码和 HTML 页面,就可以打包到不同的系统上面运行。
React
主要用来以组件化的方式写页面,另外 React 已经有非常非常多的开源组件可以用了,我们只需要通过 npm
引入进来即可使用。
因为 Electron 运行在 Node.js
环境里,所以我们的 App 已经可以访问所有的 Node.js 的模块了——比如文件系统、文件访问等等,可以很方便的实现 Javascript 操作本地文件。另外安装其他 npm 包也不费吹灰之力。
3、环境配置
将上述技术结合起来,需要的环境配置如下:
Library & Technical | 用途 |
---|---|
electron-prebuilt | Electron 基础库 |
electron-reload |
自动检测 本地文件修改,并重新加载页面 |
electron-packager | 将最终的代码打包,提供给用户 |
react | React 基础库 |
react-dom | 将 React 组件渲染到 HTML 页面中(ReactDOM.render ) |
react-tap-event-plugin | 使 material-ui 库支持按钮点击事件(http://www.material-ui.com/#/get-started/installation) |
babel + babelify | 将 ES6 代码转换成低版本的 Javascript 代码 |
babel-preset-es2015 | 转换 ES6 代码 |
babel-preset-react | 转换 React JSX 语法的代码 |
babel-plugin-transform-es2015-spread |
babel 插件,转换 ES6 中的 spread 语法 |
babel-plugin-transform-object-rest-spread |
babel 插件,转换 ES6 中的 Object spread 语法 |
browserify + watchify | 自动检测本地文件修改,结合 babel 重新转换 ES6 代码 |
Material-UI | Google Material-Design 风格的 React UI 组件(http://www.material-ui.com/#/components/app-bar) |
4、各个库交互流程
交互流程5、开始开发
5.1、新建项目
首先我们要安装 Node.js。然后通过下面的命令新建一个项目:
新建项目npm init
5.2、添加依赖
这样,就在我们的项目目录下新建了一个 package.json
文件,然后我们安装其他 npm 依赖。用下面的命令:
npm install --save-dev module_name
npm install --save module_name
--save-dev
和 --save
参数的区别是:--save-dev
参数会把添加的 npm 包添加到 devDependencies
下,devDependencies
依赖只在开发环境下使用,而 --save
会添加到 dependencies
依赖下面。
这样,我们就知道了,将 babel
、watchify
等等生成代码的依赖库,需要安装在 devDependencies
依赖下面,而像 React
等等,需要安装在 dependencies
下面,以供打包发布后还能使用。
我们依次将前面第 3 小节所述的依赖全部通过 npm 安装进来,其中 electron
相关组件安装起来非常慢,有很大几率失败——如果安装失败了,多试几次或者翻墙安装。
npm install --save-dev
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-tap-event-plugin
npm install --save
material-ui
5.3、babel
配置
安装好 babel
后,还需要进行配置。我们通过 babel 官网了解到,需要在项目目录下放置一个 .babelrc
文件,让 babel
知道转换哪些代码。我们的 .babelrc
文件内容如下:
{
"presets": [
"es2015",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
}
通过 presets
和 plugins
两个子项,我们告知 babel 转换 ES6 和 React JSX 风格的代码,另外还需转换 ES6 中的 spread
语法。
5.4、watchify
& electron-packager
& electron
配置
另外,我们需要在 package.json
文件中配置 watchify
,让其可以自动检测本地代码变化,并且自动转换代码。package.json
如下:
// package.json
{
"name": "demoapps",
"version": "1.0.0",
"description": "",
"author": "arnozhang",
"main": "index.js",
"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=all --out=../DemoApps --version=1.2.1 --icon=./public/img/app-icon.icns"
},
"devDependencies": {
"babel": "^6.5.2",
"babel-plugin-transform-es2015-spread": "^6.8.0",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.5.0",
"babelify": "^7.3.0",
"browserify": "^13.0.1",
"electron-packager": "^7.0.3",
"electron-prebuilt": "^1.2.1",
"electron-reload": "^1.0.0",
"watchify": "^3.7.0"
},
"dependencies": {
"material-ui": "^0.15.0",
"react": "^15.1.0",
"react-color": "2.1.0",
"react-dom": "^15.1.0",
"react-tap-event-plugin": "^1.0.0"
}
}
通过 package.json
文件,我们在 scripts
下面配置了三个命令:start
、watch
、package
,分别用于启动 App、检测并转换代码、打包 App。
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=all --out=../DemoApps --version=1.2.1 --icon=./public/img/app-icon.icns
然后我们在命令行下通过 npm run xxx
,可以运行上面定义好的命令。我们看到,通过 babelify
将代码转换输出到 public/js/bundle.js
目录下,所以我们发布时只需要发布这一个转换好的 js 文件即可。
5.5、目录结构一览
在我们正式开发之前,先来看一下整个项目的目录结构:
目录结构一览6、正式开发
6.1、Electron 开发模式
这里就不得不提到 Electron
的开发模式了,Electron
只是为 HTML 页面提供了一个 Native 的壳,业务逻辑还需要通过 HTML + js 代码去实现,Electron
提供两个进程来完成这个任务:一个主进程
,负责创建 Native 窗口,与操作系统进行 Native 的交互;一个渲染进程
,负责渲染 HTML 页面,执行 js 代码。两个进程之间的交互通过 Electron 提供的 IPC API
来完成。
由于我们在 package.json
文件中,指定了 main
为 index.js
,Electron 启动后会首先在主进程
加载执行这个 js 文件——我们需要在这个进程里面创建窗口,调用方法以加载页面(index.html
)。
6.2、index.js
开发
index.js
文件如下:
/*
* Copyright (C) 2016. All Rights Reserved.
*
* @author Arno Zhang
* @email zyfgood12@163.com
* @date 2016/06/22
*/
'use strict';
const electron = require('electron');
const {app, BrowserWindow, Menu, ipcMain, ipcRenderer} = electron;
let isDevelopment = true;
if (isDevelopment) {
require('electron-reload')(__dirname, {
ignored: /node_modules|[\/\\]\./
});
}
var mainWnd = null;
function createMainWnd() {
mainWnd = new BrowserWindow({
width: 800,
height: 600,
icon: 'public/img/app-icon.png'
});
if (isDevelopment) {
mainWnd.webContents.openDevTools();
}
mainWnd.loadURL(`file://${__dirname}/index.html`);
mainWnd.on('closed', () => {
mainWnd = null;
});
}
app.on('ready', createMainWnd);
app.on('window-all-closed', () => {
app.quit();
});
这段代码很简单,在 app ready
事件中,创建了主窗口,并通过 BrowserWindow
的 loadURL
方法加载了本地目录下的 index.html
页面。在 app 的 window-all-closed
事件中,调用 app.quit
方法退出整个 App。
另外我们看到通过引入 electron-reload
模块,让本地文件更新后,自动重新加载页面:
require('electron-reload')(__dirname, {ignored: /node_modules|[\/\\]\./});
6.3、index.html
开发
接下来就是 index.html
页面的开发了,这里也比较简单:
<!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="content">
</div>
<script src="public/js/bundle.js"></script>
</body>
</html>
看的出来,我们定义了一个 id 为 content
的 div
,这个 div 是我们的容器,React 组件将会渲染到这个 div 上面。然后引入了 public/js/bundle.js
这个 Javascript 文件——前面讲过,这个文件是通过 babelify
转换生成的。
6.4、app/appEntry.js
开发
我们知道,public/js/bundle.js
是通过 app/appEntry.js
生成而来,所以,appEntry.js 文件主要负责 HTML 页面渲染——我们通过 React 来实现它。
/*
* Copyright (C) 2016. All Rights Reserved.
*
* @author Arno Zhang
* @email zyfgood12@163.com
* @date 2016/06/22
*/
'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
import injectTapEventPlugin from 'react-tap-event-plugin';
const events = window.require('events');
const path = window.require('path');
const fs = window.require('fs');
const electron = window.require('electron');
const {ipcRenderer, shell} = electron;
const {dialog} = electron.remote;
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
let muiTheme = getMuiTheme({
fontFamily: 'Microsoft YaHei'
});
class MainWindow extends React.Component {
constructor(props) {
super(props);
injectTapEventPlugin();
this.state = {
userName: null,
password: null
};
}
render() {
return (
<MuiThemeProvider muiTheme={muiTheme}>
<div style={styles.root}>
<img style={styles.icon} src='public/img/app-icon.png'/>
<TextField
hintText='请输入用户名'
value={this.state.userName}
onChange={(event) => {this.setState({userName: event.target.value})}}/>
<TextField
hintText='请输入密码'
type='password'
value={this.state.password}
onChange={(event) => {this.setState({password: event.target.value})}}/>
<div style={styles.buttons_container}>
<RaisedButton
label="登录" primary={true}
onClick={this._handleLogin.bind(this)}/>
<RaisedButton
label="注册" primary={false} style={{marginLeft: 60}}
onClick={this._handleRegistry.bind(this)}/>
</div>
</div>
</MuiThemeProvider>
);
}
_handleLogin() {
let options = {
type: 'info',
buttons: ['确定'],
title: '登录',
message: this.state.userName,
defaultId: 0,
cancelId: 0
};
dialog.showMessageBox(options, (response) => {
if (response == 0) {
console.log('OK pressed!');
}
});
}
_handleRegistry() {
}
}
const styles = {
root: {
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
display: 'flex',
flex: 1,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
},
icon: {
width: 100,
height: 100,
marginBottom: 40
},
buttons_container: {
paddingTop: 30,
width: '100%',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
}
};
let mainWndComponent = ReactDOM.render(
<MainWindow />,
document.getElementById('content'));
现在我们已经到了渲染进程了,这里引入 electron 必须使用 window.require
来引入,否则会出错。这里刨除 material-ui
的部分,主要的代码是:
let mainWndComponent = ReactDOM.render(
<MainWindow />,
document.getElementById('content'));
我们通过 ReactDOM.render
方法将一个 React 组件渲染到了一个 div 上面。
7、运行起来
现在我们已经可以运行这个程序了,首先我们启动 Watchify,主要是让其监控本地文件修改,实时转换生成 public/js/bundle.js
文件:
npm run watchnpm run watch
接下来调用 start 命令就可以启动 App 了:
npm run startnpm run start
运行之后,我们马上看到我们的窗口了:
运行结果 弹出对话框8、打包
打包命令非常简单,但比较耗时:
npm run package
执行完之后可以在对应的目录看到打包好的文件夹了,将这个文件夹压缩,可以提供给用户运行。为了防止代码泄露,你还可以通过 asar
这个 npm 模块,将你的代码打包为 asar
文件。
9、后续
这里只是一个介绍,你可以根据你自己的情况去开发对应的 App。我在开发过程中发现了一些好用的库,这里顺便记录一下:
library | 用途 |
---|---|
font-detective | 检测系统中安装的字体列表 |
extend | 拷贝一个对象 |
draft-js | facebook 出品的 React 富文本编辑器,高度可定制化 |
draft-js-export-html | 将 draft-js 的数据转换成 HTML 代码,定制化不高,有高定制化需求时,需要改代码 |
material-design-icons | 一系列 Material 风格的 Icon,可以结合 material-ui 中的 FontIcon 使用 |
md5-file | 计算文件 MD5,提供同步和异步两种计算方式 |
node-native-zip | 文件的压缩和解压缩,非常好用 |
xmlbuilder | XML 生成器,简单好用!极其推荐
|
react-color | React 组件化的颜色选择器,支持各种方式的选择!极其推荐
|
react-resizeable-and-movable | React 组件化的拖动 & 改变大小的模块 |
qrcode.react | React 组件化的二维码生成器,极其推荐
|
request | 用来下载 & 上传 |
网友评论
Could not find "wine" on your system.
Wine is required to use the appCopyright, appVersion, buildVersion, icon, and
win32metadata parameters for Windows targets.
Make sure that the "wine" executable is in your PATH.
import 'xxx.css'
我看到你是直接hardcode 写死的css(其实是js)