Responsivo ou responsivo? Análise da estrutura do componente React



Neste artigo, entenderemos a complexidade de escrever componentes adaptáveis, falaremos sobre divisão de código, consideraremos várias maneiras de organizar a estrutura de código, avaliaremos suas vantagens e desvantagens e tentaremos escolher a melhor (mas isso não é exato).

Primeiro, vamos lidar com a terminologia. Frequentemente ouvimos os termos adaptativo e responsivo . O que eles querem dizer? Qual é a diferença? Como isso se relaciona com nossos componentes?

Adaptativo (adaptável) é um complexo de interfaces visuais criadas para tamanhos de tela específicos. Responsivo (Responsivo) é uma interface única que se adapta a qualquer tamanho de tela.

Além disso, quando a interface é decomposta em pequenos fragmentos, a diferença entre adaptativo e responsivo se torna cada vez mais desfocada, até desaparecer completamente.

Ao desenvolver layouts, nossos designers e desenvolvedores geralmente não compartilham esses conceitos e combinam lógica adaptativa e responsiva.

Além disso, chamarei componentes que contêm lógica adaptativa e responsiva como simplesmente adaptativa . Em primeiro lugar, porque gosto mais dessa palavra do que “responsivo” ou, me perdoe, “responsivo”. E segundo, acho mais comum.

Vou me concentrar em duas áreas de interfaces de exibição - móvel e desktop. Por exibição em dispositivos móveis, queremos dizer largura, por exemplo, ≤ 991 pixels(o número em si não é importante, é apenas uma constante, que depende do sistema de design e do aplicativo) e na tela da área de trabalho - a largura é maior que o limite selecionado. Sentirei falta intencionalmente de monitores para tablets e monitores widescreen, porque, em primeiro lugar, nem todo mundo precisa deles e, em segundo lugar, será mais fácil colocar dessa maneira. Mas os padrões sobre os quais falaremos se expandem igualmente para qualquer número de "mapeamentos".

Além disso, quase não vou falar sobre CSS , principalmente sobre lógica de componentes.

Frontend @youla


Falarei brevemente sobre nossa pilha em Yulia, para que fique claro em que condições criamos nossos componentes. Usamos React / Redux , trabalhamos em monorep, usamos Typescript e escrevemos CSS em componentes estilizados . Como exemplo, vejamos nossos três pacotes (pacotes no conceito de monoreps são pacotes NPM interconectados, que podem ser aplicativos, bibliotecas, utilitários ou componentes separados - você mesmo escolhe o grau de decomposição). Examinaremos dois aplicativos e uma biblioteca de interface do usuário.

@ youla / ui- biblioteca de componentes. Eles são usados ​​não apenas por nós, mas também por outras equipes que precisam de interfaces "Yulian". A biblioteca tem muitas coisas, começando com botões e campos de entrada e terminando, por exemplo, com um cabeçalho ou um formulário de autorização (mais precisamente, sua parte da interface do usuário). Consideramos essa biblioteca uma dependência externa de nosso aplicativo.

@ youla-web / app-classified - o aplicativo responsável pelas seções do catálogo / produto / autorização. De acordo com os requisitos de negócios, todas as interfaces aqui devem ser adaptáveis .

@ youla-web / app-b2b é o aplicativo responsável pelas seções da sua conta pessoal para usuários profissionais. As interfaces deste aplicativo são exclusivamente para desktop .

Além disso, consideraremos escrever componentes adaptáveis ​​usando o exemplo desses pacotes. Mas primeiro você precisa lidar com isso isMobile.

A definição de mobilidade éMobile && <Component />


import React from 'react'

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

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

Antes de começar a escrever componentes adaptáveis, você precisa aprender a definir "mobilidade". Existem muitas maneiras de implementar a definição de mobilidade. Eu quero me debruçar sobre alguns pontos-chave.

Determinando a mobilidade por largura da tela e agente do usuário


A maioria de vocês sabe bem como implementar as duas opções, mas vamos examinar brevemente os pontos principais novamente.

Ao trabalhar com a largura da tela, é habitual definir pontos de limite, após os quais o aplicativo deve se comportar como móvel ou de desktop. O procedimento é o seguinte:

  1. Crie constantes com pontos de limite e salve-os no assunto (se sua solução CSS permitir). Os valores em si podem ser os que seus designers acham mais apropriados para o seu sistema de interface do usuário .
  2. Nós salvamos o tamanho da tela atual em um redux / mobx / context / qualquer fonte de dados. Em qualquer lugar, se apenas os componentes e, preferencialmente, a lógica do aplicativo tivessem acesso a esses dados.
  3. Assinamos o evento resize e atualizamos o valor da largura da tela para aquele que acionará a cadeia de atualizações da árvore de componentes.
  4. Criamos funções auxiliares simples que, usando larguras e constantes da tela, calculam o estado atual ( isMobile,isDesktop ).

Aqui está o pseudo-código que implementa esse modelo de trabalho:

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)

