Réactif ou réactif? Analyse de la structure des composants React



Dans cet article, nous allons comprendre la complexité de l'écriture de composants adaptatifs, parler de la division du code, envisager plusieurs façons d'organiser la structure du code, évaluer leurs avantages et leurs inconvénients et essayer de choisir la meilleure (mais ce n'est pas exact).

Commençons par la terminologie. Nous entendons souvent les termes adaptatif et réactif . Que signifient-ils? Quelle est la différence? Quel est le lien avec nos composants?

Adaptive (adaptative) est un complexe d'interfaces visuelles créées pour des tailles d'écran spécifiques. Responsive (Responsive) est une interface unique qui s'adapte à toutes les tailles d'écran.

De plus, lorsque l'interface est décomposée en petits fragments, la différence entre adaptatif et réactif devient de plus en plus floue, jusqu'à disparaître complètement.

Lors du développement de mises en page, nos concepteurs, ainsi que les développeurs, ne partagent généralement pas ces concepts et combinent une logique adaptative et réactive.

De plus, j'appellerai les composants qui contiennent une logique adaptative et réactive comme simplement adaptatifs . D'abord parce que j'aime ce mot plus que «réactif» ou, pardonnez-moi, «réactif». Et deuxièmement, je le trouve plus courant.

Je vais me concentrer sur deux domaines des interfaces d'affichage - mobile et de bureau. Par affichage mobile, nous entendons la largeur, par exemple, ≤ 991 pixels(le nombre lui-même n'est pas important, c'est juste une constante, qui dépend de votre système de conception et de votre application), et sous l'écran du bureau - la largeur est supérieure au seuil sélectionné. Je manquerai intentionnellement des écrans pour tablettes et moniteurs à écran large, car, premièrement, tout le monde n'en a pas besoin, et deuxièmement, il sera plus facile de le dire ainsi. Mais les modèles dont nous allons parler s'étendent également pour un certain nombre de «mappages».

De plus, je ne parlerai presque pas de CSS , principalement nous parlerons de la logique des composants.

Frontend @youla


Je vais parler brièvement de notre pile à Yulia afin de savoir clairement dans quelles conditions nous créons nos composants. Nous utilisons React / Redux , nous travaillons en monorep, nous utilisons Typescript et nous écrivons CSS sur des composants de style . À titre d'exemple, regardons nos trois packages (les packages dans le concept de monoreps sont des packages NPM qui sont interconnectés, qui peuvent être des applications, des bibliothèques, des utilitaires ou des composants distincts - vous choisissez vous-même le degré de décomposition). Nous examinerons deux applications et une bibliothèque d'interface utilisateur.

@ youla / ui- bibliothèque de composants. Ils sont utilisés non seulement par nous, mais aussi par d'autres équipes qui ont besoin d'interfaces "Yulian". La bibliothèque a beaucoup de choses, commençant par des boutons et des champs de saisie, et se terminant, par exemple, par un en-tête ou un formulaire d'autorisation (plus précisément, sa partie interface utilisateur). Nous considérons cette bibliothèque comme une dépendance externe de notre application.

@ youla-web / app-classified - l'application responsable des sections du catalogue / produit / autorisation. Selon les besoins de l'entreprise, toutes les interfaces doivent être adaptatives .

@ youla-web / app-b2b est l'application responsable des sections de votre compte personnel pour les utilisateurs professionnels. Les interfaces de cette application sont exclusivement de bureau .

De plus, nous envisagerons d'écrire des composants adaptatifs en utilisant l'exemple de ces packages. Mais vous devez d'abord y faire face isMobile.

Définition de mobilité isMobile && <Composant />


import React from 'react'

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

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

Avant de commencer à écrire des composants adaptatifs, vous devez apprendre à définir la «mobilité». Il existe de nombreuses façons de mettre en œuvre la définition de la mobilité. Je veux m'attarder sur certains points clés.

Déterminer la mobilité en fonction de la largeur de l'écran et de l'agent utilisateur


La plupart d'entre vous savent comment mettre en œuvre les deux options, mais revenons brièvement sur les points principaux.

Lorsque vous travaillez avec la largeur de l'écran, il est habituel de définir des points limites, après quoi l'application doit se comporter comme mobile ou de bureau. La procédure est la suivante:

  1. Créez des constantes avec des points limites et enregistrez-les dans le sujet (si votre solution CSS le permet). Les valeurs elles-mêmes peuvent être celles que vos concepteurs trouvent les plus appropriées pour votre système d'interface utilisateur .
  2. Nous enregistrons la taille d'écran actuelle dans une source de données redux / mobx / context / any . Partout, si seulement les composants et, de préférence, la logique d'application avaient accès à ces données.
  3. Nous souscrivons à l'événement resize et mettons à jour la valeur de la largeur de l'écran à celle qui déclenchera la chaîne de mises à jour de l'arborescence des composants.
  4. Nous créons des fonctions d'assistance simples qui, en utilisant des largeurs d'écran et des constantes, calculent l'état actuel ( isMobile,isDesktop ).

Voici le pseudo code qui implémente ce modèle de travail:

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)

