Desenvolvimento de uma biblioteca corporativa de componentes React. Abordagem multiplataforma

Este artigo conta a história da implementação bem-sucedida do sistema de design na empresa de um dos maiores varejistas de bricolage. Os princípios e abordagens do desenvolvimento de plataforma cruzada dos componentes da interface do usuário usando as bibliotecas React e React Native são descritos, bem como a solução para o problema de reutilizar código entre projetos para diferentes plataformas.

Primeiro, algumas palavras sobre como tudo começou e por que surgiu a idéia de implementar um design de sistema. Tudo começou com um aplicativo Android móvel para vendedores nas lojas. O aplicativo é construído na estrutura React-Native. A funcionalidade inicial foi representada por apenas alguns módulos, como procurar produtos no catálogo e cartão do produto, documento de vendas. A propósito, agora esse é um aplicativo bastante poderoso que já substituiu amplamente a funcionalidade dos balcões de informações nas lojas.

Em seguida, foram lançados projetos de aplicativos da Web para funcionários do departamento de logística, bem como vários configuradores.

Nesse estágio, apareceu um entendimento das abordagens gerais para o design desses aplicativos, bem como a presença de uma base de código bastante grande. E era lógico sistematizar o outro para reutilização adicional.

Para sistematizar a UI / UX, decidiu-se desenvolver um sistema de design. Não vou entrar em detalhes sobre o que é. Na Internet, você pode encontrar muitos artigos sobre esse tópico. Por exemplo, em Habré, o trabalho de Andrei Sundiev pode ser recomendado para leitura .

Por que projetar um sistema e quais são suas vantagens? A primeira é uma experiência comum e a sensação de usar produtos. Os usuários obtêm uma interface familiar, independentemente da aplicação: os botões parecem e funcionam como estão acostumados, o menu é aberto no lugar certo e com a dinâmica certa, os campos de entrada funcionam da maneira usual. A segunda vantagem é a introdução de certos padrões e abordagens comuns, tanto do lado do design quanto do lado do desenvolvimento. Cada nova funcionalidade é desenvolvida de acordo com cânones e abordagens já estabelecidos. Desde os primeiros dias, os novos funcionários recebem uma linha de trabalho clara. O próximo é reutilizar componentes e simplificar o desenvolvimento. Não há necessidade de "reinventar a roda" o tempo todo. Você pode criar interfaces a partir de blocos prontos com o resultado final esperado.Bem, em primeiro lugar, a principal vantagem para o cliente é economizar dinheiro e tempo.

Então, o que fizemos. De fato, criamos não apenas uma biblioteca de componentes, mas toda uma estrutura de plataforma cruzada. A estrutura é baseada em um esquema em lote. Temos 5 pacotes principais do NPM. É o núcleo da implantação de aplicativos da Web e Android multiplataforma. Pacotes de módulos, utilitários e serviços. E um pacote de componentes, que será discutido mais adiante.
Abaixo está o diagrama UML do pacote de componentes.

imagem

Ele inclui os componentes em si, alguns dos quais são independentes (elementos), e alguns são conectados entre si, bem como o núcleo interno ou "sub-núcleo".

Vamos considerar em mais detalhes o que está incluído no "subnúcleo". A primeira é a camada visual do design do sistema. Tudo aqui é sobre paleta de cores, tipografia, sistema de indentação, grades, etc. O próximo bloco são os serviços necessários para o funcionamento dos componentes, como: ComponentsConfig (configuração de componentes), StyleSet (discutirei esse conceito com mais detalhes posteriormente) e Device (um método para trabalhar com a API do dispositivo). E o terceiro bloco é de todos os tipos de ajudantes (resolvedores, geradores de estilo etc.).

imagem

Ao desenvolver a biblioteca, usamos uma abordagem atômica para o design de componentes. Tudo começou com a criação de componentes ou elementos elementares. São "partículas" elementares que são independentes uma da outra. Os principais são View, Text, Image, Icon. A seguir estão os componentes mais complexos. Cada um deles usa um ou mais elementos para construir sua estrutura. Por exemplo, botões, campos de entrada, seleções etc. O próximo nível é padrões. Eles são uma combinação de componentes para resolver qualquer problema de interface do usuário. Por exemplo, um formulário de autorização, um cabeçalho com parâmetros e configurações ou um cartão de produto projetado por um designer que pode ser usado em diferentes módulos. O último e mais difícil e ao mesmo tempo importante nível é o chamado comportamento. Estes são módulos prontos para uso,Implementar certa lógica comercial e, possivelmente, incluindo o conjunto necessário de solicitações de back-end.

imagem

Então, vamos seguir para a implementação da biblioteca de componentes. Como mencionei antes, temos duas plataformas de destino - Web e Android (nativo de reação). Se na web esses são elementos familiares a todos os desenvolvedores da web, como div, span, img, header, etc., em react-native, esses são os componentes View, Text, Image, Modal. E a primeira coisa em que concordamos é o nome dos componentes. Decidimos usar um sistema no estilo react-native, como em primeiro lugar, alguma base de componentes já foi implementada em projetos e, em segundo lugar, esses nomes são os mais universais e compreensíveis para desenvolvedores nativos da Web e reativos. Por exemplo, considere o componente Exibir. O método do componente de renderização condicional para a Web é mais ou menos assim:

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

