Site menu Electron: aplicações desktop em Javascript, HTML e CSS
e-mail icon
Site menu

Electron: aplicações desktop em Javascript, HTML e CSS

e-mail icon

Todas os plataformas desktop já tentaram oferecer HTML5 como segunda opção para desenvolver apps nativos. As plataformas móveis também, com os finados Firefox OS e WebOS tendo mesmo apostado em HTML5 como primeira opção.

Eu não sou o maior fã de usar HTML5 como tecnologia de UI, e muita gente deve concordar comigo, já que o namoro das platformas com HTML5 sempre acaba esfriando. Mesmo no contexto de aplicações portáteis, a primeira escolha é o framework Qt, de preferência acoplado ao PyQt para desenvolver em Python (e não precisar encarar C++).

Mas existem casos de uso onde cabe usar HTML5, principalmente no contexto de aplicações com visual mais simples e/ou que possam/devam ter a mesma aparência onde quer que rodem. Ou quando boa parte da aplicação já tenha sido implementada em HTML5 e um simples "envelopamento" nativo proporcione uma economia enorme de tempo e esforço. Ou se o designer disponível é especializado em HTML e CSS.

O framework Electron, base do famoso editor de texto Atom e desenvolvido por ninguém menos que o GitHub, possibilita o desenvolvimento portável usando tecnologias HTML5. Tem atraído muita atenção, é muito fácil achar informações na Internet, há inúmeros módulos de terceiros à disposição (bem típico, em se tratando de tecnologia Javascript), e o Electron parece ser mantido com muito capricho.

O Electron é, essencialmente, um acoplamento do Node.js com o Chromium (a parte open-source do browser Google Chrome). O programa Javascript rodando sob o Electron tem acesso simultâneo às facilidades do Node e do Chrome — uma combinação bastante poderosa.

Um problema comum de apps HTML5 é a obrigação de usar APIs "puramente HTML5". Mas um app Electron não tem essa obrigação. Ele pode até fazer uso de APIs HTML5; ou então alguma API do Node.js, se for mais conveniente. Considerando ainda que o Node.js tem módulos para absolutamente tudo, que funcionam em qualquer plataforma desktop, o framework fica muito bem posicionado para servir ao desenvolvimento de aplicativos de qualquer tipo.

Conceitos básicos

Feliz ou infelizmente, tudo no Electron gira em torno do Node.js e da ferramenta npm. Portanto, a primeira providência para brincar com Electron é instalar o Node.js.

Para acompanhar esta seção, é interessante instalar a aplicação Electron de exemplo. É praticamente uma "casca vazia" sobre a qual você pode começar a trabalhar.

Ao ser executado, um aplicativo Electron mínimo é composto de pelo menos dois processos: o processo principal e um processo 'renderer'. Como são processos separados, eles executam scripts Javascript diferentes, em contextos diferentes (um não interfere no outro).

O processo principal é onde tudo começa. No aplicativo de exemplo, o script main.js roda no processo principal. O ambiente é muito semelhante ao Node.js "puro". Quando este processo cria um objeto BrowserWindow, aparece uma janela de UI. (A rigor um app Electron não precisa ter UI mas aí faria mais sentido rodá-lo diretamente no Node.js.)

Cada janela de UI é um browser, e para cada janela é criado um processo 'renderer'. O conteúdo da UI é construído com HTML5: abre-se uma página HTML com CSS e eventual código Javascript. O script renderer.js do exemplo roda no contexto do processo 'renderer'. No exemplo, ele não faz nada, mas além das APIs HTML5 ele também tem acesso às APIs do Node.js (e portanto às APIs do Electron).

Como são processos separados, é preciso usar uma API de IPC para trocar mensagens entre o processo principal e o(s) processo(s) 'renderer', se necessário.

Como é de se esperar, algumas tarefas só podem ser levadas a cabo pelo script do processo principal (exemplo: adicionar um ícone no 'tray' da barra de tarefas). Também haverá tarefas do seu aplicativo que talvez só façam sentido no processo principal.

O interpretador do script main.js não é o Node.js instalado em sua máquina; mas sim um binário do próprio Electron (que incorpora versões estáticas do Node.js e do Chromium). Isso elimina toda uma classe de problemas de dependências, e simplifica muito o empacotamento final da aplicação.

Portando uma aplicação Web para Electron