Lorsque l'écran change, les valeurs dans propspour le composant seront mises à jour et il sera correctement redessiné. Il existe de nombreuses bibliothèques qui implémentent cette fonctionnalité. Il sera plus pratique pour quelqu'un d'utiliser une solution prête à l'emploi, par exemple, react-media , react-responsive , etc., et pour quelqu'un, il est plus facile d'écrire la vôtre .

Contrairement à la taille de l'écran, user-agentil ne peut pas changer dynamiquement pendant que l'application est en cours d'exécution (à proprement parler, peut-être via les outils du développeur, mais ce n'est pas un scénario utilisateur). Dans ce cas, nous n'avons pas besoin d'utiliser une logique complexe pour stocker la valeur et le recomptage, il suffit d'analyser la chaîne une fois window.navigator.userAgent,pour enregistrer la valeur, et vous avez terminé. Il existe un grand nombre de bibliothèques pour vous aider, par exemple, la détection mobile, react-device-detect , etc.

L'approche est user-agentplus simple, mais son utilisation ne suffit pas. Quiconque a sérieusement développé des interfaces adaptatives connaît la «touche magique» des iPads et des appareils similaires, qui en position verticale relèvent de la définition de mobile, et en horizontal - bureau, mais en même temps ont un user-agentappareil mobile. Il convient également de noter que dans une application entièrement adaptative / réactive, user-agent il est impossible de déterminer la mobilité sur la base d'informations uniquement si l'utilisateur utilise, par exemple, un navigateur de bureau, mais réduit la fenêtre à la taille «mobile».

Aussi, ne négligez pas les informations sur user-agent. Très souvent, dans le code, vous pouvez trouver des constantes telles que isSafari,isIEetc. qui gèrent les «fonctionnalités» de ces appareils et navigateurs. Il est préférable de combiner les deux approches.

Dans notre base de code, nous utilisons une constante isCheesySafariqui, comme son nom l'indique, définit l'appartenance user-agentà la famille de navigateurs Safari. Mais en plus de cela, nous avons une constante isSuperCheesySafari, ce qui implique un Safari mobile correspondant à la version 11 d'iOS, qui est devenu célèbre pour de nombreux bugs comme celui-ci: https://hackernoon.com/how-to-fix-the-ios-11-input-element -en-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'en est-il des requêtes médiatiques? Oui, en effet, CSS possède des outils intégrés pour travailler avec l'adaptabilité: les requêtes multimédias et leur méthode analogique window.matchMedia. Ils peuvent être utilisés, mais la logique de «mise à jour» des composants lors du redimensionnement devra encore être mise en œuvre. Bien que pour moi personnellement, l'utilisation de la syntaxe des requêtes multimédias au lieu des opérations de comparaison habituelles dans JS pour la logique d'application et les composants soit un avantage douteux.

