Artigo original: Building an Electron application with create-react-app

(Texto traduzido em português europeu)

Não é necessária nenhuma configuração do webpack ou "ejeção".

Recentemente, criei uma aplicação em Electron ao utilizar o create-react-app. Não precisei de me preocupar com o Webpack, ou sequer "ejetar" a minha aplicação. Vou orientar-te sobre como alcancei isto.

Senti-me atraído pela ideia de utilizar o create-react-app porque este oculta os detalhes da configuração do webpack. A minha procura por guias existentes para a utilização do Electron e do create-react-app em conjunto, no entanto, não gerou frutos. Então, decidi mergulhar de cabeça e descobrir pelos meus próprios meios.

Caso te sintas impaciente, podes começar imediatamente e dar uma vista de olhos ao meu código. Aqui está o repositório do GitHub para a minha aplicação.

Antes de começarmos, deixa-me falar-te um pouco sobre o Electron, sobre o React, e sobre o porquê de create-react-app ser uma ferramenta tão boa.

Electron e React

O React é o framework de visualização em JavaScript do Facebook.

A JavaScript library for building user interfaces - React (site do React – em inglês)

O Electron é a framework do GitHub para criar aplicações de desktop multiplataforma em JavaScript.

Electron (site do Electron – em inglês)

A maioria das pessoas utiliza o webpack para as configurações necessárias no desenvolvimento em React. O Webpack é uma ferramenta de configuração e compilação que a maior parte da comunidade do React adotou em vez de alternativas como Gulp e Grunt.

A configuração varia (detalhes disso mais à frente). Existem muitos boilerplates e geradores de aplicações disponíveis, mas, em julho de 2016, o grupo Facebook Incubator lançou uma ferramenta, o create-react-app. Ele oculta maior parte da configuração e permite ao programador utilizar simples comandos, tais como npm start e npm run build para executar e compilar as suas aplicações.

O que é ejetar, e porque queres evitar isto?

O create-react-app realiza várias suposições sobre uma típica configuração do React. Caso essas suposições não sejam do teu agrado, existe uma opção para ejetar uma aplicação (npm run eject). Ejetar uma aplicação copia toda a configuração encapsulada no create-react-app para o teu projeto, fornecendo uma configuração de boilerplate que podes alterar como desejares.

Isso, porém, é uma viagem de um só sentido. Não podes voltar atrás na ejeção. Existem 49 versões (no momento desta publicação) do create-react-app, cada uma delas apresentando melhorias. No entanto, para uma aplicação ejetada, terias de abdicar dessas melhorias ou arranjar uma forma de aplicá-las.

Uma configuração ejetada tem mais de 550 linhas ao longo de 7 ficheiros (no momento desta publicação). Eu não compreendo tudo (bem, maior parte, na realidade) e também não quero compreender.

Objetivos

Os meus objetivos são simples:

  • evitar ejetar a aplicação do React
  • minimizar a colagem para fazer com que o React e o Electron funcionem em conjunto
  • preservar as predefinições, suposições e convenções feitas pelo Electron e create-react-app/React (isso pode tornar mais fácil a utilização de outras ferramentas que assumem/requerem tais convenções).

Receita Básica

  1. executar create-react-app para gerar uma aplicação básica em React
  2. executar npm install --save-dev electron
  3. adicionar main.js a partir de electron-quick-start (vamos alterar o nome para electron-starter.js, por razões de clareza)
  4. modificar a chamada para mainWindow.loadURL (em electron-starter.js) de modo a utilizar localhost:3000 (webpack-dev-server)
  5. adicionar uma entrada principal em package.json para electron-starter.js
  6. adicionar um alvo de execução para iniciar o Electron em package.json
  7. npm start seguido por npm run electron

Os passos 1 e 2 são bastante diretos. Aqui está o código para os passos 3 e 4:

const electron = require('electron');
// Módulo para controlar a vida a aplicação.
const app = electron.app;
// Módulo para criar uma janela de browser nativa.
const BrowserWindow = electron.BrowserWindow;

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

// Mantém uma referência global do objeto da janela. Caso não o faças, a janela
// fechará automaticamente assim que o objeto JavaScript for recolhido para o lixo.
let mainWindow;

function createWindow() {
    // Criar a janela do browser.
    mainWindow = new BrowserWindow({width: 800, height: 600});

    // e carregar o index.html da aplicação.
    mainWindow.loadURL('http://localhost:3000');

    // Abrir o DevTools.
    mainWindow.webContents.openDevTools();

    // Emitido quando a janela é fechada.
    mainWindow.on('closed', function () {
        // Desreferenciar o objeto da janela, geralmente irás armazenar as janelas
        // em um array caso a tua aplicação suporte várias janelas, esta é a altura
        // em que deves apagar o elemento correspondente.
        mainWindow = null
    })
}

// Este método será chamado quando o Electron tiver terminado
// a inicialização e estiver pronto para criar janelas de browser.
// Algumas APIs podem ser utilizadas apenas após este evento ocorrer.
app.on('ready', createWindow);

// Saír assim que todas as janelas estejam fechadas.
app.on('window-all-closed', function () {
    // No Sistema Operativo X é comum que as aplicações e a sua barra de menu
    // fiquem ativas até que o utilizador saia explicitamente com o Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
});

