¿Responsivo o receptivo? Estructura de componentes de análisis de reacción



En este artículo, entenderemos la complejidad de escribir componentes adaptativos, hablaremos sobre la división del código, consideraremos varias formas de organizar la estructura del código, evaluaremos sus ventajas y desventajas e intentaremos elegir el mejor (pero esto no es exacto).

Primero, tratemos con la terminología. A menudo escuchamos los términos adaptativo y receptivo . ¿Qué quieren decir? ¿Cuál es la diferencia? ¿Cómo se relaciona esto con nuestros componentes?

Adaptativo (adaptativo) es un complejo de interfaces visuales creadas para tamaños de pantalla específicos. Responsive (Responsive) es una interfaz única que se adapta a cualquier tamaño de pantalla.

Además, cuando la interfaz se descompone en pequeños fragmentos, la diferencia entre adaptativa y sensible se vuelve cada vez más borrosa, hasta que desaparece por completo.

Al desarrollar diseños, nuestros diseñadores, así como los desarrolladores, a menudo no comparten estos conceptos y combinan lógica adaptativa y receptiva.

Además, llamaré a los componentes que contienen lógica adaptativa y receptiva como simplemente adaptativos . En primer lugar, porque me gusta más esta palabra que "receptivo" o, perdóname, "receptivo". Y en segundo lugar, me parece más común.

Me centraré en dos áreas de interfaces de pantalla: móvil y de escritorio. Por pantalla móvil queremos decir ancho, por ejemplo, ≤ 991 píxeles(el número en sí mismo no es importante, es solo una constante, que depende de su sistema de diseño y su aplicación), y debajo de la pantalla del escritorio, el ancho es mayor que el umbral seleccionado. Extrañaré intencionalmente pantallas para tabletas y monitores de pantalla ancha, porque, en primer lugar, no todos los necesitan, y en segundo lugar, será más fácil decirlo de esta manera. Pero los patrones de los que vamos a hablar se expanden igualmente para cualquier número de "mapeos".

Además, casi no hablaré sobre CSS , principalmente hablaremos sobre la lógica de componentes.

Frontend @youla


Hablaré brevemente sobre nuestra pila en Yulia para que quede claro en qué condiciones creamos nuestros componentes. Usamos React / Redux , trabajamos en monorep, usamos Typecript y escribimos CSS en componentes con estilo . Como ejemplo, veamos nuestros tres paquetes (los paquetes en el concepto de monoreps son paquetes NPM que están interconectados, que pueden ser aplicaciones, bibliotecas, utilidades o componentes separados; usted elige el grado de descomposición). Analizaremos dos aplicaciones y una biblioteca de IU.

@ youla / ui- biblioteca de componentes. Son utilizados no solo por nosotros, sino también por otros equipos que necesitan interfaces "Yulian". La biblioteca tiene muchas cosas, comenzando con botones y campos de entrada, y terminando, por ejemplo, con un encabezado o un formulario de autorización (más precisamente, su parte de la interfaz de usuario). Consideramos que esta biblioteca es una dependencia externa de nuestra aplicación.

@ youla-web / app-Classified : la aplicación responsable de las secciones del catálogo / producto / autorización. De acuerdo con los requisitos comerciales, todas las interfaces aquí deben ser adaptativas .

@ youla-web / app-b2b es la aplicación responsable de las secciones de su cuenta personal para usuarios profesionales. Las interfaces de esta aplicación son exclusivamente de escritorio .

Además consideraremos escribir componentes adaptativos usando el ejemplo de estos paquetes. Pero primero debes lidiar con eso isMobile.

Definición de movilidad isMobile && <Component />


import React from 'react'

const App = (props) => {
 const { isMobile } = props

 return (
   <Layout>
     {isMobile && <HeaderMobile />}
     <Content />
     <Footer />
   </Layout>
 )
}

Antes de comenzar a escribir componentes adaptativos, debe aprender a definir "movilidad". Hay muchas formas de implementar la definición de movilidad. Quiero detenerme en algunos puntos clave.

Determinación de la movilidad por ancho de pantalla y agente de usuario


La mayoría de ustedes saben bien cómo implementar ambas opciones, pero repasemos brevemente los puntos principales nuevamente.

