ReactNativeでプロジェクトを構造化して静的リソースを管理する方法

ReactとReactNativeは単なるフレームワークであり、プロジェクトをどのように構成するかを指示するものではありません。それはすべてあなたの個人的な好みとあなたが取り組んでいるプロジェクトに依存します。

この投稿では、プロジェクトを構築する方法とローカル資産を管理する方法について説明します。もちろん石で書かれているわけではなく、自分に合ったものだけを自由に塗ることができます。あなたが何かを学ぶことを願っています。

でブートストラップされたプロジェクトのreact-native init場合、基本構造のみを取得します。

あるiosXcodeプロジェクト、フォルダのためのandroidAndroidのプロジェクトのためのフォルダ、およびindex.jsおよびApp.jsネイティブ出発点に反応するためのファイルが。

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.jsindex.jsios1androidフォルダ、私は内部のすべてのソースファイルを置くsrcフォルダ。内部srcにはres、リソース、library機能間で使用される一般的なファイル、およびscreensコンテンツの画面があります。

依存関係をできるだけ少なくする

React Nativeは多くの依存関係に大きく依存しているので、さらに追加するときはかなり注意するようにしています。私のプロジェクトではreact-navigation、ナビゲーションのためだけに使用しています。そしてredux、それは不必要な複雑さを追加するので、私はファンではありません。本当に必要な場合にのみ依存関係を追加してください。そうしないと、価値よりも多くの問題に直面することになります。

Reactで気に入っているのはコンポーネントです。コンポーネントは、ビュー、スタイル、および動作を定義する場所です。Reactにはインラインスタイルがあります。JavaScriptを使用してスクリプト、HTML、CSSを定義するようなものです。これは、私たちが目指している機能アプローチに適合します。そのため、styled-componentsは使用しません。スタイルは単なる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、ViewControlleriOS、およびActivityAndroidで呼び出されます。React Nativeガイドでは、画面をスペース全体をカバーするものとして頻繁に言及しています。

モバイルアプリが単一の画面で構成されていることはめったにありません。複数の画面の表示とその間の移行の管理は、通常、ナビゲーターと呼ばれるものによって処理されます。

index.jsかどうか?

各画面は、各機能のエントリポイントと見なされます。名前を変更することができますLoginScreen.jsindex.jsノードモジュールの機能を活用することで:

モジュールはファイルである必要はありません。その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-navigation seems to be the most popular choice for handling navigation in a React Native app. For a feature like onboarding, there are probably many screens managed by a stack navigation, so there is OnboardingNavigator .

You can think of Navigator as something that groups sub screens or features. Since we group by feature, it’s reasonable to place Navigator inside the feature folder. It basically looks like this:

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

This is the most controversial part of structuring a project. If you don’t like the name library, you can name it 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名前空間

この手順は個人の好みによって異なりますが、Androidが生成されたRクラスを持つアセットに対して行うのと同じように、R名前空間を導入するとより整理されたものになります。

アプリリソースを外部化すると、プロジェクトのRクラスで生成されたリソースIDを使用してそれらにアクセスできます。このドキュメントでは、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()} ) }

交換するstringsR.stringscolorsR.colors、と、imagesR.images。Rアノテーションを使用すると、アプリバンドルから静的アセットにアクセスしていることがわかります。

これは、シングルトンのAirbnb規則とも一致します。これは、Rがグローバル定数のようになっているためです。

23.8コンストラクター/クラス/シングルトン/関数ライブラリ/ベアオブジェクトをエクスポートする場合は、PascalCaseを使用します。
const AirbnbStyleGuide = { es6: { }, } export default AirbnbStyleGuide

ここからどこへ行くか

この投稿では、ReactNativeプロジェクトでフォルダーとファイルを構造化する方法を説明しました。また、リソースを管理し、より安全な方法でリソースにアクセスする方法も学びました。お役に立てば幸いです。さらに詳しく調べるためのリソースは次のとおりです。

  • ReactNativeプロジェクトの組織化
  • 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 ?