Una historia sobre cómo un equipo de freelancers escribe aplicaciones JavaScript de pila completa

El autor del material, cuya traducción publicamos hoy, dice que el repositorio de GitHub , en el que trabajó y varios otros freelancers, recibió, por diversas razones, unas 8.200 estrellas en 3 días. Este repositorio llegó en primer lugar en HackerNews y GitHub Trending, y 20,000 usuarios de Reddit votaron por él.



Este repositorio refleja la metodología para desarrollar aplicaciones de pila completa, a la que se dedica este artículo.

Antecedentes


Iba a escribir este material durante bastante tiempo. Creo que no puedo encontrar un momento mejor que este, cuando nuestro repositorio es muy popular.


No. 1 en tendencias de GitHub

Trabajo en un equipo de autónomos . Nuestros proyectos usan React / React Native, NodeJS y GraphQL. Este material está destinado a aquellos que desean aprender sobre cómo desarrollamos aplicaciones. Además, será útil para aquellos que se unirán a nuestro equipo en el futuro.

Ahora hablaré sobre los principios básicos que usamos al desarrollar proyectos.

Cuanto más simple, mejor.


"Cuanto más simple, mejor" es más fácil decirlo que hacerlo. La mayoría de los desarrolladores son conscientes de que la simplicidad es un principio importante en el desarrollo de software. Pero este principio no siempre es fácil de seguir. Si el código es simple, esto facilita el soporte del proyecto y simplifica el trabajo en equipo en este proyecto. Además, el cumplimiento de este principio ayuda a trabajar con el código escrito, por ejemplo, hace seis meses.

Aquí están los errores que he encontrado con respecto a este principio:

  • Deseo injustificado de cumplir el principio SECO. A veces copiar y pegar código es bastante normal. No es necesario abstraer cada 2 fragmentos de código que son algo similares entre sí. Yo mismo cometí este error. Todos, tal vez, lo cometieron. DRY es un buen enfoque de programación, pero elegir una abstracción fallida solo puede empeorar la situación y complicar la base del código. Si desea saber más sobre estas ideas, le recomiendo leer la Programación AHA de Kent A Dodds.
  • Negativa a utilizar las herramientas disponibles. Un ejemplo de este error es el uso de reduce lugar de map o filter . Por supuesto, el uso de reduce puede reproducir el comportamiento del map . Pero es probable que esto conduzca a un aumento en el tamaño del código y al hecho de que será más difícil para otras personas entender este código, dado que la "simplicidad del código" es un concepto subjetivo. A veces puede ser necesario usar solo reduce . Y si compara la velocidad de procesamiento del conjunto de datos usando el map y filter llamadas de filter combinadas en una cadena, y usando reduce , resulta que la segunda opción funciona más rápido. En la opción con reduce debe mirar el conjunto de valores una vez, no dos. Ante nosotros hay un debate sobre productividad y simplicidad. En la mayoría de los casos, preferiría la simplicidad y trataría de evitar la optimización prematura del código, es decir, elegiría un par de map / filter lugar de reduce . Y si resulta que la construcción del map y el filter convirtió en el cuello de botella del sistema, traduciría el código para reduce .

Muchas de las ideas que se discutirán a continuación tienen como objetivo hacer que la base del código sea lo más simple posible y mantenerla en este estado.

Mantenga entidades similares cercanas entre sí


Este principio, el "principio de colocación", se aplica a muchas partes de la aplicación. Esta es la estructura de las carpetas en las que se almacena el código del cliente y el servidor, este es el almacenamiento del código del proyecto en un repositorio, esta es también la decisión sobre qué código está en un determinado archivo.

▍ Repositorio


Se recomienda que mantenga el código del cliente y el servidor en el mismo repositorio. Es simple No compliques lo que no es necesario complicar. Con este enfoque, es conveniente organizar un trabajo de equipo coordinado en un proyecto. Trabajé en proyectos que usaban varios repositorios para almacenar materiales. Esto no es un desastre, pero los mono-repositorios hacen la vida mucho más fácil.

▍ Estructura del proyecto de la parte cliente de la aplicación.


Estamos escribiendo aplicaciones de pila completa. Es decir, tanto el código del cliente como el código del servidor. La estructura de carpetas de un proyecto de cliente típico proporciona directorios separados para componentes, contenedores, acciones, reductores y rutas.