Cuando se trabaja con el ancho de la pantalla, se acostumbra establecer puntos de límite, después de lo cual la aplicación debe comportarse como móvil o de escritorio. El procedimiento es el siguiente:

  1. Cree constantes con puntos de límite y guárdelos en el asunto (si su solución CSS lo permite). Los valores en sí pueden ser lo que sus diseñadores encuentran más apropiados para su sistema de IU .
  2. Guardamos el tamaño de pantalla actual en una fuente de datos redux / mobx / context / any . En cualquier lugar, si solo los componentes y, preferiblemente, la lógica de la aplicación tuvieran acceso a estos datos.
  3. Nos suscribimos al evento de cambio de tamaño y actualizamos el valor del ancho de la pantalla al que activará la cadena de actualizaciones del árbol de componentes.
  4. Creamos funciones auxiliares simples que, utilizando anchos de pantalla y constantes, calculan el estado actual ( isMobile,isDesktop ).

Aquí está el pseudocódigo que implementa este modelo de trabajo:

const breakpoints = {
 mobile: 991
}

export const state = {
 ui: {
   width: null
 }
}

const handleSubscribe = () => {
 state.ui.width = window.innerWidth
}

export const onSubscribe = () => {
 window.addEventListener('resize', handleSubscribe)
}

export const offSubscribe = () =>
 window.removeEventListener('resize', handleSubscribe)

export const getIsMobile = (state: any) => {
 if (state.ui.width <= breakpoints.mobile) {
   return true
 }

 return false
}

export const getIsDesktop = (state) => !getIsMobile(state)

export const App = () => {
 React.useEffect(() => {
   onSubscribe()

   return () => offSubscribe()
 }, [])

 return <MyComponentMounted />
}

const MyComponent = (props) => {
 const { isMobile } = props

 return isMobile ? <MobileComponent /> : <DesktopComponent />
}

export const MyComponentMounted = anyHocToConnectComponentWithState(
 (state) => ({
   isMobile: getIsMobile(state)
 })
)(MyComponent)

Cuando la pantalla cambia, los valores propspara el componente se actualizarán y se redibujarán correctamente. Hay muchas bibliotecas que implementan esta funcionalidad. Será más conveniente para alguien usar una solución preparada, por ejemplo, react-media , react-responsive , etc., y para alguien es más fácil escribir la suya .

A diferencia del tamaño de la pantalla, user-agentno puede cambiar dinámicamente mientras la aplicación se está ejecutando (estrictamente hablando, tal vez a través de las herramientas del desarrollador, pero este no es un escenario de usuario). En este caso, no necesitamos usar una lógica compleja con el almacenamiento del valor y el recuento, solo analice la cadena una vez window.navigator.userAgent,para guardar el valor y listo. Hay un montón de bibliotecas para ayudarlo con esto, por ejemplo, detección móvil, react-device-detect , etc.

El enfoque es user-agentmás simple, pero solo usarlo no es suficiente. Cualquiera que haya desarrollado seriamente interfaces adaptativas sabe sobre el "giro mágico" de los iPads y dispositivos similares, que en la posición vertical entran en la definición de móvil, y en el escritorio horizontal, pero al mismo tiempo tienen un user-agentdispositivo móvil. También vale la pena señalar que en una aplicación totalmente adaptativa / receptiva, user-agent es imposible determinar la movilidad basada en información sobre uno solo si el usuario usa, por ejemplo, un navegador de escritorio, pero comprime la ventana al tamaño "móvil".

Además, no descuide la información sobre user-agent. Muy a menudo en el código puede encontrar constantes como isSafari,isIEetc. que manejan las "características" de estos dispositivos y navegadores. Es mejor combinar ambos enfoques.

En nuestra base de código, utilizamos una constante isCheesySafarique, como su nombre lo indica, define la pertenencia user-agenta la familia del navegador Safari. Pero además de esto, tenemos una constante isSuperCheesySafari, lo que implica un Safari móvil correspondiente a la versión 11 de iOS, que se ha hecho famoso por muchos errores como este: https://hackernoon.com/how-to-fix-the-ios-11-input-element -in-fixed-modals-bug-aaf66c7ba3f8 .

