21
Uma aplicação desktop usando React e Express com Electron
Já faz algumas semanas que comecei uma imersão em React (e React Native) que tem trazido frutos muito interessantes. Dentre eles a solução para um cliente cuja restrição de infraestrutura é exposta no diálogo a seguir:
_ A gente precisa de uma aplicação para a nossa equipe de vendas.
_ Bacana, aonde vocês querem hospedar a aplicação?
_ Nós temos um servidor de banco de dados, e apenas isto.
_ Legal, podemos implantar a aplicação neste servidor?
_ Não, nós só temos acesso ao servidor de banco de dados.
_ Pode ser então uma aplicação desktop?
_ Pode sim, mas no futuro a gente queria que ela servisse de API também.
E aí começa a aventura. O cliente realmente só tinha este servidor de banco de dados e nesta máquina apenas este SGBD poderia ser acessado. Isto me fez lembrar da época em que programava em Delphi/VB, na qual as aplicações eram essencialmente compostas por apenas por dois componentes: o servidor de banco de dados e a aplicação. O nome deste padrão arquitetural é "two-tier architecture".
Mas resolvi inovar: criei meu próprio padrão arquitetural e vou chamá-lo de "three-tier on disguise architecture" por razões que vocês vão entender mais a frente.
A solução
Então tenho uma infraestrutura que deseja ser "two-tier" inicialmente (banco de dados e cliente apenas) mas que futuramente será "three-tier". Para os mais novos, o que é uma arquitetura "three tier": é aquela composta por três módulos:
- O banco de dados.
- Uma camada intermediária de negócios que interage diretamente com o banco de dados e os clientes (uma API).
- O cliente, que normalmente era desktop até o início dos anos 2000, mas que também pode ser pensado como um cliente web.
Em um primeiro momento, no entanto, na visão macro para o cliente, haverá apenas dois módulos na solução proposta:
- O cliente desktop que acessa o banco de dados diretamente.
- O banco de dados que é compartilhado por sua equipe.
("Cliente desktop que acessa o banco de dados diretamente" - estranho para você que é novato (menos de 10 anos de experiência), mas muito comum para o pessoal que desenvolve aplicações desktop neste modelo de dois níveis. É o que chamam de "cliente rico" (rich client se quiser falar "bonito") muitas vezes.)
Muitas vezes o cliente desktop no padrão two-tier executa na prática stored procedures presentes no próprio SGBD (Sistema Gerenciador de Banco de Dados), fazendo com que assim a lógica de negócios fique centralizada, o que é uma boa ideia.
Na maioria das vezes, entretanto, os fornecedores optam por incluir todas as regras de negócio no cliente visando com isto maior portabilidade entre diferentes tipos de SGBD. Pro nosso exemplo vamos imaginar que toda a regra de negócio fica no cliente, ok?
No futuro o cliente gostaria de ter uma API, por que irá existir um servidor dedicado pra tal. Aí sim teríamos um modelo mais convencional (de acordo com os padrões de 2020) e poderíamos passar a um modelo web que do ponto de vista de implantação é muito mais interessante.
O problema é que se implementamos um cliente rico teríamos de futuramente reescrever todas estas regras de negócio na API se usarmos uma abordagem convencional, o que não é o caso desta solução.
A solução tá no cliente Desktop
Nosso cliente desktop usa três tecnologias, duas das quais você terá absoluta liberdade pra trocar, desde que sejam baseadas em Node (uma variante do "seu carro pode ter qualquer cor desde que seja preto" (Ford)).
- Uma API REST implementada usando Express.js (mas você pode usar qualquer outro framework baseado em Node aqui).
- O front-end implementado em React.js (mas você pode usar qualquer outra coisa baseada em JavaScript, HTML e CSS).
- Electron pra encapsular os dois elementos em uma aplicação desktop.
Precisamos falar sobre Electron
O Electron foi criado pela equipe do Github para a escrita do editor Atom e depois foi a base usada para criar o Visual Studio Code. Ele nos permite criar aplicações desktop baseadas em JavaScript. Mais especificamente, aplicações que sejam baseadas em Node, que é também a base do Electron.
Com isto comecei a pensar. Aplicações web baseadas em Node... Ok, eu já vi aplicações renderizadas do lado servidor... aplicações renderizadas do lado servidor... Angular, Vue e React podem ser renderizados do lado servidor... Hmm... eu poderia colocar o acesso ao banco de dados ali, hein, no próprio componente... Ou então eu poderia fazer algo melhor... por que não iniciar um segundo projeto dentro do próprio Electron que carregasse a API e esta API fosse usada pela minha interface web embarcada no Electron?....
Será que consigo embarcar meu front-end escrito em React e também uma API escrita em Express em um projeto Electron? A resposta é SIM, e isto é o que vou lhe ensinar neste post.
Voltando ao cliente e apresentando a solução
Nosso cliente desktop é portanto o Electron encapsulando dois componentes:
- O front-end escrito em React (ou qualquer outra tecnologia).
- A API escrita em Express.js (ou seu outro framework Node.js de preferência).
Ao final nosso projeto será essencialmente o que ilustro no diagrama a seguir:
O projeto Electron irá encapsular nossa aplicação Express.js que, por sua vez, irá servir como conteúdo estático o front-end escrito em React, já empacotado para o ambiente de produção.
No momento em que a aplicação Electron é carregada, inicia-se automaticamente o processo Node responsável por iniciar o Express.js que, por sua vez, irá servir o nosso front-end para os usuários finais. Esta solução trás os seguintes benefícios:
- Já prepara o projeto para o futuro: quando a versão desktop não for mais necessária, basta implantar tanto o módulo API, escrito em React.js quanto o módulo front-end escrito em React para um novo ambiente de produção.
- Não teremos aqui um cliente rico, mas sim um bastante anêmico: todo o acesso ao banco de dados será feito pela API. Lembra quando disse que este padrão arquitetural poderia ser chamado de "three-tier disguised architecture"? Pois é: o projeto já nasce como sendo algo implementado em três camadas: a diferença é que do ponto de vista do usuário parece ter apenas duas. ;)
- Nos permite ter todo o processo de desenvolvimento tal como fazemos em projetos web: a única diferença será o passo final que consistirá no empacotamento do projeto como uma aplicação desktop.
Nossa prova de conceito - quase pondo a mão na massa
Antes de pormos a mão na massa, vou expor aqui uma prova de conceito bem simples. Ela basicamente acessa uma base de dados MySQL externa que contém os posts, tal como no diagrama a seguir:
Para este exemplo estou usando como base de dados uma lista de posts do /dev/All. A API que implementei apenas lista os últimos posts, que são apresentados ao usuário de uma forma bem tosca em nossa prova de conceito tal como no print a seguir:
Pondo a mão na massa
Criando a aplicação Electron
A parte mais simples consiste na criação desta parte do projeto. Você só precisa ter o Node.js (preferencialmente em sua última versão) instalado em seu computador. Este passo a passo que irei expor aqui é essencialmente o que você encontra no site oficial neste link.
Crie um novo projeto com o nome que quiser usando o comando "npm init -y".
Logo na sequência, crie um arquivo chamado "index.js" na raíz do projeto (voltaremos a ele mais tarde) e, na sequência, instale o electron com o seguinte comando:
npm install --save-dev electron
No arquivo package.json vamos realizar pequenas modificações: atenção para o atributo "scripts" e "main".
{ "name": "devall", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "electron ." }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "electron": "^8.0.1", "electron-builder": "^22.3.2" } }
O script "start" irá instruir o Electron a iniciar o projeto para você já em uma janela. Nâo execute o comando "npm start" ainda, pois você não colocou o conteúdo necessário no index.js, que é o ponto de partida do projeto. Por falar nele, segue seu conteúdo:
const {app, BrowserWindow} = require('electron') function createWindow() { let win = new BrowserWindow( {width:1200, height:600, webPreferences:{nodeIntegration:true}} ) // win.webContents.openDevTools() win.loadURL('http://localhost:3000/') win.focus(); } app.whenReady().then(createWindow)
O script apenas irá criar uma janela para nós. Mas é importante uma breve explicação apesar deste post não ser um aprofundamento no Electron. A função createWindow é que faz boa parte da mágica acontecer aqui. Nela criamos um objeto do tipo BrowserWindow que equivale a uma janela desktop (um navegador embutido pra ser bem franco). Ali definimos sua largura e altura e também se terá integração com o Node. Neste caso tem de ter, por que iremos embarcar uma aplicação Express.js neste projeto.
Quando a aplicação estiver pronta (app.whenReady) esta função é chamada. Notou que ela manda a janela carregar o endereço "http://localhost:3000"? Mais a respeito disto lá na frente.
Preparando o projeto Express.js
Crie o projeto Express com a ferramenta de linha de comando do framework (ou então use o código fonte que você já tenha). No interior do diretório que contém seu projeto crie um diretório qualquer (chamarei de "api" aqui no nosso exemplo) e copie para seu interior o código fonte do seu projeto React.
Você está com 90% do caminho pronto agora. Vamos ao pequeno detalhe: o modo como iremos servir o conteúdo estático da aplicação. Dado que seu projeto Express será executado no contexto do Electron, o modo como conteúdo estático é carregado deve ser levemente modificado.
O modo padrão como o Express configura conteúdo estático é tal como no exemplo a seguir, certo?
app.use(express.static(path.join(__dirname, 'public')));
Mude para que fique tal como no exemplo a seguir:
app.use(express.static(__dirname + '/public'))
Você precisará servir a partir do diretório do Electron é executado. Esta é a grande diferença!
Feito isto seu projeto Express.js está pronto. Implemente seus endpoints nele, conecte-se ao banco de dados, faça o que quiser.
Preparando o projeto React.js
Sabe o que você precisa fazer com seu código fonte aqui? Praticamente nada! Apenas realize todas as suas requisições REST contra o endereço "localhost:3000" (ou qualquer outra porta que tenha configurado no seu projeto API).
E como você faz para implantar o projeto no Electron? Simples demais: apenas dois passos.
- Execute o build do projeto "npm run build".
- Copie o conteúdo da pasta "build" para a pasta "public" do seu projeto Express, já dentro do projeto Electron
Há mais um detalhe: inclua a propriedade "homepage" com o valor "./" no arquivo package.json do seu projeto. Isto garante que o carregamento do front-end será a partir deste caminho relativo pra frente quando for construir a solução.
Empacotando tudo agora
Com os três projetos prontos, tudo o que você precisa fazer é executar o "npm install" dentro do diretório que contém o código fonte da sua API. Faça o mesmo no diretório externo, isto é, o diretório do projeto "Electron".
Execute "npm start". Olha lá seu projeto em execução!
E agora, vamos ao grand finalle: como empacotar o projeto? Você vai precisar gerar um .exe se for Windows ou o equivalente pro Linux e MacOS. Use este projeto aqui: Electron Build.
Ainda não acabou: O detalhe crucial!
Se você seguiu até agora este guia e foi construindo o projeto, talvez no momento em que iniciou a aplicação tenha visto uma janela com uma tela em branco, certo? Isto ocorre por que não realizamos a integração de fato com o Express. É preciso iniciar a aplicação.
Há diversas maneiras de se fazer isto. Se você criou seu projeto com a ferramenta de linha de comando do framework basta executar o arquivo www que fica na pasta bin do seu projeto Express. Então, voltando ao arquivo index.js que criamos no projeto ELectron, seu início deverá ser tal como no exemplo a seguir:
const {app, BrowserWindow} = require('electron') // esta é a linha que faz toda a diferença! const express = require('./api/bin/www'); function createWindow() { console.log(express) let win = new BrowserWindow( {width:1200, height:600, webPreferences:{nodeIntegration:true}} ) // win.webContents.openDevTools() win.loadURL('http://localhost:3000/') win.focus(); } app.whenReady().then(createWindow)
Com isto a aplicação Express será carregada antes da janela ser exposta. E notou outra coisa? Como o front-end feito em React é carregado pelo Express, este ao ser exposto ao usuário obrigatóriamente estará já acessando a API já carregada. Resolvemos dois problemas com um único golpe.
Melhorando o projeto
O guia até aqui te deu o básico pra que você consiga construir uma aplicação desktop usando React e Node.js. Mas você pode melhorar bastante isto: como a aplicação usa Node, é possível acessar arquivos no computador do usuário, portanto criar configurações fica muito mais fácil.
E lendo a documentação oficial do Electron você verá que é possível customizar bastante a aplicação: incluir um ícone personalizado, mudar o título da janela principal, expor/ocultar menus, apresentar o console de depuração e muito mais.
Você pode ter acesso à prova de conceito (observe: é uma prova de conceito, não deve ser usada em produção sob hipótese ALGUMA) que fiz neste repositório do Github.
21