Desarrollo de una biblioteca corporativa de componentes React. Enfoque multiplataforma

Este artículo cuenta la historia de la implementación exitosa del sistema de diseño en la compañía de uno de los minoristas de bricolaje más grandes. Se describen los principios y enfoques del desarrollo multiplataforma de componentes de la interfaz de usuario utilizando las bibliotecas React y React Native, así como la solución al problema de reutilizar el código entre proyectos para diferentes plataformas.

Primero, algunas palabras sobre cómo comenzó todo y por qué surgió la idea de implementar un diseño de sistema. Todo comenzó con una aplicación móvil de Android para vendedores en tiendas. La aplicación se basa en el marco React-Native. La funcionalidad inicial estaba representada por solo unos pocos módulos, como la búsqueda de productos en el catálogo y la tarjeta de producto, documento de ventas. Por cierto, ahora esta es una aplicación bastante poderosa que ya ha reemplazado en gran medida la funcionalidad de los escritorios de información en las tiendas.

A continuación, se lanzaron proyectos de aplicaciones web para empleados del departamento de logística, así como varios configuradores.

En esta etapa, apareció una comprensión de los enfoques generales para el diseño de estas aplicaciones, así como la presencia de una base de código bastante grande. Y era lógico sistematizar el otro para su posterior reutilización.

Para sistematizar UI / UX, se decidió desarrollar un sistema de diseño. No entraré en detalles sobre lo que es. En Internet, puede encontrar muchos artículos sobre este tema. Por ejemplo, en Habré, el trabajo de Andrei Sundiev se puede recomendar para leer .

¿Por qué diseñar un sistema y cuáles son sus ventajas? La primera es una experiencia común y la sensación de usar productos. Los usuarios obtienen una interfaz familiar independientemente de la aplicación: los botones se ven y funcionan de la forma en que están acostumbrados, el menú se abre en el lugar correcto y con la dinámica correcta, los campos de entrada funcionan de la manera habitual. La segunda ventaja es la introducción de ciertos estándares y enfoques comunes tanto desde el lado del diseño como desde el lado del desarrollo. Cada nueva funcionalidad se desarrolla de acuerdo con los cánones y enfoques ya establecidos. Desde los primeros días, los nuevos empleados reciben una línea de trabajo clara. El siguiente es reutilizar componentes y simplificar el desarrollo. No hay necesidad de "reinventar la rueda" cada vez. Puede crear interfaces a partir de bloques preparados con el resultado final esperado.Bueno, la principal ventaja en primer lugar para el cliente es ahorrar dinero y tiempo.

Entonces, qué hemos hecho. De hecho, hemos creado no solo una biblioteca de componentes, sino un marco completo multiplataforma. El marco se basa en un esquema por lotes. Tenemos 5 paquetes principales de npm. Es el núcleo para implementar aplicaciones web y Android multiplataforma. Paquetes de módulos, utilidades y servicios. Y un paquete de componentes, que se discutirá más adelante.
A continuación se muestra el diagrama UML del paquete de componentes.

imagen

Incluye los componentes en sí, algunos de los cuales son independientes (elementos), y algunos están conectados entre sí, así como el núcleo interno o "sub-núcleo".

Consideremos con más detalle lo que se incluye en el "subnúcleo". El primero es la capa visual del diseño del sistema. Todo aquí se trata de la paleta de colores, tipografía, sistema de sangría, cuadrículas, etc. El siguiente bloque son los servicios necesarios para que los componentes funcionen, como: ComponentsConfig (configuración de componentes), StyleSet (analizaré este concepto con más detalle más adelante) y Device (un método para trabajar con la API del dispositivo). Y el tercer bloque es todo tipo de ayudantes (resolvers, generadores de estilos, etc.).

imagen

Al desarrollar la biblioteca, utilizamos un enfoque atómico para el diseño de componentes. Todo comenzó con la creación de componentes o elementos elementales. Son "partículas" elementales que son independientes entre sí. Los principales son Ver, Texto, Imagen, Icono. Los siguientes son los componentes más complejos. Cada uno de ellos usa uno o más elementos para construir su estructura. Por ejemplo, botones, campos de entrada, selecciones, etc. El siguiente nivel son los patrones. Son una combinación de componentes para resolver cualquier problema de IU. Por ejemplo, un formulario de autorización, un encabezado con parámetros y configuraciones, o una tarjeta de producto diseñada por un diseñador que puede usarse en diferentes módulos. El último y más difícil y al mismo tiempo importante nivel es el llamado comportamiento. Estos son módulos listos para usar,Implementando cierta lógica de negocios y, posiblemente, incluyendo el conjunto necesario de solicitudes de back-end.

imagen

Entonces, pasemos a la implementación de la biblioteca de componentes. Como mencioné antes, tenemos dos plataformas de destino: web y Android (react-native). Si en la web estos son elementos familiares para todos los desarrolladores web como div, span, img, header, etc., en react-native estos son los componentes View, Text, Image, Modal. Y lo primero que acordamos es el nombre de los componentes. Decidimos usar un sistema de estilo nativo de reacción, como en primer lugar, algunos componentes básicos ya se han implementado en proyectos y, en segundo lugar, estos nombres son los más universales y comprensibles para los desarrolladores web y los nativos de reacción. Por ejemplo, considere el componente Ver. El método del componente de representación condicional para la web se parece a esto:

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

