Desenvolva um aplicativo de desktop com Electron.js

Desenvolva um aplicativo de desktop com Electron.js

Ultimamente, o desenvolvimento web se consolidou em aplicativos de desktop, resultando em aplicativos que se destacam por seu alcance multiplataforma e interfaces espetaculares.

A cada dia é mais comum ver aplicativos de desktop criados com tecnologias web como:

  • VSCode.
  • Mongo Compass.
  • Discord.
  • Postman.
  • Microsoft Teams.

E uma longa lista que você encontra já publicada na Microsoft Store, na App store (Mac) e no Snapcraft (Linux).


Mas como eles fazem isso?


Especificamente, todos os exemplos mencionados anteriormente usam a mesma tecnologia de desktop: Electron.js.

Neste artigo, iremos nos aprofundar no uso de Electron.js desenvolvendo um quebra-cabeça multiplataforma com Vite e Vue.js.

Diante do exposto, construiremos um aplicativo que replica o comportamento de um quebra-cabeça deslizante, algo parecido com o que você pode encontrar neste pesquisa de Google.

No seguinte link você encontrará o repositório com o projeto completo. Para fins práticos, faremos apenas a seguinte visualização:

É um aplicativo simples, perfeito para este exemplo.


Vamos começar com os conceitos.

O que é Electron.js?


É um framework para o desenvolvimento de aplicações desktop multiplataforma não nativas (Linux, Mac e Windows) que permite utilizar tecnologias web como núcleo para desenvolver seu aplicativo, além de fornecer um kit de ferramentas para acessar diferentes APIs do sistema host e permite que você use frameworks como Vue, React, Svelt, etc.

Electron usa Chromium e Node.js. Simplificando, ele executa seu código dentro de um navegador, mas a mágica realmente acontece em dois processos nos quais todo o seu comportamento se baseia: Main Process e Renderer Process.

Main Process

O Main Process ou Proceso Principal é o ponto de partida de cada aplicação no Electron, que roda em um ambiente Node.js. Portanto podemos acessar todas as APIs disponíveis para este ambiente através de um simples require.

Dentro deste processo, diferentes seções da aplicação são controladas, tais como:

Gerenciamento de janelas

Este processo permite criar diferentes instâncias do módulo BrowserWindow, que inicia uma janela web com um processo isolado no qual você pode renderizar um arquivo HTML.

O módulo BrowserWindow possui diversos eventos que servem para controlar diversas ações na janela, como minimização, maximização e fechamento, entre muitas outras.

Ciclo de vida ou Lifecycle

O ciclo de vida do aplicativo é gerenciado no processo principal. Para ser exato, um aplicativo Electron é baseado em eventos que podem ou não determinar o início e o fim do processo principal. O encerramento do processo principal indica o fechamento do aplicativo como tal.

Renderer Process

Cada vez que um BrowserWindow é instanciado, o Electron inicia um processo de renderização separado. O acima permite que você faça tudo o que um navegador padrão faz.

Uma particularidade desses processos é, principalmente, que eles não rodam em ambiente Node.js, portanto será necessário utilizar um bundler como Webpack ou Vite.js (entre outros) para importar diretamente módulos NPM.

Os processos principal e de renderização não podem se comunicar diretamente. Esta é uma medida de segurança para evitar que o processo de renderização exponha variáveis ​​com conteúdo sensível, como chaves de API, por exemplo.


Este framework tem vantagens e desvantagens. Um de seus pontos fortes é que permite desenvolver uma aplicação desktop com tecnologias web.

Embora forneça diferentes APIs para controlar as funcionalidades do sistema host, também é conhecido por seu alto consumo de recursos, já que abrir literalmente dois aplicativos Electron é como abrir 2 páginas web em 2 navegadores diferentes.

Para acelerar o processo de inicialização, recomendo baixar o seguinte repositório, que contém tudo que você precisa para começar rapidamente a revisar arquivos e implementar modificações.

Ao baixar o repositório, tudo que você precisa fazer é escrever o npm install  e então iniciar o projeto usando o comando npm start. Ao ser executado, o comando anterior será exibido na tela semelhante a esta:

Dentro desses arquivos, os mais importantes que analisaremos detalhadamente serão:

  • src/main.js.
  • src/preload.js.
  • src/renderer.js.
  • vite.renderer.config.mjs.
  • index.html.

Vue.js, vue-router e @vitejs/plugin-vue não estão configurados neste template. Portanto, vamos começar com a configuração mais básica, a do processo de renderização, para permitir a utilização de componentes separados em arquivos com extensão .vue. Esta configuração deve ser feita no arquivo vite.renderer.config.mjs:

import { defineConfig } from 'vite';

import vue from '@vitejs/plugin-vue'


// https://vitejs.dev/config