Essa. sob o capô, isso não passa de um div com os adereços e descendentes necessários. No react-native, a estrutura é muito semelhante, apenas o componente View é usado em vez de divs:

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

Surge a pergunta: como combinar isso em um componente e ao mesmo tempo dividir a renderização?

É aqui que um padrão de reação chamado HOC ou componente de ordem superior vem para o resgate. Se você tentar desenhar um diagrama UML desse padrão, obterá algo como o seguinte:

imagem

Assim, cada componente consiste em um delegado chamado que recebe adereços de fora e é responsável pela lógica comum a ambas as plataformas, e duas partes da plataforma nas quais os métodos específicos para cada plataforma já estão encapsulados e a renderização mais importante. Por exemplo, considere o código de delegação do botão:

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

O delegado recebe como argumento a parte da plataforma do componente, implementa métodos comuns a ambas as plataformas e os passa para a parte da plataforma. A parte da plataforma do próprio componente é a seguinte:

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

Aqui está um método de renderização com todos os seus recursos de plataforma. A funcionalidade geral do delegado vem na forma de um objeto através do delegado props. Um exemplo da parte da plataforma de um botão para uma implementação nativa de reação:

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

Nesse caso, a lógica é semelhante, mas componentes nativos de reação são usados. Nas duas listagens, buttonDelegate é um HOC com lógica comum.

Com essa abordagem na implementação de componentes, surge a questão da separação das peças da plataforma durante a montagem do projeto. É necessário garantir que o webpack usado por nós em projetos para web colete apenas partes dos componentes destinados à web, enquanto o bundler metro no react-native deve "ligar" suas partes da plataforma, sem prestar atenção ao componente para web.

Para resolver esse problema, eles usaram o recurso de agregador metro integrado, que permite especificar o prefixo da extensão do arquivo da plataforma. No nosso caso, o metro.config.js fica assim:

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

Portanto, ao criar o pacote configurável, o metro primeiro procura arquivos com a extensão native.js e, se não estiver no diretório atual, anexa o arquivo à extensão .js. Essa funcionalidade tornou possível colocar as partes da plataforma dos componentes em arquivos separados: a parte da Web está localizada no arquivo .js, a parte react-native é colocada no arquivo com a extensão .native.js.

A propósito, o webpack tem a mesma funcionalidade usando o NormalModuleReplacementPlugin.

Outro objetivo da abordagem de plataforma cruzada era fornecer um mecanismo único para criar componentes de estilo. No caso de aplicativos da web, escolhemos o pré-processador sass, que finalmente é compilado em css regular. Essa. para componentes da Web, usamos os familiares desenvolvedores do react className.

Os componentes nativos do React são estilizados por meio de estilos inline e adereços. Foi necessário combinar essas duas abordagens, possibilitando o uso de classes de estilo para aplicativos Android. Para esse propósito, foi introduzido o conceito de styleSet, que nada mais é do que uma matriz de strings - nomes de classes:

styleSet: Array<string>

Ao mesmo tempo, o serviço StyleSet de mesmo nome foi implementado para react-native, o que permite registrar nomes de classes:

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 componentes web, styleSet é uma matriz de nomes de classe css que são “colados” usando o classnames biblioteca .

Como o projeto é multiplataforma, é óbvio que, com o crescimento da base de código, o número de dependências externas também aumenta. Além disso, as dependências são diferentes para cada plataforma. Por exemplo, para componentes da Web, são necessárias bibliotecas como loader de estilos, react-dom, nomes de classes, webpack etc. Para componentes nativos do react, um grande número de suas bibliotecas "nativas" é usado, por exemplo, o próprio nativo do react. Se um projeto no qual ela deve usar a biblioteca de componentes tiver apenas uma plataforma de destino, a instalação de todas as dependências em outra plataforma será irracional. Para resolver esse problema, usamos o gancho pós-instalação do próprio npm, no qual um script foi instalado para instalar dependências para a plataforma especificada. As próprias dependências foram registradas na seção correspondente do package.json do pacote,e a plataforma de destino deve ser especificada no projeto package.json como uma matriz.
No entanto, essa abordagem revelou uma desvantagem, que posteriormente se transformou em vários problemas durante a montagem no sistema de IC. A raiz do problema foi que, com o package-lock.json, o script especificado em postinstall não instalou todas as dependências registradas.

Eu tive que procurar outra solução para esse problema. A solução foi simples. Foi aplicado um esquema de dois pacotes no qual todas as dependências da plataforma foram colocadas na seção dependências do pacote de plataforma correspondente. Por exemplo, no caso da web, o pacote é chamado components-web, no qual existe um único arquivo package.json. Ele contém todas as dependências da plataforma web, bem como o pacote principal com componentes componentes. Essa abordagem nos permitiu manter a separação de dependências e preservar a funcionalidade do package-lock.json.

Concluindo, darei um exemplo de código JSX usando nossa 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>

Esse snippet de código é multiplataforma e funciona da mesma maneira em um aplicativo de reação para a Web e em um aplicativo Android em nativo de reação. Se necessário, o mesmo código pode ser "encerrado" no iOS.

Assim, a principal tarefa que nos confrontou foi resolvida - a reutilização máxima das abordagens de design e a base de código entre os vários projetos.
Indique nos comentários que perguntas sobre este tópico foram interessantes de aprender no próximo artigo.

All Articles