export const isMobileUA = (() => magicParser(window.navigator.userAgent))()

import isMobileUA from './isMobileUA'

const MyComponent = (props) => {
 const { isMobile } = props

 return (isMobile || isMobileUA) ? <MobileComponent /> : <DesktopComponent />
}

¿Qué pasa con las consultas de los medios? Sí, de hecho, CSS tiene herramientas integradas para trabajar con adaptabilidad: consultas de medios y su método analógico window.matchMedia. Se pueden usar, pero la lógica de "actualizar" los componentes al cambiar el tamaño aún tendrá que implementarse. Aunque para mí personalmente, usar la sintaxis de las consultas de medios en lugar de las operaciones de comparación habituales en JS para la lógica y los componentes de la aplicación es una ventaja dudosa.

Organización de la estructura de los componentes.


Hemos descubierto la definición de movilidad, ahora reflexionemos sobre el uso de los datos que hemos obtenido y la organización de la estructura del código del componente. En nuestro código, como regla, prevalecen dos tipos de componentes.

El primer tipo son los componentes, afilados debajo del teléfono celular o debajo del escritorio. En dichos componentes, los nombres a menudo contienen las palabras Mobile / Desktop, que indican claramente que el componente pertenece a uno de los tipos. Como un ejemplo de dicho componente puede considerarse <MobileList />desde @youla/ui.

import { Panel, Cell, Content, afterBorder } from './styled'
import Group from './Group'
import Button, { IMobileListButtonProps } from './Button'
import ContentOrButton, { IMobileListContentOrButton } from './ContentOrButton'
import Action, { IMobileListActionProps } from './Action'

export default { Panel, Group, Cell, Content, Button, ContentOrButton, Action }
export {
 afterBorder,
 IMobileListButtonProps,
 IMobileListContentOrButton,
 IMobileListActionProps
}

Este componente, además de la exportación muy detallada, es una lista con datos, separadores, agrupaciones por bloques, etc. Nuestros diseñadores son muy aficionados a este componente y en todas partes lo usan en las interfaces de Ula. Por ejemplo, en la descripción en la página del producto o en nuestra nueva funcionalidad de tarifas:


Y en N lugares alrededor del sitio. También tenemos un componente similar <DesktopList />que implementa esta funcionalidad de lista para la resolución de escritorio.

Los componentes del segundo tipo contienen la lógica tanto del escritorio como del móvil. Veamos una versión simplificada de la representación de nuestro componente <HeaderBoard />, que se encuentra en @ youla / app-Classified.

Hemos encontrado para mí es muy conveniente para hacer todos los componentes de estilo para un componente en un solo archivo y la importación en virtud de los espacios de nombres S, para separar el código de los otros componentes: import * as S from ‘./styled’. En consecuencia, "S" es un objeto cuyas claves son los nombres de los componentes con estilo, y los valores son los propios componentes.

 return (
   <HeaderWrapper>
     <Logo />
     {isMobile && <S.Arrow />}
     <S.Wraper isMobile={isMobile}>
       <Video src={bgVideo} />
       {!isMobile && <Header>{headerContent}</Header>}
       <S.WaveWrapper />
     </S.Wraper>
     {isMobile && <S.MobileHeader>{headerContent}</S.MobileHeader>}
     <Info link={link} />
     <PaintingInfo isMobile={isMobile} />
     {isMobile ? <CardsMobile /> : <CardsDesktop />}
     {isMobile ? <UserNavigation /> : <UserInfoModal />}
   </HeaderWrapper>
 )

Aquí isMobile, es la dependencia del componente, sobre la base de la cual el componente mismo decidirá qué interfaz renderizar.

Para una escala más conveniente, a menudo usamos el patrón de inversión de control en las partes reutilizadas de nuestro código, pero tenga cuidado de no sobrecargar las abstracciones de nivel superior con lógica innecesaria.

Ahora vamos a abstraernos un poco de los componentes "Yulian" y echemos un vistazo más de cerca a estos dos componentes:

  • <ComponentA />- Con una estricta separación de la lógica de escritorio y móvil.
  • <ComponentB />- combinado.

<Componente A /> vs <Componente B />


