Comment structurer votre projet et gérer les ressources statiques dans React Native

React et React Native ne sont que des frameworks, et ils ne dictent pas comment nous devons structurer nos projets. Tout dépend de vos goûts personnels et du projet sur lequel vous travaillez.

Dans cet article, nous verrons comment structurer un projet et comment gérer les actifs locaux. Ceci n'est bien sûr pas gravé dans le marbre, et vous êtes libre d'appliquer uniquement les pièces qui vous conviennent. J'espère que vous apprenez quelque chose.

Pour un projet bootstrap avec react-native init, nous n'obtenons que la structure de base.

Il y a le iosdossier pour les projets Xcode, le androiddossier pour les projets Android et un index.jset un App.jsfichier pour le point de départ de React Native.

ios/ android/ index.js App.js

En tant que personne ayant travaillé avec des natifs sur Windows Phone, iOS et Android, je trouve que structurer un projet revient à séparer les fichiers par type ou fonctionnalité.

type vs fonctionnalité

La séparation par type signifie que nous organisons les fichiers par type. S'il s'agit d'un composant, il existe des conteneurs et des fichiers de présentation. S'il s'agit de Redux, il existe des fichiers d'action, de réduction et de stockage. S'il s'agit d'une vue, il existe des fichiers JavaScript, HTML et CSS.

Regrouper par type

redux actions store reducers components container presentational view javascript html css

De cette façon, nous pouvons voir le type de chaque fichier et exécuter facilement un script vers un certain type de fichier. Ceci est général pour tous les projets, mais cela ne répond pas à la question «de quoi s'agit-il?» Est-ce une application de nouvelles? Est-ce une application de fidélité? S'agit-il de suivi nutritionnel?

L'organisation des fichiers par type est pour une machine, pas pour un humain. Plusieurs fois, nous travaillons sur une fonctionnalité, et trouver des fichiers à corriger dans plusieurs répertoires est un problème. C'est aussi une douleur si nous prévoyons de créer un cadre à partir de notre projet, car les fichiers sont répartis dans de nombreux endroits.

Grouper par fonctionnalité

Une solution plus raisonnable consiste à organiser les fichiers par fonctionnalité. Les fichiers liés à une fonction doivent être placés ensemble. Et les fichiers de test doivent rester proches des fichiers source. Consultez cet article pour en savoir plus.

Une fonctionnalité peut être liée à la connexion, à l'inscription, à l'intégration ou au profil d'un utilisateur. Une fonctionnalité peut contenir des sous-fonctionnalités tant qu'elles appartiennent au même flux. Si nous voulions déplacer la sous-fonctionnalité, ce serait facile, car tous les fichiers associés sont déjà regroupés.

Ma structure de projet typique basée sur des fonctionnalités ressemble à ceci:

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 [email protected] [email protected] [email protected] [email protected] scripts images.js clear.js

Outre les fichiers traditionnels App.jset index.jset les dossiers ios1et android, je place tous les fichiers source dans le srcdossier. À l'intérieur, srcj'ai resdes ressources, librarydes fichiers communs utilisés dans toutes les fonctionnalités et screensun écran de contenu.

Le moins de dépendances possible

Étant donné que React Native dépend fortement de tonnes de dépendances, j'essaie d'être assez conscient lors de l'ajout de plus. Dans mon projet, j'utilise uniquement react-navigationpour la navigation. Et je ne suis pas fan reduxcar cela ajoute une complexité inutile. N'ajoutez une dépendance que lorsque vous en avez vraiment besoin, sinon vous vous installez simplement pour plus de problèmes que de valeur.

Ce que j'aime chez React, ce sont les composants. Un composant est l'endroit où nous définissons la vue, le style et le comportement. React a un style en ligne - c'est comme utiliser JavaScript pour définir des scripts, HTML et CSS. Cela correspond à l'approche fonctionnelle que nous visons. C'est pourquoi je n'utilise pas de composants stylisés. Étant donné que les styles ne sont que des objets JavaScript, nous pouvons simplement partager des styles de commentaires dans library.

src

J'aime beaucoup Android, donc je nomme srcet resrespecte ses conventions de dossier.

react-native initmet en place Babel pour nous. Mais pour un projet JavaScript typique, il est bon d'organiser les fichiers dans le srcdossier. Dans mon electron.jsapplication IconGenerator, je place les fichiers source dans le srcdossier. Cela aide non seulement en termes d'organisation, mais aide également babel à transpiler tout le dossier en même temps. Juste une commande et j'ai les fichiers srctranspilés disten un clin d'œil.

babel ./src --out-dir ./dist --copy-files

Écran

React est basé sur des composants. Ouaip. Il existe des composants de conteneur et de présentation, mais nous pouvons composer des composants pour créer des composants plus complexes. Ils finissent généralement par s'afficher en plein écran. Il s'appelle Pagedans Windows Phone, ViewControllersous iOS et Activitysous Android. Le guide React Native mentionne très souvent l'écran comme quelque chose qui couvre tout l'espace:

Les applications mobiles sont rarement constituées d'un seul écran. La gestion de la présentation et de la transition entre plusieurs écrans est généralement gérée par ce que l'on appelle un navigateur.

index.js ou pas?

Chaque écran est considéré comme le point d'entrée de chaque fonction. Vous pouvez renommer le LoginScreen.jsen index.jsen tirant parti de la fonctionnalité du module Node:

Les modules n'ont pas besoin d'être des fichiers. Nous pouvons également créer un find-medossier sous node_moduleset y placer un index.jsfichier. La même require('find-me')ligne utilisera le index.jsfichier de ce dossier

Donc, au lieu de import LoginScreen from './screens/LoginScreen', nous pouvons simplement faire import LoginScreen from './screens'.

L'utilisation des index.jsrésultats dans l'encapsulation et fournit une interface publique pour la fonctionnalité. Tout cela est un goût personnel. Je préfère moi-même un nom explicite pour un fichier, d'où le nom LoginScreen.js.

Navigateur

react-navigation semble être le choix le plus populaire pour gérer la navigation dans une application React Native. Pour une fonctionnalité comme l'intégration, il y a probablement de nombreux écrans gérés par une navigation de pile, donc il y en a OnboardingNavigator.

Vous pouvez considérer Navigator comme quelque chose qui regroupe des sous-écrans ou des fonctionnalités. Puisque nous regroupons par fonctionnalité, il est raisonnable de placer Navigator dans le dossier de fonctionnalités. Cela ressemble essentiellement à ceci:

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)

bibliothèque

C'est la partie la plus controversée de la structuration d'un projet. Si vous ne le faites pas comme le nom library, vous pouvez le nommer utilities, common, citadel, whatever...

This is not meant for homeless files, but it is where we place common utilities and components that are used by many features. Things like atomic components, wrappers, quick fixes function, networking stuff, and login info are used a lot, and it’s hard to move them to a specific feature folder. Sometimes we just need to be practical and get the work done.

In React Native, we often need to implement a button with an image background in many screens. Here is a simple one that stays inside library/components/ImageButton.js . The components folder is for reusable components, sometimes called atomic components. According to React naming conventions, the first letter should be uppercase.

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 (   {this.props.title}    ) } } const styles = StyleSheet.create({ view: { position: 'absolute', backgroundColor: 'transparent' }, image: { }, touchable: { alignItems: 'center', justifyContent: 'center' }, text: { color: colors.button, fontSize: 18, textAlign: 'center' } })

And if we want to place the button at the bottom, we use a utility function to prevent code duplication. Here is library/utils/moveToBottom.js:

import React from 'react' import { View, StyleSheet } from 'react-native' function moveToBottom(component) { return (  {component}  ) } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'flex-end', marginBottom: 36 } }) export default moveToBottom

Use package.json to avoid relative path

Then somewhere in the src/screens/onboarding/term/Term.js , we can import by using relative paths:

import moveToBottom from '../../../../library/utils/move' import ImageButton from '../../../../library/components/ImageButton'

This is a big red flag in my eyes. It’s error prone, as we need to calculate how many .. we need to perform. And if we move feature around, all of the paths need to be recalculated.

Since library is meant to be used many places, it’s good to reference it as an absolute path. In JavaScript there are usually 1000 libraries to a single problem. A quick search on Google reveals tons of libraries to tackle this issue. But we don’t need another dependency as this is extremely easy to fix.

The solution is to turn library into a module so node can find it. Adding package.json to any folder makes it into a Node module . Add package.json inside the library folder with this simple content:

{ "name": "library", "version": "0.0.1" }

Now in Term.js we can easily import things from library because it is now a module:

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 (  {strings.onboarding.term.heading.toUpperCase()} { moveToBottom(  ) }  ) } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center' }, heading: {...palette.heading, ...{ marginTop: 72 }} })

res

You may wonder what res/colors, res/strings , res/images and res/fonts are in the above examples. Well, for front end projects, we usually have components and style them using fonts, localised strings, colors, images and styles. JavaScript is a very dynamic language, and it’s easy to use stringly types everywhere. We could have a bunch of #00B75Dcolor across many files, or Fira as a fontFamily in many Text components. This is error-prone and hard to refactor.

Let’s encapsulate resource usage inside the res folder with safer objects. They look like the examples below:

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

Like library , res files can be access from anywhere, so let’s make it a module . Add package.json to the res folder:

{ "name": "res", "version": "0.0.1" }

so we can access resource files like normal modules:

import strings from 'res/strings' import palette from 'res/palette' import images from 'res/images'

Group colors, images, fonts with palette