Las acciones y reductores están presentes en aquellos proyectos que usan Redux. Me esfuerzo por prescindir de esta biblioteca. Estoy seguro de que hay proyectos de alta calidad que utilizan la misma estructura. Algunos de mis proyectos tienen carpetas separadas para componentes y contenedores. Una carpeta de componentes puede almacenar algo así como archivos con código para entidades como BlogPost y Profile . En la carpeta del contenedor hay archivos que almacenan el código del contenedor BlogPostContainer y BlogPostContainer . El contenedor recibe datos del servidor y los pasa al componente secundario "estúpido", cuya tarea es mostrar estos datos en la pantalla.

Esta es una estructura de trabajo. Es al menos homogéneo, y esto es muy importante. Esto lleva al hecho de que el desarrollador, que se unió al trabajo en el proyecto, comprende rápidamente lo que está sucediendo en él y el papel que desempeñan sus partes individuales. La desventaja de este enfoque, debido a que recientemente he estado tratando de no usarlo, es que obliga al programador a moverse constantemente por la base del código. Por ejemplo, las BlogPostContainer y BlogPostContainer no tienen nada en común, pero sus archivos están uno al lado del otro y lejos de los archivos en los que realmente se usan.

Desde hace algún tiempo, me he esforzado por colocar archivos cuyo contenido se planea compartir en las mismas carpetas. Este enfoque para la estructuración de proyectos se basa en la agrupación de archivos en función de sus capacidades. Gracias a este enfoque, puede simplificar enormemente su vida si, por ejemplo, coloca el componente principal y su componente secundario "estúpido" en la misma carpeta.

Usualmente usamos la carpeta de routes / screens y la carpeta de components . La carpeta de componentes generalmente almacena código para elementos como Button o Input . Este código se puede usar en cualquier página de la aplicación. Cada carpeta en la carpeta para rutas es una página de aplicación separada. Al mismo tiempo, los archivos con el código de componente y el código lógico de la aplicación relacionados con esta ruta se encuentran en la misma carpeta. Y el código de los componentes que se utilizan en varias páginas cae en la carpeta de components .

Dentro de la carpeta de ruta, puede crear carpetas adicionales en las que se agrupa el código responsable de la formación de diferentes partes de la página. Esto tiene sentido en los casos en que la ruta está representada por una gran cantidad de código. Aquí, sin embargo, me gustaría advertir al lector que no vale la pena crear estructuras a partir de carpetas con un nivel muy alto de anidamiento. Esto complica el movimiento del proyecto. Las estructuras de carpetas anidadas profundas son uno de los signos de complicar demasiado un proyecto. Cabe señalar que el uso de herramientas especializadas, como los comandos de búsqueda, le brinda al programador herramientas convenientes para trabajar con el código del proyecto y para encontrar lo que necesita. Pero la estructura de archivos del proyecto también afecta la usabilidad del mismo.

Al estructurar el código del proyecto, puede agrupar archivos en función de la ruta, sino de las capacidades del proyecto implementadas por estos archivos. En mi caso, este enfoque se muestra perfectamente en proyectos de una sola página que implementan muchas características en su única página. Pero debe tenerse en cuenta que agrupar los materiales del proyecto por ruta es más fácil. Este enfoque no requiere esfuerzos mentales especiales para tomar decisiones sobre qué entidades deben colocarse una al lado de la otra, y para buscar algo.

Si seguimos el camino de agrupar el código, podemos decidir que el código de los contenedores y componentes se colocará justificadamente en el mismo archivo. Y puede ir aún más lejos: coloque el código de dos componentes en un archivo. Supongo que puede estar pensando ahora que recomendar tales cosas es realmente una blasfemia. Pero en realidad, todo está lejos de ser tan malo. De hecho, este enfoque está totalmente justificado. Y si usa React hooks, o código generado (o ambos), recomendaría este enfoque.

De hecho, la cuestión de cómo descomponer el código en archivos no es de suma importancia. La verdadera pregunta es por qué podría necesitar dividir los componentes en inteligentes y estúpidos. ¿Cuáles son los beneficios de esta separación? Hay varias respuestas a esta pregunta:

  1. Las aplicaciones creadas de esta manera son más fáciles de probar.
  2. El desarrollo de tales aplicaciones facilita el uso de herramientas como Storybook.
  3. Los componentes estúpidos se pueden usar con muchos componentes inteligentes diferentes (y viceversa).
  4. Los componentes inteligentes se pueden usar en diferentes plataformas (por ejemplo, en las plataformas React y React Native).