Estructura de carpetas y archivo index.ts raíz :

./ComponentA
- ComponentA.tsx
- ComponentADesktop.tsx
- ComponentAMobile.tsx
- index.ts
- styled.desktop.ts
- styled.mobile.ts


import ComponentA  from './ComponentA'
import ComponentAMobile  from './ComponentAMobile'
import ComponentADesktop  from './ComponentADesktop'

export default {
 ComponentACombined: ComponentA,
 ComponentAMobile,
 ComponentADesktop
}

Gracias a la nueva tecnología web-shaking webpack (o al uso de cualquier otro recopilador), puede descartar los módulos no utilizados ( ComponentADesktop, ComponentACombined), incluso con esta reexportación a través del archivo raíz:

import ComponentA from ‘@youla/ui’
<ComponentA.ComponentAMobile />

Solo el código del archivo ./ComponentAMobile ingresa al paquete final.

El componente <ComponentA />contiene importaciones asincrónicas utilizando una React.Lazyversión específica del componente <ComponentAMobile /> || <ComponentADesktop />para una situación específica.

En Yule intentamos adherirnos al patrón de un único punto de entrada en el componente a través del archivo de índice. Esto facilita la búsqueda y refactorización de componentes. Si el contenido del componente no se reexporta a través del archivo raíz, entonces podemos editarlo con seguridad, ya que sabemos que no se usa fuera del contexto de este componente. Bueno, Typecript se cubrirá en caso de necesidad. La carpeta con el componente tiene su propia "interfaz": exporta a nivel de módulo en el archivo raíz, y sus detalles de implementación no se revelan. Como resultado, al refactorizar, no puede temer guardar la interfaz.

import React from 'react'

const ComponentADesktopLazy = React.lazy(() => import('./ComponentADesktop'))
const ComponentAMobileLazy = React.lazy(() => import('./ComponentAMobile'))

const ComponentA = (props) => {
 const { isMobile } = props

//    

 return (
   <React.Suspense fallback={props.fallback}>
     {isMobile ? (
       <ComponentAMobileLazy {...props} />
     ) : (
       <ComponentADesktopLazy {...props} />
     )}
   </React.Suspense>
 )
}

export default ComponentA

Además, el componente <ComponentADesktop />contiene la importación de componentes de escritorio:

import React from 'react'

import { DesktopList, UserAuthDesktop, UserInfo } from '@youla/ui'

import Banner from '../Banner'

import * as S from './styled.desktop'

const ComponentADesktop = (props) => {
 const { user, items } = props

 return (
   <S.Wrapper>
     <S.Main>
       <Banner />
       <DesktopList items={items} />
     </S.Main>
     <S.SideBar>
       <UserAuthDesktop user={user} />
       <UserInfo user={user} />
     </S.SideBar>
   </S.Wrapper>
 )
}

export default ComponentADesktop

Un componente <ComponentAMobile />contiene la importación de componentes móviles:

import React from 'react'

import { MobileList, MobileTabs, UserAuthMobile } from '@youla/ui'

import * as S from './styled.mobile'

const ComponentAMobile = (props) => {
 const { user, items, tabs } = props

 return (
   <S.Wrapper>
     <S.Main>
       <UserAuthMobile user={user} />
       <MobileList items={items} />
       <MobileTabs tabs={tabs} />
     </S.Main>
   </S.Wrapper>
 )
}

export default ComponentAMobile

El componente es <ComponentA />adaptable: según el indicador, isMobilepuede decidir qué versión dibujar, solo puede descargar los archivos necesarios de forma asincrónica, es decir, las versiones móvil y de escritorio se pueden usar por separado.

Veamos el componente ahora <ComponentB />. En él, no descompondremos profundamente la lógica móvil y de escritorio, dejaremos todas las condiciones dentro del marco de una función. Del mismo modo, no separaremos los componentes de los estilos.

Aquí está la estructura de carpetas. El archivo root index.ts simplemente reexporta ./ComponentB:

./ComponentB
- ComponentB.tsx
- index.ts
- styled.ts


export { default } from './ComponentB'

El archivo ./ComponentB con el componente en sí:


import React from 'react'