Quando a tela muda, os valores propspara o componente serão atualizados e serão redesenhados corretamente. Existem muitas bibliotecas que implementam essa funcionalidade. Será mais conveniente para alguém usar uma solução pronta, por exemplo, reagir com mídia , reagir com resposta etc., e para alguém é mais fácil escrever sua própria .

Ao contrário do tamanho da tela, user-agentele não pode ser alterado dinamicamente enquanto o aplicativo está em execução (estritamente falando, talvez por meio das ferramentas do desenvolvedor, mas esse não é um cenário do usuário). Nesse caso, não precisamos usar lógica complexa para armazenar o valor e recontar, basta analisar a sequência uma vez window.navigator.userAgent,para salvar o valor e pronto. Existem várias bibliotecas para ajudá-lo, por exemplo, detecção móvel, detecção de reação do dispositivo etc.

A abordagem é user-agentmais simples, mas apenas usá-la não é suficiente. Qualquer pessoa que tenha desenvolvido seriamente interfaces adaptáveis ​​conhece o “toque mágico” de iPads e dispositivos similares, que na posição vertical se enquadram na definição de celular e no desktop horizontal, mas possuem um user-agentdispositivo móvel. Também é importante notar que em um aplicativo totalmente adaptável / responsivo, user-agent é impossível determinar a mobilidade com base em informações apenas se o usuário usar, por exemplo, um navegador de desktop, mas apertar a janela para o tamanho "móvel".

Além disso, não negligencie informações sobre user-agent. Muitas vezes, no código, você pode encontrar constantes como isSafari,isIEetc. que lidam com os "recursos" desses dispositivos e navegadores. É melhor combinar as duas abordagens.

Em nossa base de código, usamos uma constante isCheesySafarique, como o nome indica, define a associação user-agentà família de navegadores Safari. Além disso, temos uma constante isSuperCheesySafari, que implica um Safari móvel correspondente à versão 11 do iOS, que ficou famosa por muitos bugs como este: https://hackernoon.com/how-to-fix-the-ios-11-input-element -in-reparos-modais-bug-aaf66c7ba3f8 .

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

import isMobileUA from './isMobileUA'

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

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

E as consultas de mídia? Sim, de fato, o CSS possui ferramentas internas para trabalhar com adaptabilidade: consultas de mídia e seu método analógico window.matchMedia. Eles podem ser usados, mas a lógica de "atualizar" os componentes ao redimensionar ainda precisará ser implementada. Embora, para mim, usar a sintaxe das consultas de mídia em vez das operações de comparação usuais em JS para componentes e lógica de aplicativos seja uma vantagem duvidosa.

Organização da estrutura de componentes


Descobrimos a definição de mobilidade, agora vamos refletir sobre o uso dos dados que obtivemos e a organização da estrutura do código do componente. Em nosso código, como regra, dois tipos de componentes prevalecem.

O primeiro tipo são os componentes, afiados sob o telefone celular ou na área de trabalho. Nesses componentes, os nomes geralmente contêm as palavras Celular / Computador, que indicam claramente que o componente pertence a um dos tipos. Como um exemplo de componente de um tal pode ser considerada <MobileList />a partir de @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
}

Esse componente, além de uma exportação muito detalhada, é uma lista com dados, separadores, agrupamentos por blocos, etc. Nossos designers gostam muito desse componente e em todos os lugares o usam nas interfaces do Ula. Por exemplo, na descrição da página do produto ou em nossa nova funcionalidade tarifária:


E em N lugares ao redor do site. Também temos um componente semelhante <DesktopList />que implementa a funcionalidade dessa lista para a resolução da área de trabalho.

Os componentes do segundo tipo contêm a lógica do computador e do dispositivo móvel. Vejamos uma versão simplificada da renderização do nosso componente <HeaderBoard />, que vive em @ youla / app-rated.

Nós encontramos para mim é muito conveniente para fazer todos os componentes-estilo para um componente em um único arquivo e importá-lo sob os namespaces S, para separar o código dos outros componentes: import * as S from ‘./styled’. Assim, “S” é um objeto cujas chaves são os nomes dos componentes estilizados e os valores são os próprios 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>
 )

Aqui isMobile, é a dependência do componente, com base na qual o próprio componente decidirá qual interface renderizar.

Para um dimensionamento mais conveniente, geralmente usamos o padrão de inversão de controle nas partes reutilizadas do nosso código, mas tenha cuidado para não sobrecarregar as abstrações de nível superior com lógica desnecessária.

Vamos nos abstrair um pouco dos componentes "Yulian" e examinar mais de perto esses dois componentes:

  • <ComponentA />- com uma separação estrita da lógica de desktop e móvel.
  • <ComponentB />- combinado.

<ComponentA /> vs <ComponentB />


Estrutura de pastas e arquivo index.ts raiz :

./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
}

Graças ao novo webpack de agitação da árvore da tecnologia (ou usando qualquer outro coletor), você pode descartar módulos não utilizados ( ComponentADesktop, ComponentACombined), mesmo com essa reexportação pelo arquivo raiz:

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

