Як структурувати свій проект та керувати статичними ресурсами в React Native

React і React Native - це просто рамки, і вони не визначають, як нам структурувати наші проекти. Все залежить від вашого особистого смаку та проекту, над яким ви працюєте.

У цій публікації ми розглянемо, як структурувати проект та як управляти місцевими активами. Це, звичайно, не написано каменем, і ви можете вільно застосовувати лише ті частини, які вам підходять. Сподіваюся, ти чогось навчишся.

Для завантаженого проекту react-native init, ми отримуємо лише базову структуру.

Існує iosпапка для проектів Xcode, androidпапка для проектів Android, index.jsа також App.jsфайл для початкової точки React Native.

ios/ android/ index.js App.js

Як хтось, хто працював з рідною системою як на Windows Phone, iOS, так і на Android, я вважаю, що структурування проекту зводиться до поділу файлів за типом або функцією

тип проти функції

Поділ за типом означає, що ми впорядковуємо файли за їх типом. Якщо це компонент, то є контейнерні та презентаційні файли. Якщо це Redux, існують файли дій, редуктора та зберігання. Якщо це перегляд, існують файли JavaScript, HTML та CSS.

Групувати за типом

redux actions store reducers components container presentational view javascript html css

Таким чином, ми можемо бачити тип кожного файлу та легко запускати сценарій до певного типу файлу. Це загальне для всіх проектів, але це не відповідає на запитання "про що цей проект?" Це додаток для новин? Це програма лояльності? Мова йде про відстеження харчування?

Організація файлів за типом призначена для машини, а не для людини. Багато разів ми працюємо над функцією, і пошук файлів для виправлення в декількох каталогах є клопотом. Також боляче, якщо ми плануємо створити фреймворк з нашого проекту, оскільки файли поширюються по багатьох місцях.

Групувати за ознаками

Більш розумним рішенням є організація файлів за ознаками. Файли, пов’язані з об’єктом, слід розміщувати разом. А тестові файли повинні знаходитись поблизу вихідних файлів. Перегляньте цю статтю, щоб дізнатися більше.

Функція може бути пов’язана з входом, реєстрацією, реєстрацією або профілем користувача. Функція може містити підфункції, якщо вони належать одному потоку. Якби ми хотіли перемістити допоміжну функцію, це було б легко, оскільки всі пов’язані файли вже згруповані.

Моя типова структура проекту на основі функцій виглядає так:

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

Крім традиційних файлів App.jsі index.jsта ios1і androidпапок, я поклав всі вихідні файли всередині srcпапки. Всередині у srcмене є resресурси, libraryзагальні файли, що використовуються у різних функціях, і screensекран вмісту.

Як можна менше залежностей

Оскільки React Native сильно залежить від тонни залежностей, я намагаюся бути досить обізнаним, додаючи більше. У своєму проекті я використовую лише react-navigationдля навігації. І я не фанат, reduxоскільки це додає непотрібної складності. Додайте залежність лише тоді, коли вона вам справді потрібна, інакше ви просто налаштовуєтесь на більші проблеми, ніж цінність.

Мені подобається React - це компоненти. Компонент - це місце, де ми визначаємо погляд, стиль та поведінку. React має вбудований стиль - це як використання JavaScript для визначення сценарію, HTML та CSS. Це відповідає функціональному підходу, до якого ми прагнемо. Тому я не використовую стилізовані компоненти. Оскільки стилі - це лише об'єкти JavaScript, ми можемо просто ділитися стилями коментарів у library.

src

Мені дуже подобається Android, тому я називаю srcта resвідповідаю її умовам папок.

react-native initвстановлює нам бабел. Але для типового проекту JavaScript добре організувати файли в srcпапці. У моїй electron.jsпрограмі IconGenerator я розміщую вихідні файли всередині srcпапки. Це не тільки допомагає з точки зору організації, але й допомагає Babel перевести всю папку відразу. Просто команда, і у мене файли srcпереписані distв мить.

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

Екран

React базується на компонентах. Так. Є контейнерні та презентаційні компоненти, але ми можемо складати компоненти для створення більш складних компонентів. Зазвичай вони закінчуються показом на весь екран. Це називається Pageв Windows Phone, ViewControllerв iOS і Activityв Android. У посібнику React Native дуже часто згадується екран як щось, що охоплює весь простір:

Мобільні програми рідко складаються з одного екрану. Управління презентацією та переходом між кількома екранами, як правило, обробляється тим, що називається навігатором.

index.js чи ні?

Кожен екран вважається точкою входу для кожної функції. Ви можете перейменувати LoginScreen.jsна index.js, використовуючи функцію модуля Node:

Модулі не повинні бути файлами. Ми також можемо створити find-meпапку node_modulesі помістити index.jsтуди файл. Той самий require('find-me')рядок використовуватиме index.jsфайл цієї папки

Тому замість import LoginScreen from './screens/LoginScreen', ми можемо просто зробити import LoginScreen from './screens'.

Використання index.jsрезультатів в інкапсуляції та забезпечує загальнодоступний інтерфейс для функції. Це все особистий смак. Я сам віддаю перевагу явному імені файлу, звідси і назва LoginScreen.js.

Навігатор

реакція-навігація, здається, є найпопулярнішим вибором для обробки навігації в програмі React Native. Для такої функції, як вбудовування, існує, мабуть, багато екранів, якими керує стекова навігація, тому є OnboardingNavigator.

Ви можете сприймати Навігатор як щось, що групує додаткові екрани або функції. Оскільки ми групуємося за ознаками, розумно розмістити Навігатор усередині папки об’єктів. В основному це виглядає так:

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, ви можете назвати його 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/" ] }

Доступ до шрифтів за іменами спричинений помилками, ми можемо створити сценарій, подібний до того, що ми зробили із зображеннями, щоб створити більш безпечне приєднання. Додати fonts.jsдо нашої 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()

Тепер ви можете використовувати власний шрифт через Rпростір імен.

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

Простір імен R.

Цей крок залежить від особистого смаку, але я вважаю його більш організованим, якщо ми введемо простір імен R, подібно до того, як це робить Android для активів із сформованим класом R.

Після того, як ви екстерналізуєте ресурси програми, ви зможете отримати до них доступ за допомогою ідентифікаторів ресурсів, створених у Rкласі вашого проекту . Цей документ показує, як згрупувати ресурси у своєму проекті Android та надати альтернативні ресурси для певних конфігурацій пристроїв, а потім отримати до них доступ із коду програми або інших файлів XML.

Таким чином, давайте створимо файл з ім'ям R.jsв 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

І отримати доступ до нього на екрані:

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

Замінити stringsна R.strings, colorsз R.colorsі imagesз R.images. З анотацією R стає зрозуміло, що ми отримуємо доступ до статичних об’єктів із набору додатків.

Це також відповідає конвенції Airbnb для синглтона, оскільки наш R тепер схожий на глобальну константу.

23.8 Використовуйте PascalCase під час експорту конструктора / класу / синглтона / бібліотеки функцій / оголеного об'єкта.
const AirbnbStyleGuide = { es6: { }, } export default AirbnbStyleGuide

Куди піти звідси

У цьому дописі я показав вам, як, на мою думку, слід структурувати папки та файли у проекті React Native. Ми також дізналися, як керувати ресурсами та отримувати до них доступ безпечніше. Сподіваюся, вам це було корисно. Ось ще кілька ресурсів для подальшого вивчення:

  • Організація реактивного проекту React
  • Структурування проектів та іменування компонентів у React
  • Використання index.js для розваг та публічних інтерфейсів

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 ?