wonderwander

使用create-react-app构建Electron应用

原文链接: medium.freecodecamp.org

转到Christian Sepulveda的简介

Christian Sepulveda浓咖啡狂热爱好者,程序员,Bastille Network 首席技术执行官,前Pivotal实验室主管。 justideas.io 1月11


如何使用create-react-app构建Electron应用程序 无需webpack配置或“ejecting”

我最近使用create-react-app创建了一个Electron应用程序_。_无需使用Webpack,或者“eject”我的应用程序。接下来我将告诉你我是如何做到的

我之前就有过使用create-react-app的想法,因为它隐藏了webpack的配置细节。但是目前我搜不到Electron搭配create-react-app使用的教程,所以我只好自己动手弄明白了。

如果你已经饥渴难耐了,你可以直接看我的代码。这是我的app的GitHub地址

在我们开始之前,我先介绍下Electron和React,以及为什么create-react-app这么吊。

Electron 与 React

React是Facebook出品的JavaScript视图框架。

用于构建用户界面的JavaScript库 - React 用于构建用户界面的JavaScript库. facebook.github.io

Electron是GitHub的框架,能通过JavaScript构建跨平台桌面应用程序。

Electron _Build cross platform desktop apps with JavaScript, HTML, and CSS._electron.atom.io

大多数使用webpack来进行React开发所需的配置。 webpack是一个配置和构建工具,而之前大多数React社区采用了诸如GulpGrunt等方案。

不同的配置的开销有所不同(稍后会有更多介绍),并且有许多样板和应用程序生成器可用,但2016年7月Facebook Incubator发布了一个工具create-react-app _._它隐藏了大部分的配置细节,并让开发者使用简单的命令,比如npm startnpm run build来运行或建立他们的应用。

什么是ejecting,你为什么要避免它?

create-react-app对一个典型的React项目配置做了一些假设。如果这些假设不适合你,可以选择 eject你的应用(npm run eject)。这样它会复制所有的create-react-app封装好的配置到你的项目中,提供可以随意更改的样板配置。

但是这是单程旅途。您无法撤消ejecting并返回。截至这篇文章发布(2017.1.11)create-react-app,已经有49个realease,每一个版本都有所改进。但是对于ejected的应用程序,您将不得不放弃这些改进或自己想办法解决。

ejected的配置超过550行跨越7个文件(截至本文发布)。我完全看不懂这些配置(额,应该是大部分看不懂),我也不想弄懂。

目标

我的目标很简单:

  • 避免ejecting React应用程序

  • 用最简单的手段,使React和Electron一起工作

  • 保留Electron和create-react-app/React的默认配置,假设和约定(这可以很简单地使用其他需要这种环境的工具)。

基本步骤

  1. 运行create-react-app来生成一个基本的React应用程序

  2. 运行npm install --save-dev electron

  3. [electron-quick-start](https://github.com/electron/electron-quick-start)中的main.js复制过来(为了清晰起见,我们将其重命名为electron-starter.js

  4. 修改mainWindow.loadURL的参数(在electron-starter.js)改为localhost:3000(webpack-dev-server)

  5. package.json的main改为electron-starter.js

  6. 添加一个用于启动Electron的run target到 package.json

  7. 分别运行npm run electronnpm start

步骤1和2非常简单。以下是步骤3和4的代码:

const electron = require('electron');
// Module to control application life.
const app = electron.app;
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow;

const path = require('path');
const url = require('url');

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

function createWindow() {
    // Create the browser window.
    mainWindow = new BrowserWindow({width: 800, height: 600});

    // and load the index.html of the app.
    mainWindow.loadURL('http://localhost:3000');

    // Open the DevTools.
    mainWindow.webContents.openDevTools();

    // Emitted when the window is closed.
    mainWindow.on('closed', function () {
        // Dereference the window object, usually you would store windows
        // in an array if your app supports multi windows, this is the time
        // when you should delete the corresponding element.
        mainWindow = null
    })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', function () {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
});

app.on('activate', function () {
    // On OS X it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (mainWindow === null) {
        createWindow()
    }
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

(Gist)

第五步和第六步

{
  "name": "electron-with-create-react-app",
  "version": "0.1.0",
  "private": true,
  "devDependencies": {
    "electron": "^1.4.14",
    "react-scripts": "0.8.5"
  },
  "dependencies": {
    "react": "^15.4.2",
    "react-dom": "^15.4.2"
  },
  "main": "src/electron-starter.js",
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject",
    "electron": "electron ."
  }
}

(Gist)

当你在步骤7中运行npm命令时,你应该看到:

您可以对React代码进行实时更改,并且可以在运行的Electron应用程序中看到结果。