Todos estos son argumentos reales a favor de dividir los componentes en "inteligentes" y "estúpidos", pero no son aplicables a todas las situaciones. Por ejemplo, a menudo usamos Apollo Client con ganchos al crear proyectos. Para probar dichos proyectos, puede crear simulacros de respuesta Apollo o simulacros de gancho. Lo mismo ocurre con el libro de cuentos. Si hablamos de mezclar y compartir componentes "inteligentes" y "estúpidos", entonces, de hecho, nunca he conocido esto en la práctica. Con respecto al uso multiplataforma del código, hubo un proyecto en el que iba a hacer algo similar, pero nunca lo hice. Se suponía que era el mono-repositorio de Lerna. En estos días, en lugar de este enfoque, puede optar por React Native Web.

Como resultado, podemos decir que en la separación de componentes en "inteligente" y "tonto" hay un cierto significado. Este es un concepto importante que vale la pena conocer. Pero a menudo, no necesita preocuparse mucho por eso, especialmente teniendo en cuenta la reciente aparición de ganchos React.

El punto fuerte de combinar las capacidades de los componentes "inteligentes" y "tontos" en una entidad es que acelera el desarrollo y simplifica la estructura del código.

Además, si surge tal necesidad, un componente siempre se puede dividir en dos componentes separados: "inteligente" y "estúpido".

Estilización


Utilizamos componentes de estilo / emoción para aplicaciones de estilo. Siempre existe la tentación de separar estilos en un archivo separado. He visto a algunos desarrolladores hacer esto. Pero, después de probar ambos enfoques, al final no pude encontrar una razón para mover los estilos a un archivo separado. Como en el caso de muchas otras cosas, de las que estamos hablando aquí, un desarrollador puede facilitarle la vida combinando los estilos y componentes con los que se relacionan en un archivo.

▍ Estructura del proyecto de la parte del servidor de la aplicación


Todo lo anterior es cierto con respecto a la estructuración del código del lado del servidor de la aplicación. Una estructura típica que personalmente trato de evitar podría verse así:

 src │ app.js #     └───api #   Express      └───config #      └───jobs #    agenda.js └───loaders #     └───models #    └───services # - └───subscribers #      └───types #    (d.ts)  Typescript 

Usualmente usamos GraphQL en nuestros proyectos. Por lo tanto, usan archivos que almacenan modelos, servicios y reconocedores. En lugar de dispersarlos en diferentes lugares del proyecto, los recopilo en una carpeta. Muy a menudo, estos archivos se compartirán y será más fácil trabajar con ellos si se almacenan en la misma carpeta.

No sobrescriba las definiciones de tipo muchas veces


Utilizamos muchas soluciones en nuestros proyectos que de alguna manera están relacionadas con los tipos de datos. Estos son TypeScript, GraphQL, esquemas de bases de datos y, a veces, MobX. Como resultado, puede resultar que los tipos para las mismas entidades se describan 3-4 veces. Cosas como esta deberían evitarse. Debemos esforzarnos por utilizar herramientas que generen automáticamente descripciones de tipo.

En el servidor, se puede utilizar una combinación de TypeORM / Typegoose y TypeGraphQL para este propósito. Esto es suficiente para describir todos los tipos utilizados. TypeORM / Typegoose le permite describir el esquema de la base de datos y los tipos de TypeScript correspondientes. TypeGraphQL ayudará a crear los tipos de GraphQL y TypeScript.

Aquí hay un ejemplo de determinación de los tipos de TypeORM (MongoDB) y TypeGraphQL en un archivo:

 import { Field, ObjectType, ID } from 'type-graphql' import { Entity, ObjectIdColumn, ObjectID, Column, CreateDateColumn, UpdateDateColumn, } from 'typeorm' @ObjectType() @Entity() export default class Policy { @Field(type => ID) @ObjectIdColumn() _id: ObjectID @Field() @CreateDateColumn({ type: 'timestamp' }) createdAt: Date @Field({ nullable: true }) @UpdateDateColumn({ type: 'timestamp', nullable: true }) updatedAt?: Date @Field() @Column() name: string @Field() @Column() version: number } 

GraphQL Code Generator también puede generar muchos tipos diferentes. Utilizamos esta herramienta para crear tipos de TypeScript en el cliente, así como los ganchos React que acceden al servidor.

Si usa MobX para controlar el estado de la aplicación, luego, utilizando un par de líneas de código, puede obtener tipos de TS generados automáticamente. Si también usa GraphQL, debería echar un vistazo al nuevo paquete: MST-GQL , que genera un árbol de estado a partir del esquema GQL.

El uso conjunto de estas herramientas le ahorrará reescribir grandes cantidades de código y lo ayudará a evitar errores comunes.