Para ilustrar melhor este artigo, fiz o porte de uma aplicação Web pequena, porém completa, para Electron. Trata-se da minha calculadora de biorritmo. O fonte completo da versão Electron pode ser encontrado e consultado no GitHub. Também pretendo publicar este app na Mac App Store em breve, para "fechar o círculo".

Figura 1: Aplicativo da Web a ser convertido para app Electron
Figura 2: Visual do app Electron. Note o ícone no tray da barra de tarefas

Além da calculadora em si, o aplicativo emite uma notificação diária, informando o usuário qual é seu "horóscopo" de biorritmo para aquele dia. A notificação deve ser emitida mesmo que o app não possua janela aberta. (É um recurso que obviamente vai para o processo principal, e não para o 'renderer'.)

Confissão: como essa calculadora já tinha sido portada para Chrome OS, a notificação diária já estava desenvolvida e testada. (Poder reaproveitar código é quase tão agradável quanto receber uma restituição do Imposto de Renda, ou levar um tiro e escapar ileso.)

Script do processo principal

Como o fonte do aplicativo está no GitHub, não faz sentido discutir cada linha do código, mas vou mostrar alguns destaques do script main.js que é executado no contexto do processo principal.

app.setPath("userData",
	path.join(app.getPath('home'), '.Biorhytmics'));
if (app.makeSingleInstance(function () {})) {
	app.quit();
}

A primeira linha configura a pasta onde as preferências do aplicativo serão armazenadas. O Electron também grava informações intrínsecas de cada página (como cookies e local storage) nessa pasta, por isso é importante que ela esteja definida.

Em seguida, usamos uma API do Electron para limitar a execução em apenas uma instância. O callback passado como parâmetro é invocado na instância sobrevivente (por exemplo, para focar ou restaurar uma janela quando o usuário tenta executar o app pela segunda vez). No exemplo acima, passamos um callback nulo.

var storage = require("electron-json-storage");

Usamos um módulo de storage para armazenar preferências. Poderíamos ter usado local storage HTML5, porém nesse caso os dados estariam prontamente acessíveis apenas no processo 'renderer', e precisaremos acessar as preferências também aqui no processo principal.

