Développement d'une bibliothèque corporative de composants React. Approche multiplateforme

Cet article raconte l'histoire de la mise en œuvre réussie du système de conception en compagnie de l'un des plus grands détaillants de bricolage. Les principes et approches du développement multiplateforme de composants d'interface utilisateur à l'aide des bibliothèques React et React Native sont décrits, ainsi que la solution au problème de la réutilisation de code entre des projets pour différentes plates-formes.

Tout d'abord, quelques mots sur la façon dont tout a commencé et pourquoi l'idée de mettre en œuvre une conception de système est apparue. Tout a commencé avec une application Android mobile pour les vendeurs en magasin. L'application est basée sur le framework React-Native. La fonctionnalité de départ était représentée par seulement quelques modules, tels que la recherche de produits dans le catalogue et la fiche produit, le document de vente. À propos, il s'agit maintenant d'une application assez puissante qui a déjà largement remplacé la fonctionnalité des bureaux d'information dans les magasins.

Ensuite, des projets d'applications web pour les employés du service logistique, ainsi que divers configurateurs, ont été lancés.

À ce stade, une compréhension des approches générales de la conception de ces applications, ainsi que la présence d'une base de code assez importante, sont apparues. Et il était logique de systématiser l'autre pour une réutilisation ultérieure.

Pour systématiser UI / UX, il a été décidé de développer un système de conception. Je n'entrerai pas dans les détails de ce que c'est. Sur Internet, vous pouvez trouver de nombreux articles sur ce sujet. Par exemple, sur Habré, le travail d'Andrei Sundiev peut être recommandé à la lecture .

Pourquoi concevoir un système et quels sont ses avantages? Le premier est une expérience commune et le sentiment d'utiliser des produits. Les utilisateurs obtiennent une interface familière quelle que soit l'application: les boutons ressemblent et fonctionnent comme ils sont habitués, le menu s'ouvre au bon endroit et avec la bonne dynamique, les champs de saisie fonctionnent de la manière habituelle. Le deuxième avantage est l'introduction de certaines normes et approches communes à la fois du côté de la conception et du côté du développement. Chaque nouvelle fonctionnalité est développée selon des canons et des approches déjà établis. Dès les premiers jours, les nouveaux employés reçoivent une ligne de travail claire. La prochaine consiste à réutiliser les composants et à simplifier le développement. Il n'est pas nécessaire de «réinventer la roue» à chaque fois. Vous pouvez créer des interfaces à partir de blocs prêts à l'emploi avec le résultat final attendu.Eh bien, l'avantage principal pour le client est d'économiser temps et argent.

Alors, qu'avons-nous fait. En fait, nous avons créé non seulement une bibliothèque de composants, mais tout un framework multiplateforme. Le cadre est basé sur un schéma par lots. Nous avons 5 packages core npm. C'est le cœur du déploiement d'applications Web et Android multiplateformes. Forfaits de modules, utilitaires et services. Et un ensemble de composants, qui sera discuté plus tard.
Vous trouverez ci-dessous le diagramme UML du package de composants.

image

Il comprend les composants eux-mêmes, dont certains sont indépendants (éléments), et certains sont connectés les uns aux autres, ainsi que le noyau interne ou «sous-noyau».

Examinons plus en détail ce qui est inclus dans le «sous-noyau». Le premier est la couche visuelle de la conception du système. Tout ici concerne la palette de couleurs, la typographie, le système d'indentation, les grilles, etc. Le bloc suivant est les services nécessaires au fonctionnement des composants, tels que: ComponentsConfig (configuration des composants), StyleSet (je discuterai plus tard de ce concept) et Device (une méthode pour travailler avec l'api du périphérique). Et le troisième bloc est toutes sortes d'aides (résolveurs, générateurs de style, etc.).

image

Lors du développement de la bibliothèque, nous avons utilisé une approche atomique pour la conception des composants. Tout a commencé avec la création de composants ou d'éléments élémentaires. Ce sont des «particules» élémentaires indépendantes les unes des autres. Les principaux sont Affichage, Texte, Image, Icône. Viennent ensuite les composants les plus complexes. Chacun d'eux utilise un ou plusieurs éléments pour construire sa structure. Par exemple, boutons, champs de saisie, sélections, etc. Le niveau suivant est celui des modèles. Ils sont une combinaison de composants pour résoudre tout problème d'interface utilisateur. Par exemple, un formulaire d'autorisation, un en-tête avec paramètres et réglages, ou une carte de produit conçue par un concepteur qui peut être utilisée dans différents modules. Le dernier et le plus difficile et en même temps important niveau est le soi-disant comportement. Ce sont des modules prêts à l'emploi,Implémentation de certaines logiques métier et, éventuellement, y compris l'ensemble nécessaire de requêtes back-end.

