
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 #00B75D
color
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 ?