Somente o código do arquivo ./ComponentAMobile entra no pacote final.

O componente <ComponentA />contém importações assíncronas usando uma React.Lazyversão específica do componente <ComponentAMobile /> || <ComponentADesktop />para uma situação específica.

No Yule, tentamos aderir ao padrão de um único ponto de entrada no componente por meio do arquivo de índice. Isso facilita a localização e refatoração de componentes. Se o conteúdo do componente não for reexportado pelo arquivo raiz, podemos editá-lo com segurança, pois sabemos que ele não é usado fora do contexto desse componente. Bem, o texto datilografado se aproxima. A pasta com o componente possui sua própria “interface”: exporta no nível do módulo no arquivo raiz e seus detalhes de implementação não são divulgados. Como resultado, ao refatorar, você não pode ter medo de salvar a interface.

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

Além disso, o componente <ComponentADesktop />contém a importação de componentes da área de trabalho:

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

Um componente <ComponentAMobile />contém a importação de componentes móveis:

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

O componente é <ComponentA />adaptável: pelo sinalizador ele isMobilepode decidir qual versão desenhar, só pode baixar os arquivos necessários de forma assíncrona, ou seja, as versões móvel e de desktop podem ser usadas separadamente.

Vamos dar uma olhada no componente agora <ComponentB />. Nele, não decomporemos profundamente a lógica móvel e de desktop, deixaremos todas as condições dentro da estrutura de uma função. Da mesma forma, não separaremos os componentes dos estilos.

Aqui está a estrutura da pasta. O arquivo raiz index.ts simplesmente reexporta ./ComponentB:

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


export { default } from './ComponentB'

O arquivo ./ComponentB com o próprio componente:


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

Vamos tentar estimar as vantagens e desvantagens desses componentes.



Total de três argumentos prós e contras sugado do dedo para cada um deles. Sim, notei que alguns critérios são mencionados imediatamente em vantagens e desvantagens: isso foi feito de propósito, todos serão excluídos do grupo errado.

Nossa experiência com @youla


Em nossa biblioteca de componentes @ youla / ui, tentamos não misturar componentes de desktop e móveis, porque essa é uma dependência externa para muitos de nossos pacotes e outros. O ciclo de vida desses componentes é o maior possível, quero mantê-los o mais finos e leves possível.

dois pontos importantes a serem observados .

Em primeiro lugar, quanto menor o arquivo JS montado, mais rápido ele será entregue ao usuário, isso é óbvio e todos sabem. Mas essa característica é importante apenas para o primeiro download do arquivo; durante visitas repetidas, o arquivo será entregue a partir do cache e não haverá problemas na entrega do código.

Aqui passamos à razão número dois, que em breve poderá se tornar, ou já se tornou, o principal problema de grandes aplicativos da web. Muitos já imaginaram: sim, estamos falando sobre a duração da análise.

Mecanismos modernos como o V8 podem armazenar em cache e o resultado da análise, mas até agora não funciona com muita eficiência. Eddie Osmani tem um ótimo artigo sobre este tópico: https://v8.dev/blog/cost-of-javascript-2019 . Você também pode se inscrever no blog da V8: https://twitter.com/v8js .

É a duração da análise que reduziremos significativamente; isso é especialmente importante para dispositivos móveis com processadores fracos .

Nos pacotes de aplicativos @ youla-web / app- *, ​​o desenvolvimento é mais "orientado aos negócios". E por uma questão de velocidade / simplicidade / preferências pessoais, escolhe-se a decisão de que o próprio desenvolvedor considere o mais correto nessa situação. Muitas vezes acontece que, ao desenvolver pequenos recursos do MVP, é melhor primeiro escrever uma versão mais simples e rápida (<ComponentB />); nesse componente, existem metade do número de linhas. E, como sabemos, quanto mais código - mais erros.

Após verificar a relevância do recurso, será possível substituir o componente por uma versão mais otimizada e produtiva <ComponentA />, se necessário.

Também aconselho que você dê uma olhada no componente. Se as UIs das versões móvel e desktop forem muito diferentes, talvez elas devam ser separadas, mantendo alguma lógica comum em um só lugar. Isso permitirá que você se livre da dor ao escrever CSS complexo, problemas com erros em uma das telas ao refatorar ou alterar outra. E vice-versa, se a interface do usuário estiver o mais próxima possível, por que o trabalho extra?

Conclusão


Para resumir. Compreendemos a terminologia da interface adaptativa / responsiva, examinamos várias maneiras de determinar a mobilidade e várias opções para organizar a estrutura de código do componente adaptável e identificamos as vantagens e desvantagens de cada uma. Certamente muitas das opções acima já eram conhecidas por você, mas a repetição é a melhor maneira de consolidar. Espero que você tenha aprendido algo novo para si mesmo. Da próxima vez, queremos publicar uma coleção de recomendações para a criação de aplicativos Web progressivos, com dicas sobre organização, reutilização e manutenção de código.

All Articles