The design of the app should be consistent. Certain elements should have the same look and feel so they don’t confuse the user. For example, the heading Text should use one color, font, and font size. The Image component should use the same placeholder image. In React Native, we already use the name styles with const styles = StyleSheet.create({}) so let’s use the name palette.

Below is my simple palette. It defines common styles for heading and 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

And then we can use them in our screen:

const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center' }, heading: {...palette.heading, ...{ marginTop: 72 }} })

Here we use the object spread operator to merge palette.heading and our custom style object. This means that we use the styles from palette.heading but also specify more properties.

If we were to reskin the app for multiple brands, we could have multiple palettes. This is a really powerful pattern.

Generate images

You can see that in /src/res/images.js we have properties for each image in the src/res/images folder:

const images = { button: require('./images/button.png'), logo: require('./images/logo.png'), placeholder: require('./images/placeholder.png') } export default images

This is tedious to do manually, and we have to update if there’s changes in image naming convention. Instead, we can add a script to generate the images.js based on the images we have. Add a file at the root of the project /scripts/images.js:

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

The cool thing about Node is that we have access to the fs module, which is really good at file processing. Here we simply traverse through images, and update /src/res/images.js accordingly.

Whenever we add or change images, we can run:

node scripts/images.js

And we can also declare the script inside our main package.json :

"scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", "test": "jest", "lint": "eslint *.js **/*.js", "images": "node scripts/images.js" }

Now we can just run npm run images and we get an up-to-date images.js resource file.

How about custom fonts

React Native has some custom fonts that may be good enough for your projects. You can also use custom fonts.

One thing to note is that Android uses the name of the font file, but iOS uses the full name. You can see the full name in Font Book app, or by inspecting in running app

for (NSString* family in [UIFont familyNames]) { NSLog(@"%@", family); for (NSString* name in [UIFont fontNamesForFamilyName: family]) { NSLog(@"Family name: %@", name); } }

For custom fonts to be registered in iOS, we need to declare UIAppFonts in Info.plist using the file name of the fonts, and for Android, the fonts need to be placed at app/src/main/assets/fonts .

It is good practice to name the font file the same as full name. React Native is said to dynamically load custom fonts, but in case you get “Unrecognized font family”, then simply add those fonts to target within Xcode.

Doing this by hand takes time, luckily we have rnpm that can help. First add all the fonts inside res/fonts folder. Then simply declare rnpm in package.json and run react-native link . This should declare UIAppFonts in iOS and move all the fonts into app/src/main/assets/fonts for Android.

"rnpm": { "assets": [ "./src/res/fonts/" ] }

L'accès aux polices par nom est sujet aux erreurs, nous pouvons créer un script similaire à ce que nous avons fait avec des images pour générer une adhésion plus sûre. Ajouter fonts.jsà notre scriptsdossier

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

Vous pouvez maintenant utiliser une police personnalisée via l' Respace de noms.

import R from 'res/R' const styles = StyleSheet.create({ text: { fontFamily: R.fonts.FireCodeNormal } })

L'espace de noms R

Cette étape dépend de vos goûts personnels, mais je la trouve plus organisée si nous introduisons l'espace de noms R, tout comme le fait Android pour les actifs avec la classe R générée.

Une fois que vous avez externalisé les ressources de votre application, vous pouvez y accéder à l'aide des ID de ressources générés dans la Rclasse de votre projet . Ce document vous montre comment regrouper vos ressources dans votre projet Android et fournir des ressources alternatives pour des configurations d'appareils spécifiques, puis y accéder à partir de votre code d'application ou d'autres fichiers XML.

De cette façon, nous allons faire un fichier appelé R.jsdans 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

Et accédez-y dans l'écran:

import R from 'res/R' render() { return (    {R.strings.onboarding.welcome.title.toUpperCase()} ) }

Remplacez stringspar R.strings, colorspar R.colorset imagespar R.images. Avec l'annotation R, il est clair que nous accédons aux actifs statiques à partir du bundle d'applications.

Cela correspond également à la convention Airbnb pour le singleton, car notre R est maintenant comme une constante globale.

23.8 Utilisez PascalCase lorsque vous exportez un constructeur / classe / singleton / bibliothèque de fonctions / objet nu.
const AirbnbStyleGuide = { es6: { }, } export default AirbnbStyleGuide

Où aller en partant d'ici

Dans cet article, je vous ai montré comment je pense que vous devez structurer les dossiers et les fichiers dans un projet React Native. Nous avons également appris à gérer les ressources et à y accéder de manière plus sûre. J'espère que vous l'avez trouvé utile. Voici quelques ressources supplémentaires à explorer davantage:

  • Organisation d'un projet React Native
  • Structurer les projets et nommer les composants dans React
  • Utilisation d'index.js pour des interfaces amusantes et publiques

Since you are here, you may enjoy my other articles

  • 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

If you like this post, consider visiting my other articles and apps ?