这对于开发模式没问题,但有两个缺点:

  • 生成模式中不会使用webpack-dev-server。它需要使用构建好的React项目的静态文件

  • (笑)要用两个npm命令,真麻烦

在生成模式和开发模式中指定loadURL

在开发模式,我们可以通过一个环境变量指定mainWindow.loadURL(在electron-starter.js中)。如果这个变量被设置了,我们将使用它;否则我们将使用生产静态HTML文件。

我们将添加一个npm run target(到package.json),如下所示:

"electron-dev": "ELECTRON_START_URL=http://localhost:3000 electron ."

更新:Windows用户将需要执行以下操作: (thanks to @bfarmilo)

"electron-dev": "set ELECTRON_START_URL=http://localhost:3000 && electron ."

下面是我们将要修改的mainWindow.loadURL(在electron-starter.js中):

const startUrl = process.env.ELECTRON_START_URL || url.format({
            pathname: path.join(__dirname, '/../build/index.html'),
            protocol: 'file:',
            slashes: true
        });
    mainWindow.loadURL(startUrl);

(Gist)

有一个问题:create-react-app(默认)构建一个使用绝对路径的index.html。在Electron中加载时会失败。谢天谢地,有一个配置选项可以解决这个问题:在package.json中设置一个homepage属性。 (有关该属性的Facebook文档是here.)

所以我们可以把这个属性设置为当前目录,npm run build将把它用作相对路径。

"homepage": "./",

用Foreman管理React和Electron进程

为了方便起见,我不想

  1. 同时启动/管理React devserver和Electron进程(我只想处理一个)

  2. 等待React devserver启动,然后启动Electron

Foremen是一个很好的进程管理工具。我们可以利用它,

npm install --save-dev foreman

并添加以下Procfile

react: npm start
electron: npm run electron

(Gist)

现在第一步搞定了。对于第二步,我们可以添加一个简单的节点node脚本(electron-wait-react.js)它可以在React devserver启动后启动Electron。

const net = require('net');
const port = process.env.PORT ? (process.env.PORT - 100) : 3000;

process.env.ELECTRON_START_URL = `http://localhost:${port}`;

const client = new net.Socket();

let startedElectron = false;
const tryConnection = () => client.connect({port: port}, () => {
        client.end();
        if(!startedElectron) {
            console.log('starting electron');
            startedElectron = true;
            const exec = require('child_process').exec;
            exec('npm run electron');
        }
    }
);

tryConnection();

client.on('error', (error) => {
    setTimeout(tryConnection, 1000);
});

(Gist)

注意:Foreman会将端口号偏移100。 (See here.) ,因此,electron-wait-react.js减去100,以正确设置 React devserver的端口号。

现在修改Procfile

react: npm start
electron: node src/electron-wait-react

(Gist)

最后,我们将改变package.json中的run targets来替换electron-dev

"dev" : "nf start"

而现在,我们可以执行:

npm run dev

更新(1/25/17):我已经添加了以下部分以回应用户的一些评论(here and here). 他们需要从应用程序内部访问 Electron ,而简单的require 或 import则会引发错误。下面是我发现的一个解决方案

从React App访问Electron

一个Electron应用程序有两个主要的进程: Electron host/wrapper和你的应用程序。在某些情况下,您希望从您的应用程序中访问Electron。例如,您可能想访问本地文件系统或使用Electron的ipcRenderer。但是,如果你这样做,你会得到一个错误

const electron = require('electron')
//or
import electron from 'electron';

在各种GitHub和Stack Overflow问题中,有一些关于这个错误的讨论,例如one。大多数解决方案都会提出webpack配置更改,但这需要eject应用程序。

但是,有一个简单的解决方法/破解。

const electron = window.require('electron');

const fs = electron.remote.require('fs');
const ipcRenderer  = electron.ipcRenderer;

总结

为了方便起见,下面是一个GitHub 仓库,其中包含上述所有更改,每个步骤都有标签。但是,在那里使用create-react-app引导一个Electron应用程序没有太多的工作。 (这篇文章比代码长得多,你需要将这两篇文章进行整合。)

如果你正在使用create-react-app,你可能想看看我的文章,在WebStorm中调试测试和create-react-app.

谢谢阅读。您可以在justideas.io查看更多我的帖子。

更新(2/2/17)。一位读者Carl Vitullo建议使用npm start而不是npm run dev,并在GitHub上提交一个包含更改的pull请求。这些调整在这branch

Show your support

Clapping shows how much you appreciated Christian Sepulveda’s story.

64422* BlockedUnblockFollowFollowing

转到Christian Sepulveda的简介

Christian Sepulveda

espresso fanatic, coder (still), VP Engineering Bastille, former Pivotal Labs exec. justideas.io