image

Passons donc à l'implémentation de la bibliothèque de composants. Comme je l'ai mentionné précédemment, nous avons deux plates-formes cibles - Web et Android (réactif natif). Si sur le web ce sont des éléments bien connus de tous les développeurs web comme div, span, img, header, etc., en react-native ce sont les composants View, Text, Image, Modal. Et la première chose sur laquelle nous nous sommes mis d'accord est le nom des composants. Nous avons décidé d'utiliser un système de style natif réactif, comme d'une part, une base de composants a déjà été implémentée dans les projets, et d'autre part, ces noms sont les plus universels et les plus compréhensibles pour les développeurs web et réactifs. Par exemple, considérez le composant View. La méthode du composant de rendu conditionnel pour le Web ressemble à ceci:

render() {
	return(
		<div {...props}>
			{children}
		</div>
	)
}

Ceux. sous le capot, ce n'est rien de plus qu'un div avec les accessoires et les descendants nécessaires. En react-native, la structure est très similaire, seul le composant View est utilisé à la place des divs:

render() {
	return(
		<View {...props}>
			{children}
		</View>
	)
}

La question se pose: comment combiner cela en un seul composant et en même temps diviser le rendu?

C'est là qu'un modèle de réaction appelé HOC ou composant d'ordre supérieur vient à la rescousse. Si vous essayez de dessiner un diagramme UML de ce modèle, vous obtenez quelque chose comme ceci:

image

Ainsi, chaque composant se compose d'un soi-disant délégué qui reçoit les accessoires de l'extérieur et est responsable de la logique commune aux deux plates-formes, et de deux parties de plate-forme dans lesquelles les méthodes spécifiques à chaque plate-forme sont déjà encapsulées et le rendu le plus important. Par exemple, considérez le code délégué du bouton:

export default function buttonDelegate(ReactComponent: ComponentType<Props>): ComponentType<Props> {
    return class ButtonDelegate extends PureComponent<Props> {
        
        // Button common methods

        render() {
           const { onPress, onPressIn, onPressOut } = this.props;
            const delegate = {
                buttonContent: this.buttonContent,
                buttonSize: this.buttonSize,
                iconSize: this.iconSize,
                onClick: onPress,
                onMouseUp: onPressIn,
                onMouseDown: onPressOut,
                onPress: this.onPress,
                textColor: this.textColor,
            };
            return (<ReactComponent {...this.props} delegate={delegate} />);
        }
    };
}

Le délégué reçoit en argument la partie plate-forme du composant, implémente des méthodes communes aux deux plates-formes et les transmet à la partie plate-forme. La partie plate-forme du composant lui-même est la suivante:

class Button extends PureComponent<WebProps, State> {
    
   // Web specific methods

    render() {
        const { delegate: { onPress, buttonContent } } = this.props;
        return (
            <button
                className={this.classes}
                {...buttonProps}
                onClick={onPress}
                style={style}
            >
                {buttonContent(this.spinner, this.iconText)}
            </button>
        );
    }
}

export default buttonDelegate(Button);

Voici une méthode de rendu avec toutes ses fonctionnalités de plateforme. La fonctionnalité générale du délégué se présente sous la forme d'un objet via le délégué des accessoires. Un exemple de la partie plate-forme d'un bouton pour une implémentation native native:

class Button extends PureComponent<NativeProps, State> {

    // Native specific methods

    render() {
        const { delegate: { onPress, buttonContent } } = this.props;
        return (
            <View styleSet={this.styles} style={style}>
                <TouchableOpacity
                    {...butonProps}
                    onPress={onPress}
                    style={this.touchableStyles}
                    {...touchableProps}    
                >
                    {buttonContent(this.spinner, this.iconText)}
                </TouchableOpacity>
            </View>
        );
    }
}

export default buttonDelegate(Button);

Dans ce cas, la logique est similaire, mais des composants natifs réactifs sont utilisés. Dans les deux listes, buttonDelegate est un HOC avec une logique commune.

Avec cette approche dans la mise en œuvre des composants, la question se pose de la séparation des pièces de la plateforme lors de l'assemblage du projet. Il est nécessaire de s'assurer que le webpack utilisé par nous dans les projets pour le web ne recueille que les parties des composants destinés au web, tandis que le bundler metro en react-native doit «accrocher» ses parties de plateforme, sans prêter attention au composant pour le web.

Pour résoudre ce problème, ils ont utilisé la fonctionnalité intégrée de bundler metro, qui vous permet de spécifier le préfixe d'extension de fichier de plate-forme. Dans notre cas, metro.config.js ressemble à ceci:

