Entwicklung einer Unternehmensbibliothek von React-Komponenten. PlattformĂŒbergreifender Ansatz

Dieser Artikel erzĂ€hlt die Geschichte der erfolgreichen Implementierung des Designsystems in der Gesellschaft eines der grĂ¶ĂŸten Heimwerker. Die Prinzipien und AnsĂ€tze der plattformĂŒbergreifenden Entwicklung von UI-Komponenten unter Verwendung der React- und React Native-Bibliotheken sowie die Lösung des Problems der Wiederverwendung von Code zwischen Projekten fĂŒr verschiedene Plattformen werden beschrieben.

ZunĂ€chst ein paar Worte darĂŒber, wie alles begann und warum die Idee zur Implementierung eines Systemdesigns aufkam. Alles begann mit einer mobilen Android-Anwendung fĂŒr VerkĂ€ufer in GeschĂ€ften. Die Anwendung basiert auf dem React-Native-Framework. Die StartfunktionalitĂ€t wurde durch nur wenige Module dargestellt, z. B. die Suche nach Produkten im Katalog und in der Produktkarte, Verkaufsbeleg. Übrigens ist dies jetzt eine ziemlich leistungsfĂ€hige Anwendung, die die FunktionalitĂ€t von Informationsschaltern in GeschĂ€ften bereits weitgehend ersetzt hat.

Als nĂ€chstes wurden Webanwendungsprojekte fĂŒr Mitarbeiter der Logistikabteilung sowie verschiedene Konfiguratoren gestartet.

Zu diesem Zeitpunkt zeigte sich ein VerstĂ€ndnis der allgemeinen AnsĂ€tze fĂŒr das Design dieser Anwendungen sowie des Vorhandenseins einer ziemlich großen Codebasis. Und es war logisch, den anderen fĂŒr die weitere Wiederverwendung zu systematisieren.

Um UI / UX zu systematisieren, wurde beschlossen, ein Design-System zu entwickeln. Ich werde nicht ins Detail gehen, was es ist. Im Internet finden Sie viele Artikel zu diesem Thema. Zum Beispiel kann auf Habré die Arbeit von Andrei Sundiev zum Lesen empfohlen werden .

Warum Design System und was sind seine Vorteile? Das erste ist eine gemeinsame Erfahrung und das GefĂŒhl, Produkte zu verwenden. Benutzer erhalten unabhĂ€ngig von der Anwendung eine vertraute BenutzeroberflĂ€che: Die SchaltflĂ€chen sehen aus und funktionieren wie gewohnt, das MenĂŒ wird an der richtigen Stelle geöffnet und mit der richtigen Dynamik funktionieren die Eingabefelder wie gewohnt. Der zweite Vorteil ist die EinfĂŒhrung bestimmter Standards und gemeinsamer AnsĂ€tze sowohl von der Design- als auch von der Entwicklungsseite. Jede neue FunktionalitĂ€t wird nach bereits etablierten Kanonen und AnsĂ€tzen entwickelt. Neue Mitarbeiter erhalten ab den ersten Tagen eine klare Arbeitslinie. Das nĂ€chste ist die Wiederverwendung von Komponenten und die Vereinfachung der Entwicklung. Es ist nicht notwendig, das Rad jedes Mal neu zu erfinden. Sie können Schnittstellen aus vorgefertigten Blöcken mit dem erwarteten Endergebnis erstellen.Der Hauptvorteil fĂŒr den Kunden besteht darin, Geld und Zeit zu sparen.

Also, was haben wir getan? TatsĂ€chlich haben wir nicht nur eine Komponentenbibliothek erstellt, sondern ein ganzes plattformĂŒbergreifendes Framework. Das Framework basiert auf einem Batch-Schema. Wir haben 5 Kern-npm-Pakete. Es ist der Kern fĂŒr die Bereitstellung plattformĂŒbergreifender Web- und Android-Anwendungen. Pakete von Modulen, Dienstprogrammen und Diensten. Und ein Paket von Komponenten, die spĂ€ter besprochen werden.
Unten sehen Sie das UML-Diagramm des Komponentenpakets.

Bild

Es enthĂ€lt die Komponenten selbst, von denen einige unabhĂ€ngig sind (Elemente) und einige miteinander verbunden sind, sowie den inneren Kern oder „Unterkern“.

Lassen Sie uns genauer betrachten, was im „Sub-Core“ enthalten ist. Die erste ist die visuelle Ebene des Systemdesigns. Hier dreht sich alles um Farbpalette, Typografie, EinrĂŒckungssystem, Gitter usw. Der nĂ€chste Block sind die Dienste, die fĂŒr das Funktionieren der Komponenten erforderlich sind, z. B.: ComponentsConfig (Konfiguration der Komponenten), StyleSet (auf dieses Konzept werde ich spĂ€ter nĂ€her eingehen) und Device (eine Methode zum Arbeiten mit der GerĂ€te-API). Und der dritte Block besteht aus allen Arten von Helfern (Resolver, Stilgeneratoren usw.).