Organisation de la structure des composants


Nous avons trouvé la définition de la mobilité, réfléchissons maintenant à l'utilisation des données que nous avons obtenues et à l'organisation de la structure du code des composants. Dans notre code, en règle générale, deux types de composants prévalent.

Le premier type est les composants, affûtés sous le téléphone portable ou sous le bureau. Dans ces composants, les noms contiennent souvent les mots Mobile / Desktop, qui indiquent clairement que le composant appartient à l'un des types. À titre d'exemple d'un tel composant peut être considéré à <MobileList />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
}

Ce composant, en plus de l'exportation très verbeuse, est une liste de données, séparateurs, regroupements par blocs, etc. Nos concepteurs sont très friands de ce composant et l'utilisent partout dans les interfaces Ula. Par exemple, dans la description sur la page produit ou dans notre nouvelle fonctionnalité tarifaire:


Et dans N endroits autour du site. Nous avons également un composant similaire <DesktopList />qui implémente cette fonctionnalité de liste pour la résolution du bureau.

Les composants du deuxième type contiennent la logique du bureau et du mobile. Regardons une version simplifiée du rendu de notre composant <HeaderBoard />, qui vit dans @ youla / app-classified.

Nous avons trouvé pour moi - même est très pratique pour faire toutes les composantes pour un composant de style dans un seul fichier et l' importer dans les espaces de noms S, de séparer le code des autres composants: import * as S from ‘./styled’. Par conséquent, "S" est un objet dont les clés sont les noms des composants stylisés et les valeurs sont les composants eux-mêmes.

 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>
 )

Ici isMobile, c'est la dépendance du composant, sur la base de laquelle le composant lui-même décidera de l'interface à restituer.

Pour une mise à l'échelle plus pratique, nous utilisons souvent le modèle d'inversion de contrôle dans les parties réutilisées de notre code, mais veillez à ne pas surcharger les abstractions de niveau supérieur avec une logique inutile.

Abandonnons maintenant un peu les composants «Yulian» et regardons de plus près ces deux composants:

  • <ComponentA />- avec une séparation stricte de la logique de bureau et mobile.
  • <ComponentB />- combiné.

<ComponentA /> vs <ComponentB />


Structure des dossiers et fichier racine index.ts :

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

Grâce au nouveau webpack secouant l'arborescence technologique (ou en utilisant tout autre collecteur), vous pouvez supprimer les modules inutilisés ( ComponentADesktop, ComponentACombined), même avec cette réexportation via le fichier racine:

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

Seul le code du fichier ./ComponentAMobile entre dans le bundle final.

Le composant <ComponentA />contient des importations asynchrones utilisant une React.Lazyversion spécifique du composant <ComponentAMobile /> || <ComponentADesktop />pour une situation spécifique.

Chez Yule, nous essayons d'adhérer au modèle d'un seul point d'entrée dans le composant via le fichier d'index. Cela facilite la recherche et la refactorisation de composants. Si le contenu du composant n'est pas réexporté via le fichier racine, nous pouvons le modifier en toute sécurité, car nous savons qu'il n'est pas utilisé en dehors du contexte de ce composant. Eh bien, Typescript se couvrira en un rien de temps. Le dossier avec le composant a sa propre «interface»: exporte au niveau du module dans le fichier racine, et ses détails d'implémentation ne sont pas divulgués. Par conséquent, lors de la refactorisation, vous ne pouvez pas avoir peur d'enregistrer l'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

En outre, le composant <ComponentADesktop />contient l'importation de composants de bureau:

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 composant <ComponentAMobile />contient l'importation de composants mobiles:

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

Le composant est <ComponentA />adaptatif: par l'indicateur, il isMobilepeut décider de la version à dessiner, ne peut télécharger que les fichiers requis de manière asynchrone, c'est-à-dire que les versions mobile et de bureau peuvent être utilisées séparément.