module.exports = {
    resolver: {
        useWatchman: false,
        platforms: ['native'],
    },
};

Ainsi, lors de la création du bundle, metro recherche d'abord les fichiers avec l'extension native.js, puis, s'il n'est pas dans le répertoire en cours, il accroche le fichier avec l'extension .js. Cette fonctionnalité a permis de placer les parties de plate-forme des composants dans des fichiers séparés: la partie pour le web est située dans le fichier .js, la partie react-native est placée dans le fichier avec l'extension .native.js.

Soit dit en passant, webpack a les mêmes fonctionnalités que NormalModuleReplacementPlugin.

Un autre objectif de l'approche multiplateforme était de fournir un mécanisme unique pour styliser les composants. Dans le cas des applications Web, nous avons choisi le préprocesseur Sass, qui se compile finalement en CSS standard. Ceux. pour les composants Web, nous avons utilisé les développeurs familiers react className.

Les composants natifs de React sont stylisés à l'aide de styles en ligne et de style d'accessoires. Il a fallu combiner ces deux approches, permettant d'utiliser des classes de style pour les applications Android. À cette fin, le concept de styleSet a été introduit, qui n'est rien de plus qu'un tableau de chaînes - noms de classe:

styleSet: Array<string>

Dans le même temps, le service StyleSet du même nom a été implémenté pour react-native, ce qui permet d'enregistrer les noms de classe:

export default StyleSet.define({
    'lmui-Button': {
        borderRadius: 6,
    },
    'lmui-Button-buttonSize-md': {
        paddingTop: 4,
        paddingBottom: 4,
        paddingLeft: 12,
        paddingRight: 12,
    },
    'lmui-Button-buttonSize-lg': {
        paddingTop: 8,
        paddingBottom: 8,
        paddingLeft: 16,
        paddingRight: 16,
    },
})

Pour les composants Web, styleSet est un tableau de noms de classes css qui sont «collés» à l'aide de la bibliothèque de noms de classes .

Étant donné que le projet est multiplateforme, il est évident qu'avec la croissance de la base de code, le nombre de dépendances externes augmente également. De plus, les dépendances sont différentes pour chaque plateforme. Par exemple, pour les composants Web, des bibliothèques telles que style-loader, react-dom, classnames, webpack, etc. sont nécessaires. Pour les composants react-native, un grand nombre de ses bibliothèques «natives» sont utilisées, par exemple, react-native lui-même. Si le projet dans lequel il est censé utiliser la bibliothèque de composants n'a qu'une seule plate-forme cible, l'installation de toutes les dépendances sur une autre plate-forme est irrationnelle. Pour résoudre ce problème, nous avons utilisé le hook postinstall de npm lui-même, dans lequel un script a été installé pour installer les dépendances pour la plate-forme spécifiée. Les dépendances elles-mêmes ont été enregistrées dans la section correspondante de package.json du package,et la plate-forme cible doit être spécifiée dans le package package.json sous forme de tableau.
Cependant, cette approche a révélé un inconvénient, qui s'est ensuite transformé en plusieurs problèmes lors de l'assemblage dans le système CI. La racine du problème était qu'avec package-lock.json, le script spécifié dans postinstall n'a pas installé toutes les dépendances enregistrées.

J'ai dû chercher une autre solution à ce problème. La solution était simple. Un schéma à deux packages a été appliqué, dans lequel toutes les dépendances de plateforme ont été placées dans la section des dépendances du package de plateforme correspondant. Par exemple, dans le cas du Web, le package est appelé components-web, dans lequel il existe un seul fichier package.json. Il contient toutes les dépendances de la plate-forme Web, ainsi que le package principal avec les composants composants. Cette approche nous a permis de maintenir la séparation des dépendances et de préserver la fonctionnalité de package-lock.json.

En conclusion, je vais donner un exemple de code JSX en utilisant notre bibliothèque de composants:

<View row>
   <View
      col-xs={12}
      col-md={8}
      col-lg={4}
      col-xl={4}
      middle-xs
      col-md-offset-3
   />
     <Text size=”fs1”>Sample text</Text>
   </View>
</View>

Cet extrait de code est multiplateforme et fonctionne de la même manière dans une application React pour le Web et dans une application Android sur React-Native. Si nécessaire, le même code peut être «liquidé» sous iOS.

Ainsi, la tâche principale à laquelle nous avons été confrontés a été résolue - la réutilisation maximale des deux approches de conception et de la base de code entre divers projets.
Veuillez indiquer dans les commentaires quelles questions sur ce sujet étaient intéressantes à apprendre dans le prochain article.

All Articles