LoginSignup
11
13

More than 5 years have passed since last update.

create-react-native-appで ReactNative + Expo + TypeScriptの環境を構築してやろう。ついでに絶対パスでimportできるようにしてやろう。

Last updated at Posted at 2018-08-16

はじめに

巷で人気のReactNativeを始めよう。
なに?Expoってやつを使うと実装しながら実機で確認できるって?
しかもcreate-react-native-appを使うと一発で環境ができるとな?
そいつぁすげぇや!:dolphin:
よし、いっそのことTypeScriptで作ってみよう。

さて

import Hoge from '../../../hoge/hoge';
import Foo from '../../../../foo/foo';

こんなの書きたくないよ:weary:

import Hoge from 'hoge/hoge';
import Foo from 'foo/foo';

イイネ!:thumbsup:

こんな気持ちで始めてみました。

やるやらない

やること

  • create-react-native-app(以下 crna )でReactNative + Expo + TypeScriptの環境を構築
  • tsconfigとbabel-plugin-module-resolverを使って絶対パスでimportできるようにする

サンプルコード
https://github.com/tshackstream/expo-ts-example

こんな感じのコードでexpoを起動、正常に表示できるところまでやります。

やらないこと

トランスパイルの細かい設定などはやりません。

TL;DR

  • crnaでプロジェクトを作って
  • TypeScriptを導入して
  • babel-plugin-module-resolverを入れて
  • tsconfig.json.babelrcを書き換えて

おわり

環境

開発環境
macOS HighSierra
node v10.8.0
npm v6.2.0
確認端末
端末 iPhoneX
iOS v11.4

事前準備

個人的な趣味でyarnを使います。npmでも問題ありません。

$ npm install -g yarn

create-react-native-app, typescriptをグローバルインストール

$ yarn add global reate-react-native-app typescript

typescriptをインストールするとtscコマンドが使えるようになります。

ReactNative + Expo + TypeScript環境構築

プロジェクト作成、TypeScript導入

expo-ts-exampleという名前で作成します。

$ create-react-native-app expo-ts-example

プロジェクトにtypescriptをインストール

$ yarn add typescript -D

型定義もインストール

$ yarn add @types/react @types/react-native -D

tsconfig.jsonを作成

$ tsc --init

tsconfig.jsonの編集

tsconfig.json
@@ -2,17 +2,17 @@

-    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "module": "es2015",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
     // "lib": [],                             /* Specify library files to be included in the compilation. */
-    // "allowJs": true,                       /* Allow javascript files to be compiled. */
+    "allowJs": true,                       /* Allow javascript files to be compiled. */
     // "checkJs": true,                       /* Report errors in .js files. */
-    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+    "jsx": "react-native",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
     // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
     // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
     // "sourceMap": true,                     /* Generates corresponding '.map' file. */
     // "outFile": "./",                       /* Concatenate and emit output to single file. */
-    // "outDir": "./",                        /* Redirect output structure to the directory. */
-    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    "outDir": "build",                        /* Redirect output structure to the directory. */
+    "rootDir": "src",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
     // "composite": true,                     /* Enable project compilation */
     // "removeComments": true,                /* Do not emit comments to output. */
     // "noEmit": true,                        /* Do not emit outputs. */
@@ -34,15 +34,15 @@
     // "noUnusedParameters": true,            /* Report errors on unused parameters. */
     // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
     // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
-
+    "skipLibCheck": true,
     /* Module Resolution Options */
-    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
     // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
     // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
     // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
     // "typeRoots": [],                       /* List of folders to include type definitions from. */
     // "types": [],                           /* Type declaration files to be included in compilation. */
-    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
     "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
     // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

@@ -55,5 +55,7 @@
     /* Experimental Options */
     // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
     // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
-  }
+  },
+  "compileOnSave": false,
+  "exclude": ["node_modules", "App.js", "App.test.js", "build"]
 }
\ No newline at end of file

srcディレクトリを作成しその中にApp.jsRoot.tsxという名前でリネーム
ファイル名は当初App.tsxでしたがそれだとimportを絶対パスにした時に動きませんでした:thinking:

ただしcrnaでプロジェクトを作成した場合、mainファイルが

package.json
"main": "./node_modules/react-native-scripts/build/bin/crna-entry.js"

となっており、その中ではプロジェクトルートのApp.jsをrequireしています。

crna-entry.js
var _App = require('../../../../App');

このままではエラーになるのでプロジェクトルートにApp.jsを作ってあげましょう。このApp.jsはtsをトランスパイルしたjsを読み込ませます
トランスパイル先はbuildなので、以下のようなファイルにします。

App.js
import App from "./build/Root";
export default App;

現在の構造はこんな感じ

expo-ts-example
├── App.test.js
├── README.md
├── app.json
├── node_modules
├── package.json
├── src
    └── Root.tsx
├── tsconfig.json
└── yarn.lock