Regardons maintenant le composant <ComponentB />. Dans ce document, nous ne décomposerons pas profondément la logique mobile et de bureau, nous laisserons toutes les conditions dans le cadre d'une fonction. De même, nous ne séparerons pas les composants des styles.

Voici la structure des dossiers. Le fichier racine index.ts réexporte simplement ./ComponentB:

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


export { default } from './ComponentB'

Le fichier ./ComponentB avec le composant lui-même:


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

Essayons d'estimer les avantages et les inconvénients de ces composants.



Au total, trois arguments pour et contre ont été aspirés du doigt pour chacun d'eux. Oui, j'ai remarqué que certains critères sont mentionnés immédiatement dans les avantages et les inconvénients: cela a été fait exprès, tout le monde les supprimera du mauvais groupe.

Notre expérience avec @youla


Dans notre bibliothèque de composants @ youla / ui, nous essayons de ne pas mélanger les composants de bureau et mobiles, car il s'agit d'une dépendance externe pour beaucoup de nos packages et d'autres. Le cycle de vie de ces composants est le plus long possible, je souhaite les garder aussi fins et légers que possible.

Il y a deux points importants à noter .

Premièrement, plus le fichier JS assemblé est petit, plus il sera livré rapidement à l'utilisateur, cela est évident et tout le monde le sait. Mais cette caractéristique n'est importante que pour le premier téléchargement du fichier, lors de visites répétées le fichier sera délivré depuis le cache, et il n'y aura pas de problème de livraison de code.

Nous passons ici à la deuxième raison, qui pourrait bientôt devenir, ou est déjà devenue, le principal problème des grandes applications Web. Beaucoup ont déjà deviné: oui, nous parlons de la durée de l'analyse.

Les moteurs modernes comme le V8 peuvent mettre en cache et le résultat de l'analyse, mais jusqu'à présent, il ne fonctionne pas très efficacement. Eddie Osmani a un excellent article sur ce sujet: https://v8.dev/blog/cost-of-javascript-2019 . Vous pouvez également vous abonner au blog V8: https://twitter.com/v8js .

C'est la durée de l'analyse que nous allons réduire considérablement, ce qui est particulièrement important pour les appareils mobiles avec des processeurs faibles .

Dans les packages d'application @ youla-web / app- *, ​​le développement est plus «orienté métier». Et dans un souci de rapidité / simplicité / préférences personnelles, la décision est choisie que le développeur lui-même considère comme la plus correcte dans cette situation. Il arrive souvent que lors du développement de petites fonctionnalités MVP, il soit préférable d'écrire d'abord une option plus simple et plus rapide (<ComponentB />), dans un tel composant, il y a la moitié des lignes. Et, comme nous le savons, plus il y a de code - plus il y a d'erreurs.

Après avoir vérifié la pertinence de la fonctionnalité, il sera possible de remplacer le composant par une version <ComponentA /> plus optimisée et productive, si nécessaire.

Je vous conseille également de jeter un œil au composant. Si les interfaces utilisateur des versions mobile et de bureau sont très différentes, alors elles devraient peut-être être séparées, en gardant une logique commune au même endroit. Cela vous permettra de vous débarrasser de la douleur lors de l'écriture de CSS complexes, des problèmes d'erreurs dans l'un des écrans lors de la refactorisation ou de la modification d'un autre. Et vice versa, si l'interface utilisateur est aussi proche que possible, alors pourquoi faire le travail supplémentaire?

Conclusion


Résumer. Nous avons compris la terminologie de l'interface adaptative / réactive, examiné plusieurs façons de déterminer la mobilité et plusieurs options pour organiser la structure de code du composant adaptatif, et identifié les avantages et les inconvénients de chacun. Certes, une grande partie de ce qui précède était déjà connue de vous, mais la répétition est le meilleur moyen de consolider. J'espère que vous avez appris quelque chose de nouveau par vous-même. La prochaine fois, nous voulons publier une collection de recommandations pour l'écriture d'applications Web progressives, avec des conseils sur l'organisation, la réutilisation et la maintenance du code.

All Articles