app.on('activate', function () {
    // No Sistema Operativo X é comum recriar uma janela na aplicação quando o
    // icon do dock é clicado e não existem outras janelas abertas.
    if (mainWindow === null) {
        createWindow()
    }
});

// Podes incluir neste ficheiro o restante código específico do processo principal da tua
// aplicação. Também podes colocá-los em ficheiros diferentes e pedi-los aqui.

(Gist)

Para os passos 5 e 6:

{
  "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)

Assim que executares os comandos do npm no passo 7, observarás o seguinte:

sJ4H86R1flN7pTYswhLYhoXMwBHTa9xe3XaX

Podes fazer alterações diretamente no código do React e deves conseguir vê-las refletidas na aplicação do Electron em execução.

FlDGGzcvBH1uEMNToEmmBz8uDDJDV6GpsLoD

Isto funciona bem para programação, mas tem duas limitações:

  • a produção não utilizará o webpack-dev-server. Ela precisa de utilizar o ficheiro estático da compilação do projeto React
  • (pequeno) transtorno ao executar ambos os comandos do npm

Especificar o loadURL em Produção e Desenvolvimento

Em programação, uma variável de ambiente pode especificar o URL para mainWindow.loadURL (em electron-starter.js). Caso a variável env exista, vamos utilizá-la; caso contrário, vamos utilizar o ficheiro de produção de HTML estático.

Vamos adicionar um alvo de execução do npm (o package.json) da seguinte forma:

"electron-dev": "ELECTRON_START_URL=http://localhost:3000 electron ."
Atualização: utilizadores do Windows vão precisar de fazer o seguinte: (obrigado ao @bfarmilo)
”electron-dev”: "set ELECTRON_START_URL=http://localhost:3000 && electron .”

Em electron-starter.js, vamos modificar a chamada mainWindow.loadURL da seguinte forma:

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

(Gist)

Existe um problema com isso: create-react-app (por defeito) cria um index.html que utiliza caminhos absolutos. Isto vai falhar ao carregá-lo no Electron. Felizmente, existe uma opção de configuração para mudar isso: definir uma propriedade homepage em package.json (a documentação do Facebook sobre esta propriedade está aqui).

Assim, podemos definir essa propriedade para a pasta atual e npm run build vai utilizá-la como um caminho relativo.

"homepage": "./",

Utilizar o Foreman para Gerir Processos do React e Electron

Por conveniência, prefiro não fazer o seguinte:

  1. Iniciar/gerir o servidor de desenvolvimento do React e os processos do Electron em conjunto (prefiro lidar apenas com um deles)
  2. Esperar que o servidor de desenvolvimento do React inicie e depois iniciar o Electron

O Foreman é uma boa ferramenta de gestão de processos. Podemos adicioná-lo assim:

npm install --save-dev foreman

Adicionamos, também, o seguinte Procfile:

react: npm startelectron: npm run electron

(Gist)

Isto lida com (1). Para (2), podemos adicionar um script simples do node (electron-wait-react.js) que espera pelo inicio do servidor de desenvolvimento do React e, depois, inicia o 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)

OBSERVAÇÃO: o Foreman vai deslocar o número da porta em 100 para processos de tipos diferentes (veja aqui). Então, electron-wait-react.js subtrai 100 para definir corretamente o número da porta do servidor de desenvolvimento do React.

Agora, modifica o Procfile:

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

(Gist)

Por fim, alteramos os alvos de execução em package.json para substituir electron-dev por:

"dev" : "nf start"

Então, podemos executar:

npm run dev
ATUALIZAÇÃO (1/25/17) : adicionei a seguinte secção em resposta a alguns comentários de utilizadores (aqui e aqui). Eles precisam de ter acesso ao Electron a partir de dentro da aplicação do React e um simples require ou import vai gerar um erro. Deixo uma solução abaixo.

Aceder ao Electron a partir da aplicação em React

Uma aplicação em Electron tem dois processos principais: O Host/wrapper do Electron e a tua aplicação. Em alguns casos, vais querer ter acesso ao Electron a partir de dentro da tua aplicação. Por exemplo, podes querer aceder ao sistema de ficheiros local ou utilizar o ipcRenderer do Electron. Caso faças o seguinte, vais obter um erro.

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

Existe alguma discussão sobre esse erro no GitHub e em vários artigos do Stack Overflow, tal como essa. A maioria das soluções propõe alterações na configuração do webpack, mas isto exigiria a ejeção da aplicação.

No entanto, existe uma solução simples alternativa

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

Conclusão

Por conveniência, aqui está um repositório do GitHub que tem todas as alterações acima, com etiquetas para cada passo. Porém, não é muito difícil inicializar uma aplicação em Electron que utiliza o create-react-app (esta publicação é bastante mais extensa do que o código e alterações que precisarias para integrar os dois).

Caso estejas a utilizar o create-react-app, é melhor dares uma vista de olhos na minha publicação, Debugging tests in WebStorm and create-react-app (texto em inglês).

Obrigado pela leitura. Podes ver mais publicações minhas em justideas.io

ATUALIZAÇÃO (2/2/17). Um leitor, Carl Vitullo, sugeriu a utilização de npm start em vez de npm run dev e submeteu um pull request com as alterações, no GitHub. Estes ajustes estão disponíveis nesta branch.