@(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
* @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
* @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 watch
接下來調用 start 命令就可以啟動 App 了:
npm 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 | 用來下載 & 上傳 |