この状態でトランスパイル後、expoを起動すると正常に起動するはずです。

$ tsc
$ yarn start

IMG_0011.jpg

:relaxed:

ファイル変更検知⇒トランスパイルするようにする

そのままだとファイル変更後いちいちtscしないと変更が反映されないため
ファイル監視⇒ファイル変更検知⇒tsc⇒expo反映⇒(以下ループ)
を自動でできるようにします。

concurrently,rimrafをプロジェクトにインストール

$ yarn add concurrently rimraf -D

concurrently
npmスクリプトを同時実行できるようにする。npm-run-allと似たようなもの

rimraf
rm -rfを実行する

package.jsonを編集

package.json
     "eject": "react-native-scripts eject",
     "android": "react-native-scripts android",
     "ios": "react-native-scripts ios",
-    "test": "jest"
+    "test": "jest",
+    "tsc": "tsc",
+    "clean": "rimraf build",
+    "build": "yarn run clean && yarn run tsc --",
+    "watch": "yarn run build -- -w",
+    "watchAndRunAndroid": "concurrently \"yarn run watch\" \"yarn run android\"",
+    "buildRunAndroid": "yarn run build && yarn run watchAndRunAndroid ",
+    "watchAndRunIOS": "concurrently \"yarn run watch\" \"yarn run ios\"",
+    "buildRunIOS": "yarn run build && yarn run watchAndRunIOS ",
+    "watchAndStart": "concurrently \"yarn run watch\" \"yarn run start\"",
+    "buildAndStart": "yarn run build && yarn run watchAndStart "
   },
   "jest": {
     "preset": "jest-expo"

importを絶対パスにする

srcにcomponentsというディレクトリを作り、その中にMyComponent.tsxというファイルを作りました。

src/components/MyComponent.tsx
import React from 'react';
import { Text, View } from 'react-native';

export default class MyComponent extends React.Component {
    render() {
        return (
            <View>
                <Text>It's my component!</Text>
            </View>
        );
    }
}

Root.tsxでimportしてやりましょう。もちろん相対パスは使わずに。

src/Root.tsx
@@ -1,11 +1,14 @@
 import React from 'react';
 import { StyleSheet, Text, View } from 'react-native';
+  import MyComponent from "components/MyComponent";

 export default class Root extends React.Component {
   render() {
     return (
       <View style={styles.container}>
          <Text>Open up App.js to start working on your app!</Text>
          <Text>Changes you make will automatically reload.</Text>
          <Text>Shake your phone to open the developer menu.</Text>
+         <MyComponent/>
       </View>
     );

このままではトランスパイラもexpoも
import MyComponent from "components/MyComponent";
を解決することができません。

トランスパイラはtsconfig.jsonで、expoは.babelrcでそれぞれパスの解決をさせます。
babelはプラグインが無いと解決できないのでインストールします。

babel-plugin-module-resolverをプロジェクトにインストール

$ yarn add babel-plugin-module-resolver -D

.babelrc,tsconfig.jsonに追記

tsconfig.json
@@ -37,8 +37,10 @@
     "skipLibCheck": true,
     /* Module Resolution Options */
     "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
-    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
-    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+    "baseUrl": "./src",                       /* Base directory to resolve non-absolute module names. */
+    "paths": {
+      "components*": ["components*"]
+    },                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
     // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
     // "typeRoots": [],                       /* List of folders to include type definitions from. */
     // "types": [],                           /* Type declaration files to be included in compilation. */

.babelrc
@@ -4,5 +4,10 @@
     "development": {
       "plugins": ["transform-react-jsx-source"]
     }
-  }
+  },
+  "plugins": [
+    ["module-resolver", {
+      "root": ["./build"]
+    }]
+  ]
 }

ここで注意したいのが.babelrcの設定で、expoが参照しているのはApp.js経由のbuild/Root.tsxです。
つまりexpo視点のルートディレクトリはbuildなのでrootにはbuildを指定してあげる必要があります。

最終的なディレクトリ構成はこのようになりました。

expo-ts-example
├── App.js
├── App.test.js
├── README.md
├── app.json
├── build
    ├── Root.js
    └── components
        └── MyComponent.js
├── expo-ts-example.iml
├── node_modules
├── package.json
├── src
    ├── Root.tsx
    └── components
        └── MyComponent.tsx
├── tsconfig.json
└── yarn.lock

さてお待ちかね実行タイム

$ yarn buildAndStart

IMG_0012.jpg

:thumbsup:

所感

import周りで1日ほどハマりました。楽をしたかったはずなのに。

572da61930da592cfc85108207146d46.png

玄人だけでなく、エンジニアにとっても真理ではなかろうか。

参考

TypeScript in CRNA
React-Native App using Expo CRNA and Typescript - Part 1
babel-plugin-module-resolverでimportを快適にする(eslint-plugin-import + flow + path-autocomplete)
哲也~雀聖と呼ばれた男~

11
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
13