Diseño y construcción de una aplicación web progresiva sin un marco (Parte 2)
Publicado: 2022-03-10La razón de ser de esta aventura fue empujar un poco a su humilde autor en las disciplinas del diseño visual y la codificación JavaScript. La funcionalidad de la aplicación que había decidido construir no era diferente a una aplicación de 'tareas pendientes'. Es importante enfatizar que este no fue un ejercicio de pensamiento original. El destino era mucho menos importante que el viaje.
¿Quieres saber cómo terminó la aplicación? Apunte el navegador de su teléfono a https://io.benfrain.com.
Aquí hay un resumen de lo que cubriremos en este artículo:
- La configuración del proyecto y por qué opté por Gulp como herramienta de construcción;
- Patrones de diseño de aplicaciones y lo que significan en la práctica;
- Cómo almacenar y visualizar el estado de la aplicación;
- cómo se amplió el CSS a los componentes;
- qué sutilezas de UI/UX se emplearon para hacer las cosas más 'parecidas a una aplicación';
- Cómo cambió el mandato a través de la iteración.
Comencemos con las herramientas de compilación.
Herramientas de construcción
Para poner en funcionamiento mis herramientas básicas de TypeScipt y PostCSS y crear una experiencia de desarrollo decente, necesitaría un sistema de compilación.
En mi trabajo diario, durante los últimos cinco años, he estado creando prototipos de interfaz en HTML/CSS y, en menor medida, en JavaScript. Hasta hace poco, he usado Gulp con una gran cantidad de complementos casi exclusivamente para satisfacer mis necesidades de compilación bastante humildes.
Por lo general, necesito procesar CSS, convertir JavaScript o TypeScript a JavaScript más compatible y, ocasionalmente, realizar tareas relacionadas, como minimizar la salida del código y optimizar los activos. Usar Gulp siempre me ha permitido resolver esos problemas con aplomo.
Para aquellos que no están familiarizados, Gulp le permite escribir JavaScript para hacer 'algo' con los archivos en su sistema de archivos local. Para usar Gulp, normalmente tiene un solo archivo (llamado gulpfile.js
) en la raíz de su proyecto. Este archivo JavaScript le permite definir tareas como funciones. Puede agregar 'Complementos' de terceros, que son esencialmente otras funciones de JavaScript, que se ocupan de tareas específicas.
Una tarea de Gulp de ejemplo
Un ejemplo de tarea de Gulp podría ser usar un complemento para aprovechar PostCSS para procesar a CSS cuando cambia una hoja de estilo de creación (gulp-postcss). O compilar archivos TypeScript en JavaScript estándar (gulp-typescript) a medida que los guarda. Aquí hay un ejemplo simple de cómo escribir una tarea en Gulp. Esta tarea utiliza el complemento gulp 'del' para eliminar todos los archivos en una carpeta llamada 'build':
var del = require("del"); gulp.task("clean", function() { return del(["build/**/*"]); });
El require
asigna el complemento del
a una variable. Luego se llama al método gulp.task
. Nombramos la tarea con una cadena como primer argumento ("limpiar") y luego ejecutamos una función, que en este caso usa el método 'del' para eliminar la carpeta que se le pasó como argumento. Los símbolos de asterisco allí son patrones 'glob' que esencialmente dicen 'cualquier archivo en cualquier carpeta' de la carpeta de compilación.
Las tareas de Gulp pueden volverse mucho más complicadas, pero en esencia, esa es la mecánica de cómo se manejan las cosas. La verdad es que, con Gulp, no necesitas ser un mago de JavaScript para salir adelante; Las habilidades de copiar y pegar de grado 3 son todo lo que necesita.
Me quedé con Gulp como mi herramienta de compilación/ejecutor de tareas predeterminado durante todos estos años con una política de 'si no está roto; no intentes arreglarlo'.
Sin embargo, me preocupaba quedarme estancado en mis caminos. Es una trampa en la que es fácil caer. Primero, comienzas a ir de vacaciones al mismo lugar todos los años, luego te niegas a adoptar nuevas tendencias de moda antes de finalmente y te niegas firmemente a probar nuevas herramientas de construcción.
Escuché muchas conversaciones en Internet sobre 'Webpack' y pensé que era mi deber probar un proyecto utilizando el brindis novedoso de los chicos geniales de los desarrolladores front-end.
paquete web
Recuerdo claramente haber saltado al sitio webpack.js.org con gran interés. La primera explicación de lo que es y hace Webpack comenzó así:
import bar from './bar';
¿Que qué? En palabras del Dr. Evil, "Tírame un maldito hueso aquí, Scott".
Sé que es mi propio problema con el que lidiar, pero he desarrollado una repugnancia a cualquier explicación de codificación que mencione 'foo', 'bar' o 'baz'. Eso, además de la completa falta de una descripción sucinta para lo que realmente era Webpack, me hizo sospechar que tal vez no era para mí.
Al profundizar un poco más en la documentación de Webpack, se ofreció una explicación un poco menos opaca: "En esencia, webpack es un paquete de módulos estáticos para aplicaciones JavaScript modernas".
Mmm. Empaquetador de módulos estáticos. ¿Era eso lo que quería? No estaba convencido. Seguí leyendo, pero cuanto más leía, menos claro estaba. En aquel entonces, los conceptos como los gráficos de dependencia, la recarga de módulos en caliente y los puntos de entrada prácticamente se me escapaban.
Después de un par de tardes de investigar Webpack, abandoné cualquier idea de usarlo.
Estoy seguro de que en la situación correcta y en manos más experimentadas, Webpack es inmensamente poderoso y apropiado, pero parecía una completa exageración para mis humildes necesidades. La agrupación de módulos, la sacudida de árboles y la recarga de módulos en caliente sonaron geniales; Simplemente no estaba convencido de que los necesitara para mi pequeña 'aplicación'.
Entonces, volvamos a Gulp entonces.
Sobre el tema de no cambiar las cosas por el bien del cambio, otra tecnología que quería evaluar era Yarn over NPM para administrar las dependencias del proyecto. Hasta ese momento, siempre había usado NPM y Yarn se promocionaba como una alternativa mejor y más rápida. No tengo mucho que decir sobre Yarn, excepto si actualmente está usando NPM y todo está bien, no necesita molestarse en probar Yarn.
Una herramienta que llegó demasiado tarde para evaluar esta aplicación es Parceljs. Con configuración cero y una recarga de navegador similar a BrowserSync respaldada, ¡desde entonces he encontrado una gran utilidad en él! Además, en defensa de Webpack, me dijeron que la versión 4 en adelante de Webpack no requiere un archivo de configuración. Como anécdota, en una encuesta más reciente que realicé en Twitter, de los 87 encuestados, más de la mitad eligió Webpack sobre Gulp, Parcel o Grunt.
Comencé mi archivo Gulp con la funcionalidad básica para ponerlo en marcha.
Una tarea 'predeterminada' observaría las carpetas 'fuente' de las hojas de estilo y los archivos TypeScript y los compilaría en una carpeta de build
junto con el HTML básico y los mapas fuente asociados.
También obtuve BrowserSync trabajando con Gulp. Puede que no sepa qué hacer con un archivo de configuración de Webpack, pero eso no significa que sea una especie de animal. Tener que actualizar manualmente el navegador mientras se itera con HTML/CSS es taaaan 2010 y BrowserSync le brinda ese ciclo breve de retroalimentación e iteración que es tan útil para la codificación front-end.
Aquí está el archivo de trago básico a partir del 11.6.2017
Puede ver cómo ajusté el Gulpfile más cerca del final del envío, agregando minificación con ugilify:
Estructura del proyecto
Como consecuencia de mis elecciones tecnológicas, algunos elementos de la organización del código para la aplicación se definían solos. Un gulpfile.js
en la raíz del proyecto, una carpeta node_modules
(donde Gulp almacena el código del complemento), una carpeta preCSS
para las hojas de estilo de creación, una carpeta ts
para los archivos TypeScript y una carpeta de build
para que viva el código compilado.
La idea era tener un index.html
que contuviera el 'shell' de la aplicación, incluida cualquier estructura HTML no dinámica y luego enlaces a los estilos y al archivo JavaScript que haría que la aplicación funcionara. En el disco, se vería así:
build/ node_modules/ preCSS/ img/ partials/ styles.css ts/ .gitignore gulpfile.js index.html package.json tsconfig.json
Configurar BrowserSync para mirar esa carpeta de build
significaba que podía apuntar mi navegador a localhost:3000
y todo estaba bien.
Con un sistema de compilación básico implementado, la organización de archivos resuelta y algunos diseños básicos para comenzar, ¡me había quedado sin forraje para postergar que podía usar legítimamente para evitar que realmente construyera la cosa!
escribir una aplicación
El principio de cómo funcionaría la aplicación era este. Habría un almacén de datos. Cuando se cargaba el JavaScript, cargaba esos datos, recorría cada jugador en los datos, creando el HTML necesario para representar a cada jugador como una fila en el diseño y colocándolos en la sección de entrada/salida adecuada. Luego, las interacciones del usuario moverían a un jugador de un estado a otro. Sencillo.
Cuando se trataba de escribir la aplicación, los dos grandes desafíos conceptuales que debían entenderse eran:
- Cómo representar los datos de una aplicación de manera que puedan ampliarse y manipularse fácilmente;
- Cómo hacer que la interfaz de usuario reaccione cuando se cambiaron los datos de la entrada del usuario.
Una de las formas más sencillas de representar una estructura de datos en JavaScript es con la notación de objetos. Esa oración se lee un poco de informática. Más simplemente, un 'objeto' en la jerga de JavaScript es una forma práctica de almacenar datos.
Considere este objeto de JavaScript asignado a una variable llamada ioState
(para estado de entrada/salida):
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 }
Si realmente no conoce JavaScript tan bien, probablemente al menos pueda comprender lo que está sucediendo: cada línea dentro de las llaves es una propiedad (o 'clave' en el lenguaje de JavaScript) y un par de valores. Puede configurar todo tipo de cosas en una clave de JavaScript. Por ejemplo, funciones, matrices de otros datos u objetos anidados. Aquí hay un ejemplo:
var testObject = { testFunction: function() { return "sausages"; }, testArray: [3,7,9], nestedtObject { key1: "value1", key2: 2, } }
El resultado neto es que usando ese tipo de estructura de datos puede obtener y establecer cualquiera de las claves del objeto. Por ejemplo, si queremos establecer el recuento del objeto ioState en 7:
ioState.Count = 7;
Si queremos establecer un fragmento de texto en ese valor, la notación funciona así:
aTextNode.textContent = ioState.Count;
Puede ver que obtener valores y establecer valores para ese objeto de estado es simple en el lado de JavaScript de las cosas. Sin embargo, reflejar esos cambios en la interfaz de usuario no lo es tanto. Esta es el área principal donde los marcos y las bibliotecas buscan abstraerse del dolor.
En términos generales, cuando se trata de actualizar la interfaz de usuario según el estado, es preferible evitar consultar el DOM, ya que generalmente se considera un enfoque subóptimo.
Considere la interfaz de entrada/salida. Por lo general, muestra una lista de jugadores potenciales para un juego. Se enumeran verticalmente, uno debajo del otro, en la parte inferior de la página.
Quizás cada jugador esté representado en el DOM con una label
que envuelve una input
de casilla de verificación. De esta manera, al hacer clic en un jugador, el jugador cambiaría a 'Entrada' en virtud de la etiqueta que hace que la entrada esté 'marcada'.
Para actualizar nuestra interfaz, podríamos tener un 'oyente' en cada elemento de entrada en el JavaScript. Con un clic o un cambio, la función consulta el DOM y cuenta cuántas de las entradas de nuestro jugador se verifican. Sobre la base de ese conteo, luego actualizaríamos algo más en el DOM para mostrarle al usuario cuántos jugadores están verificados.
Consideremos el costo de esa operación básica. Estamos escuchando en múltiples nodos DOM para hacer clic/verificar una entrada, luego consultamos el DOM para ver cuántos de un tipo de DOM en particular se verifican, luego escribimos algo en el DOM para mostrarle al usuario, en cuanto a la interfaz de usuario, la cantidad de jugadores. acabamos de contar.
La alternativa sería mantener el estado de la aplicación como un objeto JavaScript en la memoria. Un botón/clic de entrada en el DOM podría simplemente actualizar el objeto de JavaScript y luego, en función de ese cambio en el objeto de JavaScript, realizar una actualización de un solo paso de todos los cambios de interfaz que se necesitan. Podríamos omitir la consulta del DOM para contar los jugadores, ya que el objeto JavaScript ya tendría esa información.
Entonces. El uso de una estructura de objeto de JavaScript para el estado parecía simple pero lo suficientemente flexible como para encapsular el estado de la aplicación en un momento dado. La teoría de cómo se podría manejar esto también parecía lo suficientemente sólida: ¿esto debe ser de lo que se tratan frases como "flujo de datos unidireccional"? Sin embargo, el primer truco real sería crear un código que actualizaría automáticamente la interfaz de usuario en función de cualquier cambio en esos datos.
La buena noticia es que personas más inteligentes que yo ya han descubierto estas cosas ( ¡gracias a Dios! ). La gente ha estado perfeccionando enfoques para este tipo de desafío desde el comienzo de las aplicaciones. Esta categoría de problemas es el pan y la mantequilla de los 'patrones de diseño'. El apodo 'patrón de diseño' me sonaba esotérico al principio, pero después de indagar un poco, todo empezó a sonar menos a la informática y más al sentido común.
Patrones de diseño
Un patrón de diseño, en el léxico de la informática, es una forma predefinida y comprobada de resolver un desafío técnico común. Piense en los patrones de diseño como el equivalente de codificación de una receta de cocina.
Quizás la literatura más famosa sobre patrones de diseño es "Patrones de diseño: elementos de software orientado a objetos reutilizables" de 1994. Aunque se trata de C++ y conversaciones triviales, los conceptos son transferibles. Para JavaScript, "Aprender patrones de diseño de JavaScript" de Addy Osmani cubre un terreno similar. También puedes leerlo en línea gratis aquí.
Patrón de observador
Normalmente, los patrones de diseño se dividen en tres grupos: de creación, estructurales y de comportamiento. Estaba buscando algo conductual que ayudara a lidiar con la comunicación de cambios en las diferentes partes de la aplicación.
Más recientemente, vi y leí un análisis profundo realmente excelente sobre la implementación de la reactividad dentro de una aplicación de Gregg Pollack. Hay una publicación de blog y un video para su disfrute aquí.
Al leer la descripción inicial del patrón 'Observer' en Learning JavaScript Design Patterns
, estaba bastante seguro de que era el patrón para mí. Se describe así:
El observador es un patrón de diseño en el que un objeto (conocido como sujeto) mantiene una lista de objetos que dependen de él (observadores), notificándoles automáticamente cualquier cambio de estado.
Cuando un sujeto necesita notificar a los observadores sobre algo interesante que sucede, transmite una notificación a los observadores (que puede incluir datos específicos relacionados con el tema de la notificación).
La clave de mi entusiasmo fue que esto parecía ofrecer alguna forma de que las cosas se actualizaran cuando fuera necesario.
Supongamos que el usuario hizo clic en un jugador llamado "Betty" para seleccionar que ella estaba 'adentro' para el juego. Es posible que deban suceder algunas cosas en la interfaz de usuario:
- Agregue 1 al conteo de reproducción
- Eliminar a Betty del grupo de jugadores 'Fuera'
- Agregue a Betty al grupo de jugadores 'In'
La aplicación también necesitaría actualizar los datos que representaban la interfaz de usuario. Lo que estaba muy interesado en evitar era esto:
playerName.addEventListener("click", playerToggle); function playerToggle() { if (inPlayers.includes(e.target.textContent)) { setPlayerOut(e.target.textContent); decrementPlayerCount(); } else { setPlayerIn(e.target.textContent); incrementPlayerCount(); } }
El objetivo era tener un flujo de datos elegante que actualizara lo que se necesitaba en el DOM cuando y si se cambiaban los datos centrales.
Con un patrón Observer, era posible enviar actualizaciones al estado y, por lo tanto, a la interfaz de usuario de manera bastante sucinta. Aquí hay un ejemplo, la función real utilizada para agregar un nuevo jugador a la 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 }); }
La parte relevante para el patrón Observer es el método io.notify
. Como eso nos muestra modificando los items
forman parte del estado de la aplicación, permítame mostrarle el observador que escuchó los cambios en los 'elementos':
io.addObserver({ props: ["items"], callback: function renderItems() { // Code that updates anything to do with items... } });
Tenemos un método de notificación que realiza cambios en los datos y luego Observadores de esos datos que responden cuando se actualizan las propiedades que les interesan.
Con este enfoque, la aplicación podría tener observables que busquen cambios en cualquier propiedad de los datos y ejecutar una función cada vez que ocurra un cambio.
Si está interesado en el patrón Observer por el que opté, lo describo con más detalle aquí.
Ahora había un enfoque para actualizar la interfaz de usuario de manera efectiva en función del estado. Aterciopelado. Sin embargo, esto todavía me dejó con dos problemas evidentes.
Una era cómo almacenar el estado a través de recargas/sesiones de página y el hecho de que, a pesar de que la interfaz de usuario funcionaba, visualmente no era muy similar a una aplicación. Por ejemplo, si se presiona un botón, la interfaz de usuario cambia instantáneamente en la pantalla. Simplemente no fue particularmente convincente.
Primero tratemos el lado del almacenamiento de las cosas.
Estado de ahorro
Mi principal interés desde el punto de vista del desarrollo al entrar en esto se centró en comprender cómo las interfaces de las aplicaciones se pueden construir y hacer interactivas con JavaScript. Cómo almacenar y recuperar datos de un servidor o abordar la autenticación de usuarios y los inicios de sesión estaba 'fuera del alcance'.
Por lo tanto, en lugar de conectarme a un servicio web para las necesidades de almacenamiento de datos, opté por mantener todos los datos en el cliente. Hay una serie de métodos de plataforma web para almacenar datos en un cliente. Opté por localStorage
.
La API para localStorage es increíblemente simple. Usted establece y obtiene datos como este:
// Set something localStorage.setItem("yourKey", "yourValue"); // Get something localStorage.getItem("yourKey");
LocalStorage tiene un método setItem
al que pasa dos cadenas. El primero es el nombre de la clave con la que desea almacenar los datos y la segunda cadena es la cadena real que desea almacenar. El método getItem
toma una cadena como argumento que le devuelve lo que esté almacenado bajo esa clave en localStorage. Bonito y sencillo.
Sin embargo, entre las razones para no usar localStorage está el hecho de que todo debe guardarse como una 'cadena'. Esto significa que no puede almacenar directamente algo como una matriz u objeto. Por ejemplo, intente ejecutar estos comandos en la consola de su navegador:
// Set something localStorage.setItem("myArray", [1, 2, 3, 4]); // Get something localStorage.getItem("myArray"); // Logs "1,2,3,4"
Aunque intentamos establecer el valor de 'myArray' como una matriz; cuando lo recuperamos, se había almacenado como una cadena (tenga en cuenta las comillas alrededor de '1,2,3,4').
Sin duda, puede almacenar objetos y matrices con localStorage, pero debe tener en cuenta que necesitan convertir cadenas de un lado a otro.
Entonces, para escribir datos de estado en localStorage, se escribieron en una cadena con el método JSON.stringify()
como este:
const storage = window.localStorage; storage.setItem("players", JSON.stringify(io.items));
Cuando era necesario recuperar los datos de localStorage, la cadena se convertía de nuevo en datos utilizables con el método JSON.parse()
como este:
const players = JSON.parse(storage.getItem("players"));
El uso localStorage
significaba que todo estaba en el cliente y eso significaba que no había servicios de terceros ni problemas de almacenamiento de datos.
Los datos ahora persistían en actualizaciones y sesiones: ¡bien! La mala noticia fue que localStorage no sobrevive a que un usuario vacíe los datos de su navegador. Cuando alguien hiciera eso, todos sus datos de Entrada/Salida se perderían. Esa es una deficiencia grave.
No es difícil apreciar que 'localStorage' probablemente no sea la mejor solución para aplicaciones 'adecuadas'. Además del problema de la cadena antes mencionado, también es lento para un trabajo serio, ya que bloquea el 'hilo principal'. Están llegando alternativas, como KV Storage, pero por ahora, tome nota mental para advertir su uso en función de la idoneidad.
A pesar de la fragilidad de guardar datos localmente en el dispositivo de un usuario, se resistió la conexión a un servicio o base de datos. En cambio, el problema se eludió al ofrecer una opción de 'cargar/guardar'. Esto permitiría a cualquier usuario de In/Out guardar sus datos como un archivo JSON que podría volver a cargarse en la aplicación si fuera necesario.
Esto funcionó bien en Android pero mucho menos elegantemente para iOS. En un iPhone, resultó en un derroche de texto en la pantalla como este:
Como se puede imaginar, no estaba solo para reprender a Apple a través de WebKit por esta deficiencia. El error relevante estaba aquí.
Al momento de escribir este error, este error tiene una solución y un parche, pero aún no se ha abierto camino en iOS Safari. Supuestamente, iOS13 lo arregla, pero está en Beta mientras escribo.
Entonces, para mi producto mínimo viable, eso fue abordado por el almacenamiento. ¡Ahora era el momento de intentar hacer las cosas más 'parecidas a una aplicación'!
Aplicación-I-Ness
Resulta que después de muchas discusiones con muchas personas, definir exactamente qué significa "me gusta la aplicación" es bastante difícil.
En última instancia, me decidí por 'similar a una aplicación' como sinónimo de una astucia visual que generalmente falta en la web. Cuando pienso en las aplicaciones que se sienten bien para usar, todas cuentan con movimiento. No es gratuito, sino un movimiento que se suma a la historia de sus acciones. Podrían ser las transiciones de página entre pantallas, la forma en que aparecen los menús. Es difícil de describir con palabras, pero la mayoría de nosotros lo sabemos cuando lo vemos.
La primera pieza de estilo visual necesaria fue cambiar los nombres de los jugadores hacia arriba o hacia abajo de 'Dentro' a 'Fuera' y viceversa cuando se seleccionaron. Hacer que un jugador se moviera instantáneamente de una sección a otra fue sencillo, pero ciertamente no 'como una aplicación'. Se espera que una animación cuando se haga clic en el nombre de un jugador enfatice el resultado de esa interacción: el jugador que se mueve de una categoría a otra.
Al igual que muchos de estos tipos de interacciones visuales, su aparente simplicidad desmiente la complejidad que implica lograr que funcione bien.
Se necesitaron algunas iteraciones para obtener el movimiento correcto, pero la lógica básica era esta:
- Una vez que se hace clic en un 'jugador', capture dónde está ese jugador, geométricamente, en la página;
- Mida a qué distancia de la parte superior del área debe moverse el jugador si sube ('Adentro') y qué tan lejos está la parte inferior, si baja ('Afuera');
- Si va hacia arriba, se debe dejar un espacio igual a la altura de la fila del jugador a medida que el jugador se mueve hacia arriba y los jugadores de arriba deben colapsar hacia abajo al mismo ritmo que el tiempo que tarda el jugador en viajar hacia arriba para aterrizar en el espacio. desocupado por los jugadores 'In' existentes (si los hay) que bajan;
- Si un jugador va a 'Fuera' y se mueve hacia abajo, todo lo demás debe moverse hacia arriba al espacio que queda y el jugador debe terminar debajo de cualquier jugador 'Fuera' actual.
¡Uf! Fue más complicado de lo que pensaba en inglés, ¡no importa JavaScript!
Hubo complejidades adicionales para considerar y probar, como las velocidades de transición. Al principio, no estaba claro si una velocidad de movimiento constante (por ejemplo, 20 px por 20 ms) o una duración constante del movimiento (por ejemplo, 0,2 s) se vería mejor. El primero era un poco más complicado ya que la velocidad debía calcularse "sobre la marcha" en función de la distancia que el jugador necesitaba viajar: una mayor distancia requería una transición más prolongada.
Sin embargo, resultó que una duración de transición constante no solo era más simple en el código; en realidad produjo un efecto más favorable. La diferencia fue sutil, pero este es el tipo de opciones que solo puede determinar una vez que haya visto ambas opciones.
De vez en cuando, mientras intentaba lograr este efecto, un error visual llamaba la atención, pero era imposible deconstruirlo en tiempo real. Descubrí que el mejor proceso de depuración era crear una grabación QuickTime de la animación y luego revisarla cuadro por cuadro. Invariablemente, esto reveló el problema más rápido que cualquier depuración basada en código.
Mirando el código ahora, puedo apreciar que en algo más allá de mi humilde aplicación, esta funcionalidad casi seguramente podría escribirse de manera más efectiva. Dado que la aplicación conocería el número de jugadores y conocería la altura fija de los listones, debería ser completamente posible realizar todos los cálculos de distancia solo en JavaScript, sin ninguna lectura de DOM.
No es que lo que se envió no funcione, es solo que no es el tipo de solución de código que mostraría en Internet. Oh espera.
Otras interacciones 'similares a la aplicación' fueron mucho más fáciles de lograr. En lugar de menús simplemente entrando y saliendo con algo tan simple como alternar una propiedad de visualización, se ganó mucho kilometraje simplemente exponiéndolos con un poco más de delicadeza. Todavía se activó simplemente, pero CSS estaba haciendo todo el trabajo 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; } }
Allí, cuando el data-evswitcher-showing="true"
se alternaba en un elemento principal, el menú se desvanecía, se transformaba de nuevo en su posición predeterminada y los eventos de puntero se volvían a habilitar para que el menú pudiera recibir clics.
Metodología de la hoja de estilo ECSS
Notará en ese código anterior que, desde el punto de vista de creación, las anulaciones de CSS se anidan dentro de un selector principal. Esa es la forma en que siempre prefiero escribir hojas de estilo de interfaz de usuario; una fuente única de verdad para cada selector y cualquier invalidación para ese selector encapsulado dentro de un solo conjunto de llaves. Es un patrón que requiere el uso de un procesador CSS (Sass, PostCSS, LESS, Stylus, et al), pero creo que es la única forma positiva de hacer uso de la funcionalidad de anidamiento.
Consolidé este enfoque en mi libro, CSS duradero y, a pesar de que hay una plétora de métodos más complicados disponibles para escribir CSS para elementos de interfaz, ECSS me ha servido bien a mí y a los grandes equipos de desarrollo con los que trabajo desde que el enfoque se documentó por primera vez. en 2014! Resultó igual de efectivo en este caso.
Parcializando el TypeScript
Incluso sin un procesador CSS o un lenguaje de superconjunto como Sass, CSS ha tenido la capacidad de importar uno o más archivos CSS a otro con la directiva de importación:
@import "other-file.css";
Cuando comencé con JavaScript me sorprendió que no hubiera un equivalente. Cada vez que los archivos de código son más largos que una pantalla o tan altos, siempre parece que dividirlos en partes más pequeñas sería beneficioso.
Otra ventaja de usar TypeScript fue que tiene una forma maravillosamente simple de dividir el código en archivos e importarlos cuando sea necesario.
Esta capacidad es anterior a los módulos nativos de JavaScript y fue una característica de gran comodidad. Cuando se compiló TypeScript, lo unió todo a un solo archivo JavaScript. Significaba que era posible dividir fácilmente el código de la aplicación en archivos parciales manejables para la creación e importarlos fácilmente al archivo principal. La parte superior del inout.ts
principal se veía así:
/// <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" />
Esta sencilla tarea de limpieza y organización ayudó enormemente.
Múltiples Eventos
Al principio, sentí que desde el punto de vista de la funcionalidad, un solo evento, como "Tuesday Night Football" sería suficiente. En ese escenario, si cargaba Entrada/Salida, simplemente agregaba/eliminaba o movía jugadores dentro o fuera y eso era todo. No había noción de eventos múltiples.
Rápidamente decidí que (incluso buscando un producto mínimo viable) esto sería una experiencia bastante limitada. ¿Qué pasaría si alguien organizara dos juegos en días diferentes, con una lista diferente de jugadores? ¿Seguramente In/Out podría/debería adaptarse a esa necesidad? No tomó demasiado tiempo remodelar los datos para que esto fuera posible y modificar los métodos necesarios para cargar en un conjunto diferente.
Al principio, el conjunto de datos predeterminado se parecía a esto:
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 } ];
Una matriz que contiene un objeto para cada jugador.
Después de tener en cuenta varios eventos, se modificó para que se vea así:
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 } ] } ];
Los nuevos datos eran una matriz con un objeto para cada evento. Luego, en cada evento había una propiedad EventData
que era una matriz con objetos de jugador como antes.
Tomó mucho más tiempo reconsiderar cómo la interfaz podría manejar mejor esta nueva capacidad.
Desde el principio, el diseño siempre había sido muy estéril. Teniendo en cuenta que se suponía que esto también era un ejercicio de diseño, no sentí que estaba siendo lo suficientemente valiente. Así que se agregó un poco más de estilo visual, comenzando con el encabezado. Esto es lo que me burlé en Sketch:
No iba a ganar premios, pero ciertamente fue más llamativo que donde comenzó.
Dejando de lado la estética, no fue hasta que alguien más lo señaló, que me di cuenta de que el gran ícono de más en el encabezado era muy confuso. La mayoría de la gente pensó que era una forma de agregar otro evento. En realidad, cambió a un modo 'Agregar jugador' con una transición elegante que le permitía escribir el nombre del jugador en el mismo lugar donde estaba el nombre del evento.
Este fue otro caso en el que los ojos frescos fueron invaluables. También fue una lección importante sobre dejar ir. La verdad honesta fue que me aferré a la transición del modo de entrada en el encabezado porque sentí que era genial e inteligente. Sin embargo, el hecho era que no estaba al servicio del diseño y, por lo tanto, de la aplicación en su conjunto.
Esto fue cambiado en la versión en vivo. En cambio, el encabezado solo se ocupa de los eventos, un escenario más común. Mientras tanto, agregar jugadores se realiza desde un submenú. Esto le da a la aplicación una jerarquía mucho más comprensible.
La otra lección aprendida aquí fue que, siempre que sea posible, es muy beneficioso obtener comentarios sinceros de los compañeros. Si son personas buenas y honestas, ¡no te dejarán pasar!
Resumen: Mi código apesta
Derecha. Hasta ahora, una pieza retrospectiva normal de aventuras tecnológicas; ¡Estas cosas cuestan diez centavos en Medium! La fórmula es más o menos así: el desarrollador detalla cómo derribó todos los obstáculos para lanzar una pieza de software finamente afinada en Internet y luego obtener una entrevista en Google o ser contratado en algún lugar. Sin embargo, la verdad del asunto es que yo era un novato en esta tontería de creación de aplicaciones, por lo que el código finalmente se envió como la aplicación 'terminada' apestaba hasta el cielo.
Por ejemplo, la implementación del patrón Observer utilizada funcionó muy bien. Era organizado y metódico al principio, pero ese enfoque 'se fue al sur' a medida que me desesperaba más por terminar las cosas. Al igual que una persona que hace dieta en serie, los viejos hábitos familiares volvieron a aparecer y la calidad del código disminuyó posteriormente.
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.