export default defineConfig({

 plugins: [ vue() ],

 resolve: {

   alias: {

     vue: 'vue/dist/vue.esm-bundler.js',

   },

 },

});


Antes de continuar com os arquivos, devemos criar nosso componente principal em um novo diretório chamado src/components e, dentro dele, um novo arquivo que chamaremos de Game.vue e no qual adicionaremos o seguinte conteúdo como placeholder:

<script setup>

console.log('game view')

</script>


<template>

 <section class="controls">

 </section>

 <section class="hero center">

   Game

 </section>

 <section class="content">

 </section>

</template>



Agora adicionaremos a configuração do roteador. Para isso, precisamos gerar um novo arquivo chamado src/routes/index.js, que virá com o seguinte conteúdo:

import { createRouter, createWebHashHistory } from "vue-router";

import Game from "../components/Game.vue"


const routes = [

{ path: '/', component: Game },

]


export const router = createRouter({

 history: createWebHashHistory(),

 routes,

})


Se você não conhece o vue-router não há muitos problemas: é fácil entender como ele funciona. Na verdade, todas as rotas estão dentro do array de routes, você só precisa especificar o path e o componente para o qual elas apontam.

É hora de continuar com os arquivos aos quais precisamos prestar atenção especial:

Index.html

Este arquivo é o alvo de renderização do Main process (src/main.js). Aqui podemos alterar o texto do frame da app com mudar o tag <title>. Também podemos indicar o elemento que será o ponto de montagem do nosso aplicativo vue.js e é isso que faremos (além de adicionar a montagem do vue router com a tag <router-view>):

<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

   <title>PZZL</title>

 </head>

 <script type="module" src="/src/renderer.js"></script>

 <body id="app">

   <router-view></router-view>

 </body>

</html>


Com isso, concluímos o arquivo index.html.

src/renderer.js (Renderer Process)

Este arquivo cuida da lógica do processo de renderização. Embora não seja exatamente importante, é fundamental que nosso index.html faça referência a ele.

Aqui iremos configurar nosso aplicativo vue, incluindo o roteador que criamos anteriormente:

import './styles/index.css'

import { createApp } from 'vue';

import { router } from './routes';

const app = createApp({}) // instancia de vue

// agrega el plugin del router creado en src/routes/index.js

app.use(router)

// monta la app de vue usando de raíz el elemento del dom con el id #app

app.mount('#app')


Com isso, o Vue.js foi configurado e pronto para uso em nosso aplicativo Electron. Se executarmos npm start, veremos algo como o seguinte:


Agora vamos ver como o ciclo de vida do nosso aplicativo é alcançado. A próxima etapa é verificar o arquivo src/main.js.

src/Main.js (Main Process)

Este é o arquivo diretamente relacionado ao Main Process: aqui você pode gerenciar janelas e modificar o que acontece durante o ciclo de vida do aplicativo. Vejamos este arquivo em partes:

  • Início do aplicativo

O início da aplicação é indicado pelo evento ready. Neste ponto, Electron nos diz que está pronto para gerar novas janelas do navegador. Um detalhe: nesta implementação, o listener deste evento está vinculado à criação da janela principal:

app.on('ready', createWindow);

  • Criando janelas

Esta etapa é relativamente simples, pois você pode adicionar propriedades específicas às janelas (instâncias de BrowserWindow) para fazê-las se comportarem de uma maneira específica (incluindo adicionar listeners ou configurando um preload script):

const createWindow = () => {

 // Create the browser window.

 const mainWindow = new BrowserWindow({

   width: 400,

   height: 600,

   autoHideMenuBar: true,

   resizable: false,

   maximizable: false,

   webPreferences: {

     preload: path.join(__dirname, 'preload.js'),

   },

 });

 // and load the index.html of the app.

 if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {

   mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);

 } else {

   mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));

 }

 // Open the DevTools.

 mainWindow.webContents.openDevTools({ mode: 'detach' });

};


Normalmente, a criação de janelas permite a execução de um preload script, mesmo que é executado após o início da janela, mas antes do Renderer Process mesmo, aqui você pode compartilhar valores entre um processo e outro e expor uma classe API. Aqui um exemplo:

// Preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld(

 'electron',

 {

   closeApp: () => ipcRenderer.send('close'),

 }

)


// Main.js

const { ipcMain } = require('electron')

// Recepción del evento close

ipcMain.on('close', () => {

 app.quit()

})


Como você pode ver, o módulo contextBridge foi utilizado para expor uma função que permite enviar um sinal para o Main Process através do módulo ipcRenderer. Este último é um emissor e receptor de eventos que possibilita o envio de sinais e dados ao Main Process, mas não pode ser usado em Renderer Process.

Por segurança, é aconselhável expor pequenas APIs com funções que realizam diretamente o processo ou o enviam por sinais para que o Main Process cuide de tudo.

  • Fechando o aplicativo

