Artigo original: https://www.freecodecamp.org/news/how-to-structure-your-project-and-manage-static-resources-in-react-native-6f4cfc947d92/
Escrito por: Khoa Pham
React e React Native são apenas frameworks, e não ditam como devemos estruturar nossos projetos. Tudo depende do seu gosto pessoal e do projeto em que você está trabalhando.
Neste post, vamos abordar como estruturar um projeto e como gerenciar ativos locais. Isso, claro, não está escrito em pedra, e você é livre para aplicar apenas as peças que lhe convierem. Espero que você aprenda alguma coisa.
Para um projeto inicializado com react-native-init
, obtemos apenas a estrutura básica (texto em inglês).
Há a pasta ios
para projetos do Xcode, a pasta android
para projetos para o Android e arquivos index.js
e App.js
para o ponto de partida do React Native.
ios/
android/
index.js
App.js
Por já ter trabalhado com arquivos nativos no Windows Phone, no iOS e no Android, penso que estruturar um projeto se resume a separar arquivos por tipo ou por recurso.
Tipo x recurso
Separar por tipo significa organizarmos os arquivos por seu tipo. Se o tipo for um componente, haverá arquivos de contêiner e arquivos de apresentação. Se for do Redux, haverá arquivos de ação, reducers e arquivos de armazenamento. Se for uma view, haverá arquivos JavaScript, HTML e CSS.
Agrupar por tipo
redux
actions
store
reducers
components
container
presentational
view
javascript
html
css
Desse modo, podemos ver o tipo de cada arquivo e executar facilmente um script para determinado tipo de arquivo. Isso vale para todos os projetos, mas não responde à pergunta "Qual é o objetivo deste projeto?" É uma aplicação de notícias? É uma aplicação de fidelização? É um acompanhamento nutricional?
Organizar arquivos por tipo é útil para máquinas, mas não para humanos. Muitas vezes, trabalhamos em recursos. Encontrar arquivos para corrigir em diversos diretórios é muito cansativo. Também é um incômodo quando planejamos fazer a estrutura de nosso projeto, já que os arquivos estão espalhados por muitos lugares.
Agrupar por recurso
Uma solução mais razoável é organizar arquivos por recurso. Os arquivos relacionados a um recurso devem ser colocados juntos. Os arquivos de teste devem ficar próximos aos arquivos de origem. Confira este artigo (texto em inglês) para saber mais.
Um recurso pode estar relacionado ao login, inscrição, integração ou ao perfil de um usuário. Um recurso pode conter sub-recursos, desde que pertençam ao mesmo fluxo. Se quiséssemos mover o sub-recurso, seria fácil, pois todos os arquivos relacionados já estariam agrupados.
Minha estrutura típica de projeto baseada em recursos tem a seguinte aparência:
index.js
App.js
ios/
android/
src
screens
login
LoginScreen.js
LoginNavigator.js
onboarding
OnboardingNavigator
welcome
WelcomeScreen.js
term
TermScreen.js
notification
NotificationScreen.js
main
MainNavigator.js
news
NewsScreen.js
profile
ProfileScreen.js
search
SearchScreen.js
library
package.json
components
ImageButton.js
RoundImage.js
utils
moveToBottom.js
safeArea.js
networking
API.js
Auth.js
res
package.json
strings.js
colors.js
palette.js
fonts.js
images.js
images
logo@2x.png
logo@3x.png
button@2x.png
button@3x.png
scripts
images.js
clear.js
Além dos arquivos App.js
e index.js
e das pastas ios1
e android
tradicionais, coloquei todos os arquivos de origem na pasta src
. Dentro de src
, tenho res
, para recursos (do inglês, resources), library
, para arquivos comuns usados em mais de um recurso, e screens
, para a tela de conteúdo.
O menor número de dependências possível
Como o React Native é fortemente dependente de uma infinidade de dependências, tento tomar muito cuidado ao adicionar mais dependências. No meu projeto, uso apenas react-navigation
para navegação. Não sou um fã do redux
. Acho que ele adiciona uma complexidade desnecessária. Só adicione uma dependência quando você realmente precisar dela. Caso contrário, você está apenas se preparando para ter mais problemas.
O que eu gosto no React são os componentes. Um componente é onde definimos visão, estilo e comportamento. O React tem estilo em linha — é como usar JavaScript para definir scripts, HTML e CSS. Isso se encaixa na abordagem de recursos que estamos buscando. É por isso que eu não uso o styled-components. Como os estilos são apenas objetos JavaScript, podemos simplesmente compartilhar estilos de comentário em library
.
src
Gosto muito do Android. Por isso, coloco os nomes src
e res
para corresponder às convenções de pasta do sistema.
O react-native init
configura o babel
para nós. Porém, em um projeto em JavaScript típico, é bom organizar os arquivos na pasta src
. Na minha aplicação IconGenerator, que usa o electron.js
, coloquei os arquivos de origem dentro da pasta src
. Isso não só ajuda em termos de organização, mas também ajuda o babel
a transpilar toda a pasta de uma só vez. Basta um comando e eu tenho os arquivos de src
transpilados para dist
em um piscar de olhos.
babel ./src --out-dir ./dist --copy-files
Screen
O React é baseado em componentes. Sim. Existem componentes de contêiner e de apresentação (texto em inglês), mas podemos compor componentes para criar componentes mais complexos. Eles, geralmente, terminam aparecendo em tela cheia. Chama-se Page
no Windows Phone, ViewController
no iOS e Activity
no Android. O guia do React Native menciona a tela (em inglês, screen) com muita frequência como algo que cobre todo o espaço:
As aplicações para dispositivos móveis raramente são compostas de uma única tela. O gerenciamento da apresentação e da transição entre várias telas normalmente é manipulado pelo que é conhecido como navegador.
Usar ou não usar o index.js?
Cada tela é considerada o ponto de entrada para um recurso. Você pode renomear LoginScreen.js
como index.js
para aproveitar o recurso de módulos do Node:
find-me
node_modules
index.js
require('find-me')
index.js
Assim, em vez de import LoginScreen from './screens/LoginScreen'
, podemos simplesmente usar import LoginScreen from './screens'
.
Usar o index.js
resulta em encapsulamento e fornece uma interface pública para o recurso. Isso tudo se baseia em gosto pessoal. Eu mesmo prefiro nomear explicitamente um arquivo – daí o nome LoginScreen.js
.
Navigator
O react-navigation parece ser a escolha mais popular para lidar com a navegação em uma aplicação do React Native. Para um recurso como a integração (do inglês, onboarding), provavelmente, existem muitas telas gerenciadas por uma navegação pela pilha. Assim, temos o OnboardingNavigator
.
Pense no Navigator como algo que agrupe subtelas ou recursos. Como estamos agrupando por recurso, é natural colocarmos o navegador dentro da pasta de recursos. A aparência, basicamente, é esta:
import { createStackNavigator } from 'react-navigation'
import Welcome from './Welcome'
import Term from './Term'
const routeConfig = {
Welcome: {
screen: Welcome
},
Term: {
screen: Term
}
}
const navigatorConfig = {
navigationOptions: {
header: null
}
}
export default OnboardingNavigator = createStackNavigator(routeConfig, navigatorConfig)
library
Esta é a parte mais polêmica da estruturação de um projeto. Se você não gosta do nome library
, pode chamá-la de utilities
, common
, citadel
, ou o que quer que seja...
Essa pasta não é destinada a arquivos sem outro lugar para ficar, mas é onde colocamos utilitários e componentes comuns, que são usados por muitos recursos. Coisas como componentes atômicos, wrappers, função de correções rápidas, coisas relacionadas à rede e informações de login são muito usadas, sendo difícil movê-las para uma pasta de recursos específica. Às vezes, só precisamos ser práticos e fazer o trabalho.
No React Native, muitas vezes, precisamos implementar um botão com uma imagem de fundo em muitas telas. Aqui está um botão simples, colocado em library/components/ImageButton.js
. A pasta components
é para componentes reutilizáveis, às vezes chamados de componentes atômicos. De acordo com as convenções de nomenclatura do React, a primeira letra deve ser maiúscula.
import React from 'react'
import { TouchableOpacity, View, Image, Text, StyleSheet } from 'react-native'
import images from 'res/images'
import colors from 'res/colors'
export default class ImageButton extends React.Component {
render() {
return (
<TouchableOpacity style={styles.touchable} onPress={this.props.onPress}>
<View style={styles.view}>
<Text style={styles.text}>{this.props.title}</Text>
</View>
<Image
source={images.button}
style={styles.image} />
</TouchableOpacity>
)
}
}
const styles = StyleSheet.create({
view: {
position: 'absolute',
backgroundColor: 'transparent'
},
image: {
},
touchable: {
alignItems: 'center',
justifyContent: 'center'
},
text: {
color: colors.button,
fontSize: 18,
textAlign: 'center'
}
})
Se quisermos colocar o botão na parte inferior, usamos uma função utilitária para evitar a duplicação de código. Aqui temos a library/utils/moveToBottom.js
:
import React from 'react'
import { View, StyleSheet } from 'react-native'
function moveToBottom(component) {
return (
<View style={styles.container}>
{component}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'flex-end',
marginBottom: 36
}
})
export default moveToBottom
Usando o package.json para evitar caminhos relativos
Em algum ponto de src/screens/onboarding/term/Term.js
, podemos importar usando caminhos relativos:
import moveToBottom from '../../../../library/utils/move'
import ImageButton from '../../../../library/components/ImageButton'
Aqui, temos um problema bem visível. A chance de cometermos erros é muito grande, pois precisamos calcular de quantos ..
precisamos. Se movermos o recurso para outro lugar, todos esses caminhos precisarão ser recalculados.
Como library
é para ser usado em muitos lugares, é bom referenciá-la como um caminho absoluto. Em JavaScript, geralmente, existem milhares de bibliotecas para um único problema. Uma rápida pesquisa no Google revela centenas de bibliotecas para resolver esse problema. Porém, não precisamos de outra dependência, pois isso é extremamente fácil de corrigir.
A solução é transformar library
em um módulo module
para que o node
consiga encontrá-la. Adicionar o package.json
a qualquer pasta a torna um módulo
no Node. Adicionamos o package.json
dentro da pasta library
com este conteúdo simples:
{
"name": "library",
"version": "0.0.1"
}
Agora, em Term.js
, podemos importar as coisas de library
facilmente, pois, agora, a pasta se tornou um módulo
:
import React from 'react'
import { View, StyleSheet, Image, Text, Button } from 'react-native'
import strings from 'res/strings'
import palette from 'res/palette'
import images from 'res/images'
import ImageButton from 'library/components/ImageButton'
import moveToBottom from 'library/utils/moveToBottom'
export default class Term extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.heading}>{strings.onboarding.term.heading.toUpperCase()}</Text>
{
moveToBottom(
<ImageButton style={styles.button} title={strings.onboarding.term.button.toUpperCase()} />
)
}
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center'
},
heading: {...palette.heading, ...{
marginTop: 72
}}
})
res
Você pode estar se perguntando o que são res/colors
, res/strings
, res/images
e res/fonts
nos exemplos acima. Bem, para projetos de front-end, geralmente temos componentes e os estilizamos usando fontes, strings com localização (traduzidas), cores, imagens e estilos propriamente ditos. O JavaScript é uma linguagem muito dinâmica, sendo fácil usar tipos em formato string em qualquer lugar. Podemos ter várias definições do tipo color: #00B75D;
em vários arquivos, ou fontFamily: Fira;
em vários componentes do tipo Text
. Isso também leva a erros e é difícil de refatorar.
Vamos encapsular o uso dos recursos na pasta res
com objetos mais seguros. Veja a aparência disso nos exemplos a seguir:
res/colors
const colors = {
title: '#00B75D',
text: '#0C222B',
button: '#036675'
}
export default colors
res/strings
const strings = {
onboarding: {
welcome: {
heading: 'Welcome',
text1: "What you don't know is what you haven't learn",
text2: 'Visit my GitHub at https://github.com/onmyway133',
button: 'Log in'
},
term: {
heading: 'Terms and conditions',
button: 'Read'
}
}
}
export default strings
res/fonts
const fonts = {
title: 'Arial',
text: 'SanFrancisco',
code: 'Fira'
}
export default fonts
res/images
const images = {
button: require('./images/button.png'),
logo: require('./images/logo.png'),
placeholder: require('./images/placeholder.png')
}
export default images
Assim como em library
, os arquivos de res
podem ser acessados de qualquer lugar. Assim, vamos tornar a pasta res
um módulo
. Adicione o package.json
à pasta res
:
{
"name": "res",
"version": "0.0.1"
}
Desse modo, podemos acessar a pasta de recursos como se fosse um módulo normal:
import strings from 'res/strings'
import palette from 'res/palette'
import images from 'res/images'
Agrupar cores, imagens e fontes com uma paleta
O design da aplicação deve ser consistente. Certos elementos devem ter a mesma aparência para não confundir o usuário. Por exemplo, os títulos do componente Text
devem usar uma cor, fonte e tamanho de fonte iguais. O componente Image
deve usar a mesma imagem placeholder. No React Native, já usamos o nome styles
em const styles = StyleSheet.create({})
. Então, vamos usar o nome palette
(em português, paleta).
Abaixo, vemos minha paleta simplificada. Ela define estilos comuns para os títulos e para Text
:
res/palette
import colors from './colors'
const palette = {
heading: {
color: colors.title,
fontSize: 20,
textAlign: 'center'
},
text: {
color: colors.text,
fontSize: 17,
textAlign: 'center'
}
}
export default palette
Também podemos usá-los em nossa tela:
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center'
},
heading: {...palette.heading, ...{
marginTop: 72
}}
})
Aqui, usamos o operador spread em um objeto para unir palette.heading
e nosso objeto de estilo personalizado. Isso quer dizer que usamos estilos de palette.heading
, mas também especificamos outras propriedades.
Se fôssemos remodelar a aplicação para diversas marcas, poderíamos ter diversas paletas. Esse é um padrão muito útil.
Geração de imagens
Podemos ver que, em /src/res/images.js
, temos propriedades para cada imagem na pasta src/res/images
:
const images = {
button: require('./images/button.png'),
logo: require('./images/logo.png'),
placeholder: require('./images/placeholder.png')
}
export default images
É um tédio fazer isso manualmente. Teríamos que atualizar se houvesse mudanças na convenção de nomenclatura de imagem. Em vez disso, podemos adicionar um script para gerar images.js
com base nas imagens que temos. Adicione um arquivo /scripts/images.js
na raiz do projeto:
const fs = require('fs')
const imageFileNames = () => {
const array = fs
.readdirSync('src/res/images')
.filter((file) => {
return file.endsWith('.png')
})
.map((file) => {
return file.replace('@2x.png', '').replace('@3x.png', '')
})
return Array.from(new Set(array))
}
const generate = () => {
let properties = imageFileNames()
.map((name) => {
return `${name}: require('./images/${name}.png')`
})
.join(',\n ')
const string = `const images = {
${properties}
}
export default images
`
fs.writeFileSync('src/res/images.js', string, 'utf8')
}
generate()
No Node, temos um recurso interessante que é o acesso ao módulo fs
, muito útil no processamento de arquivos. Aqui, simplesmente percorremos as imagens e atualizamos /src/res/images.js
conforme o caso.
Sempre que adicionarmos ou alterarmos imagens, podemos executar:
node scripts/images.js
Também podemos declarar o script dentro de nosso package.json
principal:
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
"lint": "eslint *.js **/*.js",
"images": "node scripts/images.js"
}
Agora, basta executar npm run images
e obtemos o arquivo de recurso images.js
atualizado.
Fontes personalizadas
O React Native tem algumas fontes personalizadas (texto em inglês) que podem ser boas o suficiente para seu projeto. Você também pode usar fontes personalizadas.
Uma coisa que se deve perceber é que o Android usa o nome do arquivo de fonte, mas o iOS usa o nome completo. Você pode ver o nome completo na aplicação Font Book ou inspecionando na aplicação em execução
for (NSString* family in [UIFont familyNames]) {
NSLog(@"%@", family);
for (NSString* name in [UIFont fontNamesForFamilyName: family]) {
NSLog(@"Family name: %@", name);
}
}
Para fontes personalizadas a serem registradas no iOS, precisamos declarar UIAppFonts
em Info.plist
usando o nome do arquivo das fontes. Para o Android, as fontes precisam ser colocadas em app/src/main/assets/fonts
.
É uma boa prática nomear o arquivo de fonte da mesma forma que o nome completo. Diz-se que o React Native carrega dinamicamente fontes personalizadas, mas, no caso de você obter um "Unrecognized font family" (família de fontes não reconhecidas), basta adicionar essas fontes ao destino dentro do Xcode.
Fazer isso a mão leva tempo. Por sorte, temos o rnpm, que pode ser útil. Primeiro, adicione todas as fontes na pasta res/fonts
. Depois, basta declarar o rnpm
no package.json
e executar react-native link
. Isso deverá declarar o UIAppFonts
no iOS e mover todas as fontes para app/src/main/assets/fonts
para o Android.
"rnpm": {
"assets": [
"./src/res/fonts/"
]
}
Acessar fontes pelo nome é propenso a erros. Podemos criar um script semelhante ao que fizemos com as imagens para gerar um acesso mais seguro. Adicione fonts.js
à nossa pasta scripts
:
const fs = require('fs')
const fontFileNames = () => {
const array = fs
.readdirSync('src/res/fonts')
.map((file) => {
return file.replace('.ttf', '')
})
return Array.from(new Set(array))
}
const generate = () => {
const properties = fontFileNames()
.map((name) => {
const key = name.replace(/\s/g, '')
return `${key}: '${name}'`
})
.join(',\n ')
const string = `const fonts = {
${properties}
}
export default fonts
`
fs.writeFileSync('src/res/fonts.js', string, 'utf8')
}
generate()
Agora, você pode usar as fontes personalizadas por meio do namespace R
.
import R from 'res/R'
const styles = StyleSheet.create({
text: {
fontFamily: R.fonts.FireCodeNormal
}
})
O namespace R
Essa etapa depende do gosto pessoal, mas acho mais organizada se introduzirmos o namespace R, assim como o Android faz para ativos com a classe R gerada.
Ao externalizar os recursos da sua aplicação, você pode acessá-los usando os IDs dos recursos gerados na classe R
do seu projeto. Esse documento mostra como agrupar seus recursos no seu projeto para Android e fornece recursos alternativos para configurações de dispositivos específicos, além de mostrar como acessá-los a partir do código de sua aplicação ou de outros arquivos XML.
Desse modo, vamos criar um arquivo chamado R.js
em src/res
:
import strings from './strings'
import images from './images'
import colors from './colors'
import palette from './palette'
const R = {
strings,
images,
colors,
palette
}
export default R
Depois, vamos acessá-lo na tela:
import R from 'res/R'
render() {
return (
<SafeAreaView style={styles.container}>
<Image
style={styles.logo}
source={R.images.logo} />
<Image
style={styles.image}
source={R.images.placeholder} />
<Text style={styles.title}>{R.strings.onboarding.welcome.title.toUpperCase()}</Text>
)
}
Substituímos strings
por R.strings
, colors
por R.colors
e images
por R.images
. Com a anotação de R, fica claro que estamos acessando ativos estáticos a partir do pacote da aplicação.
Isso também corresponde à convenção da Airbnb para os singletons, já que nosso R agora é uma constante global.
23.8 Use PascalCase ao exportar um construtor/classe/singleton/função/biblioteca/objeto puro.
const AirbnbStyleGuide = {
es6: {
},
}
export default AirbnbStyleGuide
O que mais podemos ver?
Neste artigo, busquei mostrar como penso que deve ser a estruturação de pastas e arquivos em um projeto do React Native. Também aprendemos a gerenciar recursos e a acessá-los de maneira mais segura. Espero que você tenha achado este artigo útil. Aqui estão mais alguns recursos para explorar mais (textos em inglês):
- Organizing a React Native Project
- Structuring projects and naming components in React
- Using index.js for Fun and Public Interfaces
Já que você está aqui, que tal dar uma olhada em outros artigos que escrevi (textos em inglês)?
- Deploying React Native to Bitrise, Fabric, CircleCI
- Position element at the bottom of the screen using Flexbox in React Native
- Setting up ESLint and EditorConfig in React Native projects
- Firebase SDK with Firestore for React Native apps in 2018
Se gostou deste artigo, dê uma olhada também em outros artigos de minha autoria e nas minhas aplicações. Obrigado!