Aquellos. debajo del capó, esto no es más que un div con los accesorios y descendientes necesarios. En react-native, la estructura es muy similar, solo se usa el componente View en lugar de divs:

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

Surge la pregunta: ¿cómo combinar esto en un componente y al mismo tiempo dividir el renderizado?

Aquí es donde un patrón de Reacción llamado HOC o Componente de Orden Superior viene al rescate. Si intenta dibujar un diagrama UML de este patrón, obtendrá algo como lo siguiente:

imagen

Por lo tanto, cada componente consta de un llamado delegado que recibe accesorios del exterior y es responsable de la lógica común a ambas plataformas, y de dos partes de la plataforma en las que los métodos específicos para cada plataforma ya están encapsulados y la representación más importante. Por ejemplo, considere el código de delegado de botón:

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

El delegado recibe como argumento la parte de plataforma del componente, implementa métodos comunes a ambas plataformas y los pasa a la parte de plataforma. La parte de la plataforma del componente en sí es la siguiente:

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

Aquí hay un método de renderizado con todas sus características de plataforma. La funcionalidad general del delegado se presenta en forma de un objeto a través del accesorio delegado. Un ejemplo de la parte de la plataforma de un botón para una implementación nativa de reacción:

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

En este caso, la lógica es similar, pero se utilizan componentes nativos de reacción. En ambos listados, buttonDelegate es un HOC con lógica común.

Con este enfoque en la implementación de componentes, surge la cuestión de la separación de las partes de la plataforma durante el montaje del proyecto. Es necesario asegurarse de que el paquete web utilizado por nosotros en proyectos para la web recolecte solo partes de componentes destinados a la web, mientras que el paquete metropolitano en react-native debería "enganchar" las partes de su plataforma, sin prestar atención al componente para la web.

Para resolver este problema, utilizaron la función integrada de paquete de metro, que le permite especificar el prefijo de extensión de archivo de la plataforma. En nuestro caso, metro.config.js se ve así:

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

Por lo tanto, al compilar el paquete, metro primero busca archivos con la extensión native.js y luego, si no está en el directorio actual, engancha el archivo con la extensión .js. Esta funcionalidad hizo posible colocar las partes de la plataforma de los componentes en archivos separados: la parte para la web se encuentra en el archivo .js, la parte de reacción nativa se coloca en el archivo con la extensión .native.js.

Por cierto, webpack tiene la misma funcionalidad usando NormalModuleReplacementPlugin.

Otro objetivo del enfoque multiplataforma era proporcionar un mecanismo único para diseñar componentes. En el caso de las aplicaciones web, elegimos el preprocesador sass, que finalmente se compila en CSS normal. Aquellos. para los componentes web, utilizamos los conocidos desarrolladores reaccionar className.

Los componentes nativos de reacción se diseñan a través de estilos en línea y estilo de accesorios. Era necesario combinar estos dos enfoques, haciendo posible el uso de clases de estilo para aplicaciones de Android. Para este propósito, se introdujo el concepto de styleSet, que no es más que una serie de cadenas: nombres de clase:

styleSet: Array<string>

Al mismo tiempo, se implementó el servicio StyleSet con el mismo nombre para react-native, que permite registrar nombres de clase:

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

Para los componentes web, styleSet es una matriz de nombres de clase css que están "pegados" usando la biblioteca de nombres de clase .

Dado que el proyecto es multiplataforma, es obvio que con el crecimiento de la base de código, también aumenta el número de dependencias externas. Además, las dependencias son diferentes para cada plataforma. Por ejemplo, para componentes web, se necesitan bibliotecas como style-loader, react-dom, classnames, webpack, etc. Para componentes react-native, se usa una gran cantidad de sus bibliotecas nativas, por ejemplo, react-native. Si un proyecto en el que se supone que debe usar la biblioteca de componentes tiene solo una plataforma de destino, entonces instalar todas las dependencias en otra plataforma es irracional. Para resolver este problema, utilizamos el gancho postinstall de npm, en el que se instaló un script para instalar dependencias para la plataforma especificada. Las dependencias mismas se registraron en la sección correspondiente de package.json del paquete,y la plataforma de destino debe especificarse en el paquete del proyecto.json como una matriz.
Sin embargo, este enfoque reveló un inconveniente, que posteriormente se convirtió en varios problemas durante el ensamblaje en el sistema CI. La raíz del problema fue que con package-lock.json, el script especificado en postinstall no instaló todas las dependencias registradas.

Tuve que buscar otra solución a este problema. La solución fue simple. Se aplicó un esquema de dos paquetes en el que todas las dependencias de la plataforma se colocaron en la sección de dependencias del paquete de la plataforma correspondiente. Por ejemplo, en el caso de la web, el paquete se llama components-web, en el que hay un solo archivo package.json. Contiene todas las dependencias para la plataforma web, así como el paquete principal con componentes componentes. Este enfoque nos permitió mantener la separación de dependencias y preservar la funcionalidad de package-lock.json.

En conclusión, daré un ejemplo de código JSX usando nuestra biblioteca de componentes:

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

Este fragmento de código es multiplataforma y funciona igual en una aplicación de reacción para la web y en una aplicación de Android en react-native. Si es necesario, el mismo código se puede "enrollar" en iOS.

Por lo tanto, se resolvió la tarea principal a la que nos enfrentamos: la reutilización máxima de ambos enfoques de diseño y la base de código entre varios proyectos.
Indique en los comentarios qué preguntas sobre este tema fueron interesantes de aprender en el próximo artículo.

All Articles