Existem várias maneiras de fechar ou encerrar um aplicativo Electron. O mais normal é definido por padrão no arquivo src/Main.js e basicamente esta função verifica o número de janelas abertas. Se for igual a 0 e a plataforma não for Darwin (Mac), termina o Main Process:

app.on('window-all-closed', () => {

  if (process.platform !== 'darwin') {

    app.quit();

  }

});


Embora tenhamos finalizado a revisão dos arquivos importantes, ainda falta algo: finalizar as funcionalidades principais do nosso app, gerar o tabuleiro do jogo, renderizar e controlar seu estado... Mas isso já está parcialmente resolvido! Faremos isso usando uma biblioteca e nos limitaremos a aplicar certas ações e renderizar o tabuleiro de forma reativa.

Agora é hora de instalar um pacote do npm que, convenientemente, resolve nosso problema:

npm i slide-puzzle-engine


Esta livraria é muito simples e permite gerar um tabuleiro deslizante apenas indicando suas dimensões. Também possui funções especiais para trocar peças na direção que escolhermos.

Vamos começar com a implementação. Primeiro, no arquivo src/components/Game.vue você deve criar a placa e inicializá-la ao montar o componente Vue:

<script setup>

import { Board, Direction } from 'slide-puzzle-engine'

import {

 reactive,

 onMounted,

 computed,

 watch,

 ref,

} from 'vue'

let Game = reactive({ board: new Board({

 dimensions: {width: 3, height: 3 }

})})


</script>


Esta nova tag de script Deve ir para o início do arquivo. Por enquanto, a placa já foi criada, embora suas propriedades precisem ser expostas através de propriedades computadas:


A função to2Array() retorna um arranjo 2D em que as peças são indicadas por números e o espaço vazio é indicado por um X. Sabendo disso, podemos renderizar o tabuleiro:


Com isso, o tabuleiro é percorrido e as peças são renderizadas, além de atribuir uma propriedade data-cursor como true quando a posição possui espaço vazio.

Deveria ficar assim:


O aplicativo está realmente começando a ficar bom, porque o modelo tem estilos pré-carregados. Dessa forma, podemos nos dedicar à operação sem nos distrairmos tanto com o layout. Está tudo no diretório src/styles para quem quiser dar uma olhada.

Neste ponto o tabuleiro já está renderizado, mas ainda precisamos fazer as peças se moverem. Faremos isso adicionando um evento keyup ao objeto document e utilizando o método move(), que facilita a troca de posição entre o espaço vazio e a peça na direção indicada, desde que seja um movimento válido:


Com isso, deverá permitir-nos movimentar-nos pressionando as setas ou as letras W, A, S ou D do teclado. Agora é possível se mover. Porém, ainda não conseguimos fazer o que é crucial para qualquer jogo: ganhar!


Para fazer isso, você deve adicionar uma série de modificações: primeiro você deve fazer um watcher que é responsável por perceber mudanças no estado solved do conselho. Esse watcher deve ativar e desativar os eventos do teclado, além de ser necessário dar algum feedback ao jogador, indicando que ele ganhou:


Com isso já vemos mudanças no estado da diretoria. O próximo é o feedback, então devemos modificar a tag <section class=”hero center”>:

Preparar! Podemos ganhar o jogo!

Pois bem, chegou-se ao ponto em que a aplicação cumpre o seu ciclo de vida. A partir deste ponto, diversas funcionalidades podem ser implementadas para dar mais vida a este projeto:

  • Timer para informar ao usuário quanto tempo levou para resolver o quebra-cabeça.
  • Botão para reiniciar o jogo (tente pressionar Ctrl+r).
  • Substitua números por imagens.
  • Classificação local com base no carimbo de data/hora.

Todas essas pequenas ideias podem ajudá-lo a desenvolver ainda mais este exemplo e até mesmo aplicar métodos API para carregamento e processamento de arquivos (entre outros detalhes), bem como o uso de Electron forge para publicar seu aplicativo nas lojas disponíveis.

Electron.js é um framework bastante amplo e pode parecer mais complicado se você misturá-lo com outra coisa para front-end. O mais importante é entender como funcionam suas bases para que funcione com as ferramentas que você mais gosta ou que melhor atendem às necessidades do seu projeto.


Conclusão

Com Electron.js não há desculpas para ter interfaces chamativas em aplicativos de desktop, embora isso tenha um custo em recursos difícil de ignorar. Se você contar com as ferramentas certas, é um framework bastante versátil e fácil de usar, excelente se você deseja mostrar algo atraente, funcional em múltiplas plataformas e que o ajudará a obter resultados bastante decentes em pouco tempo.

Espero que este guia tenha sido útil. Vejo você na próxima vez!

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.