Otras soluciones, como Prisma , Hasura y AWS AppSync , también pueden ayudar a evitar declaraciones de tipo duplicadas. El uso de tales herramientas, por supuesto, tiene sus ventajas y desventajas. En los proyectos que creamos, tales herramientas no siempre se usan, ya que necesitamos implementar el código en nuestros propios servidores de organizaciones.

Siempre que sea posible, recurra a los medios de generación automática de código


Si observa el código que crea sin utilizar las herramientas anteriores para generar código automáticamente, resulta que los programadores tienen que escribir constantemente lo mismo. El consejo principal que puedo dar sobre esto es que necesitas crear fragmentos para todo lo que usas a menudo. Si a menudo ingresa el comando console.log , cree un fragmento como cl , que se convierte automáticamente en console.log() . Si no haces esto y me pides que te ayude con la depuración de código, me molestará mucho.

Hay muchos paquetes con fragmentos, pero es fácil crear sus propios fragmentos. Por ejemplo, usando el generador de fragmentos .

Aquí está el código que me permite agregar algunos de mis fragmentos favoritos a VS Code:

 { "Export default": {   "scope": "javascript,typescript,javascriptreact,typescriptreact",   "prefix": "eid",   "body": [     "export { default } from './${TM_DIRECTORY/.*[\\/](.*)$$/$1/}'",     "$2"   ],   "description": "Import and export default in a single line" }, "Filename": {   "prefix": "fn",   "body": ["${TM_FILENAME_BASE}"],   "description": "Print filename" }, "Import emotion styled": {   "prefix": "imes",   "body": ["import styled from '@emotion/styled'"],   "description": "Import Emotion js as styled" }, "Import emotion css only": {   "prefix": "imec",   "body": ["import { css } from '@emotion/styled'"],   "description": "Import Emotion css only" }, "Import emotion styled and css only": {   "prefix": "imesc",   "body": ["import styled, { css } from ''@emotion/styled'"],   "description": "Import Emotion js and css" }, "Styled component": {   "prefix": "sc",   "body": ["const ${1} = styled.${2}`", "  ${3}", "`"],   "description": "Import Emotion js and css" }, "TypeScript React Function Component": {   "prefix": "rfc",   "body": [     "import React from 'react'",     "",     "interface ${1:ComponentName}Props {",     "}",     "",     "const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = props => {",     " return (",     " <div>",     " ${1:ComponentName}",     " </div>",     " )",     "}",     "",     "export default ${1:ComponentName}",     ""   ],   "description": "TypeScript React Function Component" } } 

Además de los fragmentos, los generadores de código pueden ayudar a ahorrar tiempo. Puedes crearlos tú mismo. Me gusta usar plop para esto.

Angular tiene sus propios generadores de código integrados. Con las herramientas de línea de comandos, puede crear un nuevo componente, que consta de 4 archivos, que presenta todo lo que puede esperar encontrar en el componente. Es una pena que React no tenga una característica estándar, pero se puede crear algo similar de forma independiente utilizando plop. Si cada nuevo componente que cree se presenta en forma de una carpeta que contiene un archivo con el código del componente, un archivo de prueba y un archivo de Storybook, el generador ayudará a crear todo esto con un solo comando. Esto en muchos casos facilita enormemente la vida del desarrollador. Por ejemplo, al agregar una nueva característica al servidor, simplemente ejecute un comando en la línea de comandos. Después de eso, los archivos de la entidad, servicios y reconocedores se crearán automáticamente con todas las estructuras básicas necesarias.

Otra fortaleza de los generadores de código es que contribuyen a la uniformidad en el desarrollo del equipo. Si todos usan el mismo generador plop, entonces el código será muy uniforme para todos.

Formato de código automático


Formatear el código es una tarea simple, pero, desafortunadamente, no siempre se resuelve correctamente. No pierda el tiempo alineando manualmente el código o insertando punto y coma en él. Use Prettier para formatear automáticamente el código al confirmar.

Resumen


En este artículo, le conté algunas cosas que aprendimos a lo largo de los años de trabajo, durante los años de prueba y error. Existen muchos enfoques para estructurar la base de código de los proyectos. Pero entre ellos no hay nadie que pueda llamarse el único correcto.

Lo más importante que quería transmitirles es que el programador debe esforzarse por la simplicidad de organizar proyectos, por su homogeneidad, por usar una estructura comprensible con la que sea fácil trabajar. Esto simplifica los proyectos de desarrollo de equipo.

Estimados lectores! ¿Qué opina de las ideas para desarrollar aplicaciones JavaScript de pila completa descritas en este artículo?



Source: https://habr.com/ru/post/456340/


All Articles