import {
 DesktopList,
 UserAuthDesktop,
 UserInfo,
 MobileList,
 MobileTabs,
 UserAuthMobile
} from '@youla/ui'

import * as S from './styled'

const ComponentB = (props) => {
 const { user, items, tabs, isMobile } = props

 if (isMobile) {
   return (
     <S.Wrapper isMobile={isMobile}>
       <S.Main isMobile={isMobile}>
         <UserAuthMobile user={user} />
         <MobileList items={items} />
         <MobileTabs tabs={tabs} />
       </S.Main>
     </S.Wrapper>
   )
 }

 return (
   <S.Wrapper>
     <S.Main>
       <Banner />
       <DesktopList items={items} />
     </S.Main>
     <S.SideBar>
       <UserAuthDesktop user={user} />
       <UserInfo user={user} />
     </S.SideBar>
   </S.Wrapper>
 )
}

export default ComponentB

Tratemos de estimar las ventajas y desventajas de estos componentes.



Total de tres pros y contras argumentos extraídos del dedo para cada uno de ellos. Sí, noté que algunos criterios se mencionan inmediatamente tanto en ventajas como en desventajas: esto se hizo a propósito, todos los eliminarán del grupo equivocado.

Nuestra experiencia con @youla


En nuestra biblioteca de componentes @ youla / ui intentamos no mezclar componentes de escritorio y móviles, ya que esta es una dependencia externa para muchos de nuestros paquetes y otros. El ciclo de vida de estos componentes es lo más largo posible, quiero mantenerlos lo más delgados y livianos posible.

Hay dos puntos importantes a tener en cuenta .

En primer lugar, cuanto más pequeño sea el archivo JS ensamblado, más rápido se entregará al usuario, esto es obvio y todos lo saben. Pero esta característica es importante solo para la primera descarga del archivo, durante las visitas repetidas, el archivo se entregará desde el caché y no habrá ningún problema de entrega de código.

Aquí pasamos a la razón número dos, que pronto puede convertirse, o ya se ha convertido, en el principal problema de las grandes aplicaciones web. Muchos ya lo han adivinado: sí, estamos hablando de la duración del análisis.

Los motores modernos como V8 pueden almacenar en caché y el resultado del análisis, pero hasta ahora no funciona de manera muy eficiente. Eddie Osmani tiene un excelente artículo sobre este tema: https://v8.dev/blog/cost-of-javascript-2019 . También puede suscribirse al blog V8: https://twitter.com/v8js .

Es la duración del análisis lo que reduciremos significativamente, esto es especialmente importante para dispositivos móviles con procesadores débiles .

En los paquetes de aplicación @ youla-web / app- *, ​​el desarrollo está más "orientado a los negocios". Y en aras de la velocidad / simplicidad / preferencias personales, se elige la decisión que el desarrollador mismo considere la más correcta en esta situación. A menudo sucede que cuando se desarrollan funciones MVP pequeñas, es mejor escribir primero una versión más simple y rápida (<ComponentB />), en dicho componente hay la mitad del número de líneas. Y, como sabemos, cuanto más código, más errores.

Después de verificar la relevancia de la característica, será posible reemplazar el componente con una versión más optimizada y productiva <ComponentA />, si es necesario.

También le aconsejo que eche un vistazo al componente. Si las IU de las versiones móvil y de escritorio son muy diferentes, entonces quizás deberían estar separadas, manteniendo una lógica común en un solo lugar. Esto le permitirá deshacerse del dolor al escribir CSS complejos, problemas con errores en una de las pantallas al refactorizar o cambiar otra. Y viceversa, si la interfaz de usuario está lo más cerca posible, ¿por qué hacer el trabajo extra?

Conclusión


Para resumir. Entendimos la terminología de la interfaz adaptativa / sensible, examinamos varias formas de determinar la movilidad y varias opciones para organizar la estructura de código del componente adaptativo, e identificamos las ventajas y desventajas de cada una. Seguramente ya conocías mucho de lo anterior, pero la repetición es la mejor manera de consolidar. Espero que hayas aprendido algo nuevo por ti mismo. La próxima vez queremos publicar una colección de recomendaciones para escribir aplicaciones web progresivas, con consejos para organizar, reutilizar y mantener el código.

All Articles