Bild

Bei der Entwicklung der Bibliothek haben wir einen atomaren Ansatz fĂŒr das Design von Komponenten verwendet. Alles begann mit der Erstellung elementarer Komponenten oder Elemente. Sie sind elementare „Teilchen“, die voneinander unabhĂ€ngig sind. Die wichtigsten sind Ansicht, Text, Bild, Symbol. Als nĂ€chstes folgen die komplexeren Komponenten. Jeder von ihnen verwendet ein oder mehrere Elemente, um seine Struktur aufzubauen. Zum Beispiel SchaltflĂ€chen, Eingabefelder, Auswahlmöglichkeiten usw. Die nĂ€chste Ebene sind Muster. Sie sind eine Kombination von Komponenten zur Lösung eines UI-Problems. Zum Beispiel ein Autorisierungsformular, eine Kopfzeile mit Parametern und Einstellungen oder eine von einem Designer entworfene Produktkarte, die in verschiedenen Modulen verwendet werden kann. Die letzte und schwierigste und zugleich wichtige Ebene ist das sogenannte Verhalten. Dies sind gebrauchsfertige Module,Implementierung bestimmter GeschĂ€ftslogiken und möglicherweise einschließlich der erforderlichen Back-End-Anforderungen.

Bild

Fahren wir also mit der Implementierung der Komponentenbibliothek fort. Wie bereits erwĂ€hnt, haben wir zwei Zielplattformen - Web und Android (React-Native). Wenn es sich im Web um Elemente handelt, die allen Webentwicklern wie div, span, img, header usw. bekannt sind, handelt es sich bei den reaktionsbezogenen Elementen um die Komponenten Ansicht, Text, Bild, Modal. Und als erstes haben wir uns auf den Namen der Komponenten geeinigt. Wir haben uns fĂŒr ein System im React-Native-Stil entschieden Erstens wurde bereits eine Komponentenbasis in den Projekten implementiert, und zweitens sind diese Namen sowohl fĂŒr Web- als auch fĂŒr reaktionsnative Entwickler am universellsten und verstĂ€ndlichsten. Betrachten Sie beispielsweise die View-Komponente. Die Methode der bedingten Renderkomponente fĂŒr das Web sieht ungefĂ€hr so ​​aus:

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

Jene. Unter der Haube ist dies nichts weiter als ein Div mit den notwendigen Requisiten und Nachkommen. In React-Native ist die Struktur sehr Àhnlich, nur die View-Komponente wird anstelle von divs verwendet:

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

Es stellt sich die Frage: Wie kann man dies zu einer Komponente kombinieren und gleichzeitig das Rendering aufteilen?

Hier kommt ein Reaktionsmuster namens HOC oder Component höherer Ordnung zur Rettung. Wenn Sie versuchen, ein UML-Diagramm dieses Musters zu zeichnen, erhalten Sie ungefÀhr Folgendes:

Bild

Somit besteht jede Komponente aus einem sogenannten Delegaten, der Requisiten von außen empfĂ€ngt und fĂŒr die beiden Plattformen gemeinsame Logik verantwortlich ist, sowie zwei Plattformteilen, in denen die fĂŒr jede Plattform spezifischen Methoden bereits gekapselt sind, und dem wichtigsten Rendering. Betrachten Sie beispielsweise den SchaltflĂ€chen-Delegatencode:

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} />);
        }
    };
}

Der Delegat erhĂ€lt als Argument den Plattformteil der Komponente, implementiert Methoden, die beiden Plattformen gemeinsam sind, und ĂŒbergibt sie an den Plattformteil. Der Plattformteil der Komponente selbst ist wie folgt:

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

Hier ist eine Rendermethode mit all ihren Plattformfunktionen. Die allgemeine FunktionalitĂ€t des Delegaten erfolgt in Form eines Objekts durch einen Requisitendelegierten. Ein Beispiel fĂŒr den Plattformteil einer SchaltflĂ€che fĂŒr eine reaktionsnative Implementierung:

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

In diesem Fall ist die Logik Àhnlich, es werden jedoch reaktionsnative Komponenten verwendet. In beiden Listen ist buttonDelegate ein HOC mit gemeinsamer Logik.

Bei diesem Ansatz bei der Implementierung von Komponenten stellt sich die Frage nach der Trennung von Plattformteilen wĂ€hrend der Montage des Projekts. Es muss sichergestellt werden, dass das von uns in Webprojekten verwendete Webpack nur Teile von Komponenten sammelt, die fĂŒr das Web bestimmt sind, wĂ€hrend der Metro-Bundler in React-Native seine Plattformteile „einhaken“ sollte, ohne auf die Komponente fĂŒr das Web zu achten.

Um dieses Problem zu lösen, verwendeten sie die integrierte Metro-Bundler-Funktion, mit der Sie das PrĂ€fix fĂŒr die Plattform-Dateierweiterung angeben können. In unserem Fall sieht metro.config.js folgendermaßen aus:

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

