Projetando e construindo um aplicativo da Web progressivo sem uma estrutura (Parte 2)
Publicados: 2022-03-10A razão de ser desta aventura foi empurrar seu humilde autor um pouco nas disciplinas de design visual e codificação JavaScript. A funcionalidade do aplicativo que decidi construir não era diferente de um aplicativo 'a fazer'. É importante ressaltar que este não foi um exercício de pensamento original. O destino era muito menos importante do que a viagem.
Quer saber como ficou o aplicativo? Aponte o navegador do seu telefone para https://io.benfrain.com.
Aqui está um resumo do que abordaremos neste artigo:
- A configuração do projeto e porque optei pelo Gulp como ferramenta de construção;
- Padrões de design de aplicativos e o que eles significam na prática;
- Como armazenar e visualizar o estado do aplicativo;
- como o CSS foi definido para componentes;
- quais sutilezas de UI/UX foram empregadas para tornar as coisas mais 'semelhantes a aplicativos';
- Como o mandato mudou por meio da iteração.
Vamos começar com as ferramentas de construção.
Ferramentas de construção
Para colocar minhas ferramentas básicas do TypeScipt e PostCSS em funcionamento e criar uma experiência de desenvolvimento decente, eu precisaria de um sistema de compilação.
No meu trabalho diário, nos últimos cinco anos, tenho construído protótipos de interface em HTML/CSS e, em menor grau, JavaScript. Até recentemente, eu usava o Gulp com qualquer número de plugins quase exclusivamente para atender às minhas necessidades de construção bastante humildes.
Normalmente, preciso processar CSS, converter JavaScript ou TypeScript para JavaScript com suporte mais amplo e, ocasionalmente, realizar tarefas relacionadas, como reduzir a saída de código e otimizar ativos. Usar o Gulp sempre me permitiu resolver esses problemas com desenvoltura.
Para quem não conhece, o Gulp permite que você escreva JavaScript para fazer 'alguma coisa' nos arquivos em seu sistema de arquivos local. Para usar o Gulp, você normalmente tem um único arquivo (chamado gulpfile.js
) na raiz do seu projeto. Este arquivo JavaScript permite definir tarefas como funções. Você pode adicionar 'Plugins' de terceiros, que são essencialmente outras funções JavaScript, que lidam com tarefas específicas.
Um Exemplo de Tarefa Gulp
Um exemplo de tarefa Gulp pode ser usar um plug-in para aproveitar o PostCSS para processar para CSS quando você altera uma folha de estilo de autoria (gulp-postcss). Ou compilar arquivos TypeScript para JavaScript vanilla (gulp-typescript) conforme você os salva. Aqui está um exemplo simples de como você escreve uma tarefa no Gulp. Esta tarefa usa o plugin 'del' gulp para excluir todos os arquivos em uma pasta chamada 'build':
var del = require("del"); gulp.task("clean", function() { return del(["build/**/*"]); });
O require
atribui o plugin del
a uma variável. Em seguida, o método gulp.task
é chamado. Nomeamos a tarefa com uma string como primeiro argumento (“clean”) e depois executamos uma função, que neste caso usa o método 'del' para excluir a pasta passada a ela como argumento. Os símbolos de asterisco são padrões 'glob' que essencialmente dizem 'qualquer arquivo em qualquer pasta' da pasta de compilação.
As tarefas do Gulp podem ficar muito mais complicadas, mas, em essência, essa é a mecânica de como as coisas são tratadas. A verdade é que, com o Gulp, você não precisa ser um mago de JavaScript para se virar; As habilidades de copiar e colar de grau 3 são tudo o que você precisa.
Eu fiquei com o Gulp como minha ferramenta de compilação padrão/executor de tarefas por todos esses anos com uma política de 'se não estiver quebrado; não tente consertá-lo'.
No entanto, eu estava preocupado que eu estava ficando preso em meus caminhos. É uma armadilha fácil de cair. Primeiro, você começa a passar férias no mesmo lugar todos os anos, depois se recusa a adotar novas tendências da moda antes de finalmente e se recusa a experimentar novas ferramentas de construção.
Eu tinha ouvido muita conversa na Internet sobre o 'Webpack' e pensei que era meu dever tentar um projeto usando o novo brinde dos desenvolvedores front-end cool-kids.
Webpack
Lembro-me claramente de pular para o site webpack.js.org com grande interesse. A primeira explicação do que é o Webpack começou assim:
import bar from './bar';
Diga o quê? Nas palavras do Dr. Evil, “Me jogue um osso aqui, Scott”.
Eu sei que é meu próprio problema para lidar, mas desenvolvi uma repulsa a qualquer explicação de codificação que mencione 'foo', 'bar' ou 'baz'. Isso, mais a completa falta de descrever sucintamente para que o Webpack era realmente, me fez suspeitar que talvez não fosse para mim.
Indo um pouco mais a fundo na documentação do Webpack, uma explicação um pouco menos opaca foi oferecida: “Em sua essência, o webpack é um empacotador de módulo estático para aplicativos JavaScript modernos”.
Hmmm. Empacotador de módulo estático. Era isso que eu queria? Eu não estava convencido. Eu lia, mas quanto mais eu lia, menos claro eu era. Naquela época, conceitos como gráficos de dependência, recarregamento de módulo quente e pontos de entrada eram essencialmente perdidos para mim.
Algumas noites depois de pesquisar o Webpack, abandonei qualquer noção de usá-lo.
Tenho certeza de que na situação certa e em mãos mais experientes, o Webpack é imensamente poderoso e apropriado, mas parecia um exagero completo para minhas necessidades humildes. Agrupamento de módulos, trepidação de árvores e recarga de módulos a quente pareciam ótimos; Eu simplesmente não estava convencido de que precisava deles para o meu pequeno 'aplicativo'.
Então, de volta ao Gulp então.
Sobre o tema de não mudar as coisas por causa da mudança, outra tecnologia que eu queria avaliar era o Yarn sobre o NPM para gerenciar as dependências do projeto. Até aquele momento, eu sempre havia usado o NPM e o Yarn estava sendo apontado como uma alternativa melhor e mais rápida. Não tenho muito a dizer sobre o Yarn, exceto se você estiver usando o NPM e tudo estiver OK, você não precisa se preocupar em experimentar o Yarn.
Uma ferramenta que chegou tarde demais para eu avaliar para esta aplicação é o Parceljs. Com configuração zero e um BrowserSync como o recarregamento do navegador, desde então encontrei uma grande utilidade nele! Além disso, em defesa do Webpack, me disseram que a v4 em diante do Webpack não requer um arquivo de configuração. Curiosamente, em uma pesquisa mais recente que fiz no Twitter, dos 87 entrevistados, mais da metade escolheu Webpack em vez de Gulp, Parcel ou Grunt.
Comecei meu arquivo Gulp com funcionalidades básicas para começar a funcionar.
Uma tarefa 'padrão' observaria as pastas 'fonte' de folhas de estilo e arquivos TypeScript e os compilaria em uma pasta de build
junto com o HTML básico e os mapas de origem associados.
Eu tenho o BrowserSync trabalhando com o Gulp também. Eu poderia não saber o que fazer com um arquivo de configuração do Webpack, mas isso não significava que eu fosse algum tipo de animal. Ter que atualizar manualmente o navegador durante a iteração com HTML/CSS é muuuuito 2010 e o BrowserSync oferece aquele feedback curto e loop de iteração que é tão útil para codificação de front-end.
Aqui está o arquivo gulp básico a partir de 11.6.2017
Você pode ver como eu ajustei o Gulpfile mais perto do final do envio, adicionando minificação com ugilify:
Estrutura do projeto
Por consequência das minhas escolhas de tecnologia, alguns elementos de organização de código para a aplicação foram se definindo. Um gulpfile.js
na raiz do projeto, uma pasta node_modules
(onde o Gulp armazena o código do plugin), uma pasta preCSS
para as folhas de estilo de autoria, uma pasta ts
para os arquivos TypeScript e uma pasta build
para o código compilado.
A idéia era ter um index.html
que contivesse o 'shell' do aplicativo, incluindo qualquer estrutura HTML não dinâmica e, em seguida, links para os estilos e o arquivo JavaScript que faria o aplicativo funcionar. No disco, ficaria mais ou menos assim:
build/ node_modules/ preCSS/ img/ partials/ styles.css ts/ .gitignore gulpfile.js index.html package.json tsconfig.json
Configurar o BrowserSync para examinar essa pasta de build
significava que eu poderia apontar meu navegador para localhost:3000
e tudo estava bem.
Com um sistema de compilação básico em vigor, organização de arquivos estabelecida e alguns projetos básicos para começar, eu tinha acabado a forragem de procrastinação que eu poderia usar legitimamente para me impedir de realmente construir a coisa!
Escrevendo um aplicativo
O princípio de como o aplicativo funcionaria era esse. Haveria um armazenamento de dados. Quando o JavaScript é carregado, ele carrega esses dados, percorre cada jogador nos dados, criando o HTML necessário para representar cada jogador como uma linha no layout e colocando-os na seção de entrada/saída apropriada. Então as interações do usuário moveriam um jogador de um estado para outro. Simples.
Quando chegou a hora de realmente escrever o aplicativo, os dois grandes desafios conceituais que precisavam ser entendidos eram:
- Como representar os dados para um aplicativo de uma maneira que possa ser facilmente estendida e manipulada;
- Como fazer a interface do usuário reagir quando os dados foram alterados na entrada do usuário.
Uma das maneiras mais simples de representar uma estrutura de dados em JavaScript é com notação de objeto. Essa frase lê um pouco de ciência da computação. Mais simplesmente, um 'objeto' na linguagem JavaScript é uma maneira prática de armazenar dados.
Considere este objeto JavaScript atribuído a uma variável chamada ioState
(para In/Out State):
var ioState = { Count: 0, // Running total of how many players RosterCount: 0; // Total number of possible players ToolsExposed: false, // Whether the UI for the tools is showing Players: [], // A holder for the players }
Se você realmente não conhece JavaScript tão bem, provavelmente pode pelo menos entender o que está acontecendo: cada linha dentro das chaves é uma propriedade (ou 'chave' na linguagem JavaScript) e um par de valores. Você pode definir todos os tipos de coisas para uma chave JavaScript. Por exemplo, funções, arrays de outros dados ou objetos aninhados. Aqui está um exemplo:
var testObject = { testFunction: function() { return "sausages"; }, testArray: [3,7,9], nestedtObject { key1: "value1", key2: 2, } }
O resultado líquido é que, usando esse tipo de estrutura de dados, você pode obter e definir qualquer uma das chaves do objeto. Por exemplo, se quisermos definir a contagem do objeto ioState para 7:
ioState.Count = 7;
Se quisermos definir um pedaço de texto para esse valor, a notação funciona assim:
aTextNode.textContent = ioState.Count;
Você pode ver que obter valores e definir valores para esse objeto de estado é simples no lado JavaScript das coisas. No entanto, refletir essas mudanças na interface do usuário é menos importante. Esta é a principal área onde frameworks e bibliotecas procuram abstrair a dor.
Em termos gerais, quando se trata de atualizar a interface do usuário com base no estado, é preferível evitar consultar o DOM, pois isso geralmente é considerado uma abordagem abaixo do ideal.
Considere a interface de entrada/saída. Normalmente, mostra uma lista de jogadores em potencial para um jogo. Eles são listados verticalmente, um abaixo do outro, na página.
Talvez cada jogador seja representado no DOM com um label
envolvendo uma input
de caixa de seleção. Dessa forma, clicar em um jogador mudaria o jogador para 'Entrada' em virtude do rótulo que torna a entrada 'marcada'.
Para atualizar nossa interface, podemos ter um 'ouvinte' em cada elemento de entrada no JavaScript. Em um clique ou alteração, a função consulta o DOM e conta quantas entradas de nossos jogadores são verificadas. Com base nessa contagem, atualizaríamos outra coisa no DOM para mostrar ao usuário quantos jogadores foram verificados.
Vamos considerar o custo dessa operação básica. Estamos ouvindo vários nós DOM para o clique/verificação de uma entrada, consultando o DOM para ver quantos de um determinado tipo de DOM são verificados e, em seguida, gravando algo no DOM para mostrar ao usuário, em termos de interface do usuário, o número de jogadores acabamos de contar.
A alternativa seria manter o estado do aplicativo como um objeto JavaScript na memória. Um clique de botão/entrada no DOM poderia simplesmente atualizar o objeto JavaScript e, em seguida, com base nessa alteração no objeto JavaScript, fazer uma atualização de passagem única de todas as alterações de interface necessárias. Poderíamos pular a consulta do DOM para contar os jogadores, pois o objeto JavaScript já conteria essa informação.
Assim. Usar uma estrutura de objeto JavaScript para o estado parecia simples, mas flexível o suficiente para encapsular o estado do aplicativo a qualquer momento. A teoria de como isso poderia ser gerenciado também parecia bastante sólida – deve ser isso que frases como 'fluxo de dados de mão única' eram? No entanto, o primeiro truque real seria criar algum código que atualizasse automaticamente a interface do usuário com base em quaisquer alterações nesses dados.
A boa notícia é que pessoas mais inteligentes do que eu já descobriram essas coisas ( graças a Deus! ). As pessoas vêm aperfeiçoando abordagens para esse tipo de desafio desde o início das aplicações. Esta categoria de problemas é o pão com manteiga dos 'padrões de design'. O apelido de 'padrão de design' soou esotérico para mim no começo, mas depois de cavar um pouco, tudo começou a soar menos ciência da computação e mais senso comum.
Padrões de design
Um padrão de projeto, no léxico da ciência da computação, é uma maneira pré-definida e comprovada de resolver um desafio técnico comum. Pense nos padrões de design como o equivalente de codificação de uma receita culinária.
Talvez a literatura mais famosa sobre design patterns seja "Design Patterns: Elements of Reusable Object-Oriented Software" de 1994. Embora trate de C++ e smalltalk, os conceitos são transferíveis. Para JavaScript, "Learning JavaScript Design Patterns" de Addy Osmani cobre um terreno semelhante. Você também pode lê-lo online gratuitamente aqui.
Padrão de observador
Normalmente, os padrões de design são divididos em três grupos: Criacional, Estrutural e Comportamental. Eu estava procurando por algo comportamental que ajudasse a lidar com as mudanças de comunicação nas diferentes partes do aplicativo.
Mais recentemente, eu vi e li um grande mergulho profundo sobre a implementação de reatividade dentro de um aplicativo por Gregg Pollack. Há uma postagem no blog e um vídeo para sua diversão aqui.
Ao ler a descrição de abertura do padrão 'Observer' em Learning JavaScript Design Patterns
, tive certeza de que era o padrão para mim. É assim descrito:
O Observer é um padrão de projeto onde um objeto (conhecido como sujeito) mantém uma lista de objetos dependentes dele (observadores), notificando-os automaticamente sobre qualquer mudança de estado.
Quando um assunto precisa notificar os observadores sobre algo interessante acontecendo, ele transmite uma notificação aos observadores (que pode incluir dados específicos relacionados ao tópico da notificação).
A chave para minha empolgação era que isso parecia oferecer uma maneira de as coisas se atualizarem quando necessário.
Suponha que o usuário clicou em uma jogadora chamada “Betty” para selecionar que ela estava 'In' para o jogo. Algumas coisas podem precisar acontecer na interface do usuário:
- Adicione 1 à contagem de reprodução
- Remover Betty do grupo de jogadores 'Fora'
- Adicione Betty ao grupo de jogadores 'In'
O aplicativo também precisaria atualizar os dados que representavam a interface do usuário. O que eu queria muito evitar era isso:
playerName.addEventListener("click", playerToggle); function playerToggle() { if (inPlayers.includes(e.target.textContent)) { setPlayerOut(e.target.textContent); decrementPlayerCount(); } else { setPlayerIn(e.target.textContent); incrementPlayerCount(); } }
O objetivo era ter um fluxo de dados elegante que atualizasse o que era necessário no DOM quando e se os dados centrais fossem alterados.
Com um padrão Observer, foi possível enviar atualizações para o estado e, portanto, a interface do usuário de forma bastante sucinta. Aqui está um exemplo, a função real usada para adicionar um novo jogador à lista:
function itemAdd(itemString: string) { let currentDataSet = getCurrentDataSet(); var newPerson = new makePerson(itemString); io.items[currentDataSet].EventData.splice(0, 0, newPerson); io.notify({ items: io.items }); }
A parte relevante para o padrão Observer é o método io.notify
. Como isso nos mostra a modificação dos items
fazem parte do estado do aplicativo, deixe-me mostrar o observador que escutou as alterações em 'items':
io.addObserver({ props: ["items"], callback: function renderItems() { // Code that updates anything to do with items... } });
Temos um método de notificação que faz alterações nos dados e, em seguida, Observadores para esses dados que respondem quando as propriedades de seu interesse são atualizadas.
Com essa abordagem, o aplicativo pode ter observáveis observando alterações em qualquer propriedade dos dados e executar uma função sempre que ocorrer uma alteração.
Se você estiver interessado no padrão Observer pelo qual optei, descrevo-o mais detalhadamente aqui.
Agora havia uma abordagem para atualizar a interface do usuário efetivamente com base no estado. Pêssego. No entanto, isso ainda me deixou com dois problemas gritantes.
Uma era como armazenar o estado entre as recargas/sessões de página e o fato de que, apesar da interface do usuário funcionar, visualmente, não era muito 'como um aplicativo'. Por exemplo, se um botão foi pressionado, a interface do usuário mudou instantaneamente na tela. Simplesmente não era particularmente convincente.
Vamos lidar primeiro com o lado do armazenamento.
Salvando estado
Meu principal interesse de um lado do desenvolvimento que entra nisso centrado em entender como as interfaces de aplicativos podem ser construídas e tornadas interativas com JavaScript. Como armazenar e recuperar dados de um servidor ou lidar com autenticação de usuário e logins estava 'fora do escopo'.
Portanto, em vez de me conectar a um serviço web para as necessidades de armazenamento de dados, optei por manter todos os dados no cliente. Existem vários métodos de plataforma web para armazenar dados em um cliente. Optei por localStorage
.
A API para localStorage é incrivelmente simples. Você define e obtém dados assim:
// Set something localStorage.setItem("yourKey", "yourValue"); // Get something localStorage.getItem("yourKey");
LocalStorage tem um método setItem
para o qual você passa duas strings. A primeira é o nome da chave com a qual você deseja armazenar os dados e a segunda string é a string real que você deseja armazenar. O método getItem
recebe uma string como argumento que retorna para você o que estiver armazenado nessa chave em localStorage. Bonito e simples.
No entanto, entre as razões para não usar localStorage está o fato de que tudo deve ser salvo como uma 'string'. Isso significa que você não pode armazenar diretamente algo como um array ou objeto. Por exemplo, tente executar estes comandos no console do seu navegador:
// Set something localStorage.setItem("myArray", [1, 2, 3, 4]); // Get something localStorage.getItem("myArray"); // Logs "1,2,3,4"
Mesmo que tenhamos tentado definir o valor de 'myArray' como um array; quando o recuperamos, ele estava armazenado como uma string (observe as aspas em torno de '1,2,3,4').
Você certamente pode armazenar objetos e arrays com localStorage, mas você precisa estar ciente de que eles precisam ser convertidos para frente e para trás de strings.
Portanto, para gravar dados de estado em localStorage, ele foi gravado em uma string com o método JSON.stringify()
assim:
const storage = window.localStorage; storage.setItem("players", JSON.stringify(io.items));
Quando os dados precisavam ser recuperados de localStorage, a string era convertida novamente em dados utilizáveis com o método JSON.parse()
assim:
const players = JSON.parse(storage.getItem("players"));
Usar localStorage
significava que tudo estava no cliente e isso significava que não havia serviços de terceiros ou preocupações com armazenamento de dados.
Os dados agora estavam persistindo em atualizações e sessões — Yay! A má notícia foi que o localStorage não sobrevive a um usuário esvaziando os dados do navegador. Quando alguém fizesse isso, todos os seus dados de entrada/saída seriam perdidos. Isso é uma falha séria.
Não é difícil perceber que o `localStorage` provavelmente não é a melhor solução para aplicações 'adequadas'. Além do problema de string mencionado acima, também é lento para trabalhos sérios, pois bloqueia o 'thread principal'. Alternativas estão chegando, como o KV Storage, mas por enquanto, faça uma nota mental para resguardar seu uso com base na adequação.
Apesar da fragilidade de salvar dados localmente no dispositivo de um usuário, a conexão com um serviço ou banco de dados foi resistida. Em vez disso, o problema foi contornado oferecendo uma opção de 'carregar/salvar'. Isso permitiria que qualquer usuário do In/Out salvasse seus dados como um arquivo JSON que poderia ser carregado de volta no aplicativo, se necessário.
Isso funcionou bem no Android, mas muito menos elegante no iOS. Em um iPhone, resultou em um excesso de texto na tela como este:
Como você pode imaginar, eu estava longe de ser o único a repreender a Apple via WebKit sobre essa falha. O bug relevante estava aqui.
No momento em que escrevo, esse bug tem uma solução e um patch, mas ainda não chegou ao iOS Safari. Alegadamente, o iOS13 corrige isso, mas está em Beta enquanto escrevo.
Então, para o meu produto mínimo viável, isso foi endereçado ao armazenamento. Agora era hora de tentar tornar as coisas mais 'app-like'!
App-I-Ness
Acontece que depois de muitas discussões com muitas pessoas, definir exatamente o que 'app like' significa é bastante difícil.
Por fim, decidi que 'app-like' é sinônimo de uma astúcia visual que geralmente falta na web. Quando penso nos aplicativos que são bons de usar, todos apresentam movimento. Não gratuito, mas movimento que acrescenta à história de suas ações. Podem ser as transições de página entre telas, a maneira pela qual os menus surgem. É difícil descrever em palavras, mas a maioria de nós sabe quando a vemos.
O primeiro toque visual necessário foi mudar os nomes dos jogadores para cima ou para baixo de 'In' para 'Out' e vice-versa quando selecionado. Fazer um jogador mover-se instantaneamente de uma seção para outra era simples, mas certamente não 'como um aplicativo'. Espera-se que uma animação com o nome de um jogador seja clicado enfatize o resultado dessa interação – o jogador passando de uma categoria para outra.
Como muitos desses tipos de interações visuais, sua aparente simplicidade desmente a complexidade envolvida em fazê-lo funcionar bem.
Foram necessárias algumas iterações para acertar o movimento, mas a lógica básica era esta:
- Assim que um 'jogador' for clicado, capture onde esse jogador está, geometricamente, na página;
- Meça a que distância o topo da área o jogador precisa se mover se estiver subindo ('In') e quão longe está o fundo, se estiver descendo ('Out');
- Se estiver subindo, um espaço igual à altura da linha do jogador precisa ser deixado à medida que o jogador se move para cima e os jogadores acima devem cair para baixo na mesma proporção que o tempo que leva para o jogador subir para pousar no espaço desocupado pelos jogadores 'In' existentes (se houver) que desçam;
- Se um jogador está saindo e se movendo para baixo, todo o resto precisa se mover para o espaço à esquerda e o jogador precisa terminar abaixo de qualquer jogador atual.
Ufa! Foi mais complicado do que eu pensava em inglês - não importa o JavaScript!
Havia complexidades adicionais a serem consideradas e testadas, como velocidades de transição. No início, não era óbvio se uma velocidade constante de movimento (por exemplo, 20px por 20ms) ou uma duração constante para o movimento (por exemplo, 0,2s) ficaria melhor. O primeiro era um pouco mais complicado, pois a velocidade precisava ser calculada 'on the fly' com base na distância que o jogador precisava percorrer - distância maior exigindo uma duração de transição mais longa.
No entanto, descobriu-se que uma duração de transição constante não era apenas mais simples no código; na verdade, produziu um efeito mais favorável. A diferença foi sutil, mas esse é o tipo de escolha que você só pode determinar depois de ver as duas opções.
De vez em quando, ao tentar acertar esse efeito, uma falha visual chamava a atenção, mas era impossível desconstruir em tempo real. Descobri que o melhor processo de depuração era criar uma gravação QuickTime da animação e depois passar por um quadro de cada vez. Invariavelmente, isso revelava o problema mais rapidamente do que qualquer depuração baseada em código.
Olhando para o código agora, posso apreciar que em algo além do meu humilde aplicativo, essa funcionalidade quase certamente poderia ser escrita de forma mais eficaz. Dado que o aplicativo saberia o número de jogadores e saberia a altura fixa dos slats, deve ser totalmente possível fazer todos os cálculos de distância apenas no JavaScript, sem nenhuma leitura do DOM.
Não é que o que foi enviado não funcione, é apenas que não é o tipo de solução de código que você exibiria na Internet. Oh espere.
Outras interações de 'app like' foram muito mais fáceis de realizar. Em vez de menus simplesmente entrando e saindo com algo tão simples quanto alternar uma propriedade de exibição, muita milhagem foi obtida simplesmente expondo-os com um pouco mais de sutileza. Ainda foi acionado simplesmente, mas o CSS estava fazendo todo o trabalho pesado:
.io-EventLoader { position: absolute; top: 100%; margin-top: 5px; z-index: 100; width: 100%; opacity: 0; transition: all 0.2s; pointer-events: none; transform: translateY(-10px); [data-evswitcher-showing="true"] & { opacity: 1; pointer-events: auto; transform: none; } }
Lá, quando o data-evswitcher-showing="true"
foi alternado em um elemento pai, o menu desapareceria, se transformaria de volta em sua posição padrão e os eventos de ponteiro seriam reativados para que o menu pudesse receber cliques.
Metodologia da Folha de Estilo ECSS
Você notará nesse código anterior que, do ponto de vista da autoria, as substituições de CSS estão sendo aninhadas em um seletor pai. É assim que sempre prefiro escrever folhas de estilo de interface do usuário; uma única fonte de verdade para cada seletor e quaisquer substituições para esse seletor encapsuladas em um único conjunto de chaves. É um padrão que requer o uso de um processador CSS (Sass, PostCSS, LESS, Stylus, et al), mas sinto que é a única maneira positiva de fazer uso da funcionalidade de aninhamento.
Eu consolidei essa abordagem em meu livro, Enduring CSS e, apesar de haver uma infinidade de métodos mais envolvidos disponíveis para escrever CSS para elementos de interface, o ECSS serviu bem a mim e às grandes equipes de desenvolvimento com as quais trabalho desde que a abordagem foi documentada pela primeira vez de volta em 2014! Provou-se tão eficaz neste caso.
Parcializando o TypeScript
Mesmo sem um processador CSS ou linguagem superset como o Sass, o CSS tem a capacidade de importar um ou mais arquivos CSS para outro com a diretiva import:
@import "other-file.css";
Ao começar com JavaScript fiquei surpreso que não havia equivalente. Sempre que os arquivos de código ficam maiores que uma tela ou tão alta, sempre parece que dividi-los em partes menores seria benéfico.
Outro bônus de usar o TypeScript foi que ele tem uma maneira muito simples de dividir o código em arquivos e importá-los quando necessário.
Esse recurso era anterior aos módulos JavaScript nativos e era um recurso de grande conveniência. Quando o TypeScript foi compilado, ele uniu tudo de volta a um único arquivo JavaScript. Isso significava que era possível dividir facilmente o código do aplicativo em arquivos parciais gerenciáveis para autoria e importá-los facilmente para o arquivo principal. O topo do inout.ts
principal ficou assim:
/// <reference path="defaultData.ts" /> /// <reference path="splitTeams.ts" /> /// <reference path="deleteOrPaidClickMask.ts" /> /// <reference path="repositionSlat.ts" /> /// <reference path="createSlats.ts" /> /// <reference path="utils.ts" /> /// <reference path="countIn.ts" /> /// <reference path="loadFile.ts" /> /// <reference path="saveText.ts" /> /// <reference path="observerPattern.ts" /> /// <reference path="onBoard.ts" />
Essa simples tarefa de limpeza e organização ajudou enormemente.
Vários eventos
No início, senti que do ponto de vista da funcionalidade, um único evento, como o “Tuesday Night Football” seria suficiente. Nesse cenário, se você carregou o In/Out, você acabou de adicionar/remover ou mover jogadores para dentro ou para fora e pronto. Não havia noção de múltiplos eventos.
Eu rapidamente decidi que (mesmo indo para um produto mínimo viável) isso resultaria em uma experiência bastante limitada. E se alguém organizasse dois jogos em dias diferentes, com uma lista de jogadores diferente? Certamente o In/Out poderia/deveria acomodar essa necessidade? Não demorou muito para remodelar os dados para tornar isso possível e alterar os métodos necessários para carregar em um conjunto diferente.
No início, o conjunto de dados padrão se parecia com isto:
var defaultData = [ { name: "Daz", paid: false, marked: false, team: "", in: false }, { name: "Carl", paid: false, marked: false, team: "", in: false }, { name: "Big Dave", paid: false, marked: false, team: "", in: false }, { name: "Nick", paid: false, marked: false, team: "", in: false } ];
Uma matriz contendo um objeto para cada jogador.
Depois de fatorar em vários eventos, foi alterado para ficar assim:
var defaultDataV2 = [ { EventName: "Tuesday Night Footy", Selected: true, EventData: [ { name: "Jack", marked: false, team: "", in: false }, { name: "Carl", marked: false, team: "", in: false }, { name: "Big Dave", marked: false, team: "", in: false }, { name: "Nick", marked: false, team: "", in: false }, { name: "Red Boots", marked: false, team: "", in: false }, { name: "Gaz", marked: false, team: "", in: false }, { name: "Angry Martin", marked: false, team: "", in: false } ] }, { EventName: "Friday PM Bank Job", Selected: false, EventData: [ { name: "Mr Pink", marked: false, team: "", in: false }, { name: "Mr Blonde", marked: false, team: "", in: false }, { name: "Mr White", marked: false, team: "", in: false }, { name: "Mr Brown", marked: false, team: "", in: false } ] }, { EventName: "WWII Ladies Baseball", Selected: false, EventData: [ { name: "C Dottie Hinson", marked: false, team: "", in: false }, { name: "P Kit Keller", marked: false, team: "", in: false }, { name: "Mae Mordabito", marked: false, team: "", in: false } ] } ];
Os novos dados eram um array com um objeto para cada evento. Então, em cada evento, havia uma propriedade EventData
que era uma matriz com objetos de jogador como antes.
Demorou muito mais para reconsiderar como a interface poderia lidar melhor com esse novo recurso.
Desde o início, o design sempre foi muito estéril. Considerando que isso também deveria ser um exercício de design, não senti que estava sendo corajoso o suficiente. Então, um pouco mais de estilo visual foi adicionado, começando com o cabeçalho. Isto é o que eu fiz no Sketch:
Não ia ganhar prêmios, mas certamente foi mais impressionante do que quando começou.
Estética à parte, não foi até que alguém apontou, que eu apreciei o grande ícone de mais no cabeçalho que era muito confuso. A maioria das pessoas pensou que era uma maneira de adicionar outro evento. Na realidade, ele mudou para um modo 'Adicionar jogador' com uma transição elegante que permite digitar o nome do jogador no mesmo local em que o nome do evento estava atualmente.
Este foi outro exemplo em que olhos novos foram inestimáveis. Foi também uma importante lição de desapego. A verdade é que eu tinha mantido a transição do modo de entrada no cabeçalho porque achei legal e inteligente. No entanto, o fato é que não estava atendendo ao desenho e, portanto, à aplicação como um todo.
Isso foi alterado na versão ao vivo. Em vez disso, o cabeçalho lida apenas com eventos – um cenário mais comum. Enquanto isso, a adição de jogadores é feita a partir de um submenu. Isso dá ao aplicativo uma hierarquia muito mais compreensível.
A outra lição aprendida aqui foi que, sempre que possível, é extremamente benéfico obter feedback sincero dos colegas. Se eles são pessoas boas e honestas, eles não vão deixar você se dar um passe!
Resumo: Meu código fede
Certo. Até agora, uma retrospectiva de aventura tecnológica normal; essas coisas custam dez centavos no Medium! A fórmula é mais ou menos assim: o desenvolvedor detalha como eles derrubaram todos os obstáculos para lançar um software bem ajustado nas Internets e, em seguida, conseguir uma entrevista no Google ou ser contratado em algum lugar. No entanto, a verdade da questão é que eu era um novato neste malarkey de construção de aplicativos, então o código acabou sendo enviado como o aplicativo 'acabado' fedendo para o céu!
Por exemplo, a implementação do padrão Observer usada funcionou muito bem. Eu era organizado e metódico no início, mas essa abordagem 'foi para o sul' à medida que fiquei mais desesperado para terminar as coisas. Como um dieter em série, velhos hábitos familiares voltaram e a qualidade do código posteriormente caiu.
Looking now at the code shipped, it is a less than ideal hodge-bodge of clean observer pattern and bog-standard event listeners calling functions. In the main inout.ts
file there are over 20 querySelector
method calls; hardly a poster child for modern application development!
I was pretty sore about this at the time, especially as at the outset I was aware this was a trap I didn't want to fall into. However, in the months that have since passed, I've become more philosophical about it.
The final post in this series reflects on finding the balance between silvery-towered code idealism and getting things shipped. It also covers the most important lessons learned during this process and my future aspirations for application development.