美文网首页React.js专题站ReactElectron
Electron + React + Node.js + ES6

Electron + React + Node.js + ES6

作者: 牧秦丶 | 来源:发表于2016-06-23 12:34 被阅读7216次

    @(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 依赖下面。

    这样,我们就知道了,将 babelwatchify 等等生成代码的依赖库,需要安装在 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"
        ]
    }
    
    

    通过 presetsplugins 两个子项,我们告知 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 下面配置了三个命令:startwatchpackage,分别用于启动 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 文件中,指定了 mainindex.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 事件中,创建了主窗口,并通过 BrowserWindowloadURL 方法加载了本地目录下的 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 为 contentdiv,这个 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 watch

    npm run watch

    接下来调用 start 命令就可以启动 App 了:

    npm run start

    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 用来下载 & 上传

    相关文章

      网友评论

      • gzgogo:整理了一下,源码见这里 http://www.jianshu.com/p/f8afff1e18ba
        0eee9d932674:请问打包是一定要安装wine吗?我总是出错
        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.
      • 平凡数:请问有源码吗
      • 丰琪Mars:请问有什么办法优雅的解决
        import 'xxx.css'
        我看到你是直接hardcode 写死的css(其实是js)
        牧秦丶:@Sorumi CSS-modules 不错,可以用
        df0a8a9a6deb:可以试试 css-modules?
      • 雨笋情缘:作者可以把代码放在GitHub上,让我们这帮菜菜看看
      • 木头lbj:很棒,感谢!!
      • FinalFantasyXX:不错😊,受教了
      • 0dd2b57ef02b:牛逼呀
      • LLVKS:不错,受教了

      本文标题:Electron + React + Node.js + ES6

      本文链接:https://www.haomeiwen.com/subject/qunndttx.html