Daher sucht die Metro beim Erstellen des Bundles zuerst nach Dateien mit der Erweiterung native.js und hakt die Datei dann mit der Erweiterung .js ein, wenn sie sich nicht im aktuellen Verzeichnis befindet. Diese FunktionalitĂ€t ermöglichte es, die Plattformteile der Komponenten in separaten Dateien zu platzieren: Der Teil fĂŒr das Web befindet sich in der .js-Datei, der reaktionsnative Teil wird in der Datei mit der Erweiterung .native.js platziert.

Webpack hat ĂŒbrigens die gleiche FunktionalitĂ€t mit NormalModuleReplacementPlugin.

Ein weiteres Ziel des plattformĂŒbergreifenden Ansatzes bestand darin, einen einzigen Mechanismus fĂŒr das Styling von Komponenten bereitzustellen. Bei Webanwendungen haben wir uns fĂŒr den sass-PrĂ€prozessor entschieden, der letztendlich zu regulĂ€rem CSS kompiliert wird. Jene. FĂŒr Webkomponenten haben wir die bekannten React ClassName-Entwickler verwendet.

Reaktiv native Komponenten werden durch Inline-Stile und Requisitenstil gestaltet. Diese beiden AnsĂ€tze mussten kombiniert werden, um die Verwendung von Stilklassen fĂŒr Android-Anwendungen zu ermöglichen. Zu diesem Zweck wurde das Konzept von styleSet eingefĂŒhrt, das nichts weiter als ein Array von Zeichenfolgen ist - Klassennamen:

styleSet: Array<string>

Gleichzeitig wurde der gleichnamige StyleSet-Dienst fĂŒr react-native implementiert, mit dem Klassennamen registriert werden können:

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

FĂŒr Webkomponenten ist styleSet ein Array von CSS-Klassennamen, die mithilfe der Klassennamenbibliothek „geklebt“ werden .

Da das Projekt plattformĂŒbergreifend ist, ist es offensichtlich, dass mit dem Wachstum der Codebasis auch die Anzahl der externen AbhĂ€ngigkeiten zunimmt. DarĂŒber hinaus sind die AbhĂ€ngigkeiten fĂŒr jede Plattform unterschiedlich. FĂŒr Webkomponenten werden beispielsweise Bibliotheken wie Style-Loader, React-Dom, Klassennamen, Webpack usw. benötigt. FĂŒr React-Native-Komponenten wird eine große Anzahl ihrer "nativen" Bibliotheken verwendet, z. B. React-Native selbst. Wenn ein Projekt, in dem die Komponentenbibliothek verwendet werden soll, nur eine Zielplattform hat, ist die Installation aller AbhĂ€ngigkeiten auf einer anderen Plattform irrational. Um dieses Problem zu lösen, haben wir den Postinstall-Hook von npm selbst verwendet, in dem ein Skript installiert wurde, um AbhĂ€ngigkeiten fĂŒr die angegebene Plattform zu installieren. Die AbhĂ€ngigkeiten selbst wurden im entsprechenden Abschnitt von package.json des Pakets registriert.und die Zielplattform sollte im Projekt package.json als Array angegeben werden.
Dieser Ansatz zeigte jedoch einen Nachteil, der spĂ€ter bei der Montage im CI-System zu mehreren Problemen fĂŒhrte. Die Wurzel des Problems war, dass mit package-lock.json das in postinstall angegebene Skript nicht alle registrierten AbhĂ€ngigkeiten installiert hat.

Ich musste nach einer anderen Lösung fĂŒr dieses Problem suchen. Die Lösung war einfach. Es wurde ein Zwei-Paket-Schema angewendet, bei dem alle PlattformabhĂ€ngigkeiten im Abschnitt AbhĂ€ngigkeiten des entsprechenden Plattformpakets platziert wurden. Im Fall des Webs heißt das Paket beispielsweise Komponenten-Web, in dem sich eine einzige Datei package.json befindet. Es enthĂ€lt alle AbhĂ€ngigkeiten fĂŒr die Webplattform sowie das Hauptpaket mit Komponentenkomponenten. Dieser Ansatz ermöglichte es uns, die Trennung von AbhĂ€ngigkeiten beizubehalten und die FunktionalitĂ€t von package-lock.json beizubehalten.

Abschließend werde ich ein Beispiel fĂŒr JSX-Code unter Verwendung unserer Komponentenbibliothek geben:

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

Dieses Code-Snippet ist plattformĂŒbergreifend und funktioniert in einer React-Anwendung fĂŒr das Web und in einer Android-Anwendung auf React-Native gleich. Bei Bedarf kann derselbe Code unter iOS "abgewickelt" werden.

Damit war die Hauptaufgabe gelöst, mit der wir konfrontiert waren - die maximale Wiederverwendung beider EntwurfsansÀtze und der Codebasis zwischen verschiedenen Projekten.
Bitte geben Sie in den Kommentaren an, welche Fragen zu diesem Thema im nÀchsten Artikel interessant waren.

All Articles