function createWindow () {
	mainWindow = new BrowserWindow({width: 800, height: 620,
		'min-width': 800, 'min-height': 620,
		'accept-first-mouse': true,
		title: "Biorhythmics", icon:'./icon-256.png'});

	mainWindow.on('minimize', function(event) {
		event.preventDefault();
		mainWindow.hide();
	});

	mainWindow.on('close', function (event) {
		if (!app.isQuiting) {
			event.preventDefault();
			mainWindow.hide();
		}
		return false;
	});

	mainWindow.loadURL(`file://${__dirname}/index.html`);

	// mainWindow.webContents.openDevTools()

	mainWindow.on('closed', function () {
		mainWindow = null
	});

Neste trecho de código, criamos a janela de UI, dentro da qual é carregada uma página HTML. Capturamos os eventos de minimizar e fechar janela para que ela seja apenas escondida, e o app continue rodando mesmo sem janelas visíveis. Seu aplicativo talvez vá adotar um funcionamento mais tradicional (fechar janela encerra o app).

O "openDevTools" comentado abre a janela de debug do Chromium, que é igual à do Chrome. É muito útil para depuração do processo renderer.

	tray = new Tray(path.join(__dirname, './icon-16.png'));
	contextMenu = Menu.buildFromTemplate([
		{
			label: 'Show chart',	
			click: function() {
				mainWindow.show();
			}
		},
		{
			label: 'Hide chart',	
			click: function() {
				mainWindow.hide();
			}
		},
		{
			label: 'Quit',
			click: function() {
				app.isQuiting = true;
				app.quit();
			}
		}
	]);

	tray.setToolTip('Biorhythmics');
	tray.setContextMenu(contextMenu);

O código acima cria o ícone no "tray" da barra de tarefas, com um menu. O usuário pode reabrir a janela ou encerrar o app por meio deste menu. Em tese essa API do Electron é compatível com Linux, OS X e Windows, mas a documentação menciona diversas "pegadinhas" de portabilidade. Só testei meu código em OS X e funcionou bem.

	options.title = "Biorhythmics";
	options.text = txt;
	options.onClickFunc = function (event) {
		mainWindow.show();
		event.closeNotification();
	};

	notify.notify(options);

O trecho de código acima cria a notificação (usando o módulo electron-notify) que é exibida diariamente para o usuário. Se o usuário clicar na notificação, a janela principal é exibida.

	storage.get("bio1", function (error, contents) {

Aqui fazemos uso do módulo de storage. As notificações são emitidas a partir do processo principal, mas o usuário configura a data de nascimento na janela (sob controle do processo renderer). O processo principal não tem acesso a cookies ou HTML5 local storage, portanto o uso de um storage "neutro" torna tudo mais fácil.

function scheduleNotifs() {
	notify = require('electron-notify');
	notify.setConfig({
		appIcon: path.join(__dirname, './icon-256.png'),
		displayTime: 86400*1000/2-10,
		maxVisibleNotifications: 2,
		width: 350,
		height: 80,
	});
	setTimeout(determine_bio, 3 * 60000);
	setInterval(determine_bio, 12 * 60 * 60000);
}

Neste ponto carregamos e agendamos as notificações periódicas. O módulo electron-notify só é requerido neste ponto porque, por algum motivo, ele quebra o programa se requerido logo no início.

Existem diversos módulos que servem notificações. Escolhi este porque me pareceu o mais adequado, mas há outros que inclusive exibem notificações mais semelhantes às nativas do OS X. (Para mim não serviram porque o texto da minha notificação pode ser um pouco longo.)

Página e scripts do processo 'renderer'

Se não fazia sentido discutir cada linha do script principal, faz menos sentido ainda discutir o conteúdo do processo renderer, que é essencialmente uma página HTML5. O código da versão Web do biorritmo foi adotado aqui praticamente sem alteração nenhuma, e estou presumindo que você conhece o básico de HTML5. Apenas alguns destaques, de coisas que tiveram de ser alteradas:

window.$ = window.jQuery = require('./jquery');

Como o contexto do renderer também é Node.js, não se pode simplesmente adicionar o jQuery como um script na página HTML. Ele precisa ser carregado à moda do Node, conforme a linha acima.

Curiosamente, faço uso de um plug-in do jQuery, o Flot, para desenhar os gráficos. Este não precisou de nenhuma alteração, ele continua sendo adicionado via página HTML como antes.

var storage = require("electron-json-storage");

Em vez de usar cookies ou HTML5 local storage, usamos um módulo para armazenar a data de nascimento do usuário, porque assim fica mais fácil acessar esta informação a partir do processo principal.

<title>Biorhythmics</title>

O título da página HTML torna-se o título da janela, então é importante que seja especificado.

Empacotando a aplicação

A priori, é extremamente fácil empacotar a aplicação Electron para distribuição. Basta instalar o utilitário electron-packager e executar

electron-packager [pasta do fonte do app] [nome do app] 

O pacote será gerado numa subpasta do código-fonte. Obviamente, você pode passar flags para que o pacote seja largado em outro canto.

O ícone padrão do pacote é o do Electron, mas você também pode passar um flag especificando o ícone. (O detalhe é que cada plataforma-alvo exige um tipo diferente de ícone. No OS X, é o .icns, do qual eu nunca tinha ouvido falar!)

A coisa fica um pouquinho mais complicada conforme seu app seja mais complicado. Por exemplo, se seu app tiver um binário ou biblioteca C++, pode precisar incluir DLLs para que o app rode corretamente no Windows 8 ou 10 (cada um pede DLLs diferentes). O electron-packager não gera o DMG no OS X, nem faz a necessária assinatura de aplicativos OS X para publicação na App Store (para isso é preciso usar outros utilitários, prontamente disponíveis mas independentes do electron-packager).

A priori, o electron-package é cross-platform, ou seja, pode gerar pacotes para todas as plataformas, basta passar o flag --all. Há algumas limitações nisso, por exemplo só é possível assinar uma app OS X no próprio OS X. É preciso ter Wine instalado para criar pacotes Windows num host não-Windows. No fim, a melhor aposta é gerar cada pacote em sua plataforma nativa.

Por menor que seja o app, o tamanho final do pacote será um pouco grande (em torno de 110MB) porque como dito antes o Electron embute versões estáticas do Node.js e do Chromium, justamente para não ter dependências. Bem-vindo à era dos frameworks e do inevitável bloat.

Conclusão

O Electron é um framework HTML5 que "faz sentido". Bem documentado, fácil de trabalhar e com ferramentas que resolvem os problemas mais chatos do desenvolvimento desktop, verbi gratia: portabilidade, resolução de dependências e geração de pacotes. Uma UI baseada em HTML5 não é adequada para todo e qualquer aplicativo de desktop, mas para os casos em que HTML5 é minimamente adequado, o Electron serve magnificamente.

e-mail icon