はじめに
巷で人気のReactNativeを始めよう。
なに?Expoってやつを使うと実装しながら実機で確認できるって?
しかもcreate-react-native-appを使うと一発で環境ができるとな?
そいつぁすげぇや!
よし、いっそのことTypeScriptで作ってみよう。
さて
import Hoge from '../../../hoge/hoge';
import Foo from '../../../../foo/foo';
こんなの書きたくないよ
import Hoge from 'hoge/hoge';
import Foo from 'foo/foo';
イイネ!
こんな気持ちで始めてみました。
やるやらない
やること
- 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
の編集
@@ -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.js
をRoot.tsx
という名前でリネーム
ファイル名は当初App.tsx
でしたがそれだとimportを絶対パスにした時に動きませんでした
ただしcrnaでプロジェクトを作成した場合、mainファイルが
"main": "./node_modules/react-native-scripts/build/bin/crna-entry.js"
となっており、その中ではプロジェクトルートのApp.js
をrequireしています。
var _App = require('../../../../App');
このままではエラーになるのでプロジェクトルートにApp.js
を作ってあげましょう。このApp.js
はtsをトランスパイルしたjsを読み込ませます
トランスパイル先はbuild
なので、以下のようなファイルにします。
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
ファイル変更検知⇒トランスパイルするようにする
そのままだとファイル変更後いちいちtsc
しないと変更が反映されないため
ファイル監視⇒ファイル変更検知⇒tsc⇒expo反映⇒(以下ループ)
を自動でできるようにします。
concurrently,rimrafをプロジェクトにインストール
$ yarn add concurrently rimraf -D
concurrently
npmスクリプトを同時実行できるようにする。npm-run-all
と似たようなもの
rimraf
rm -rf
を実行する
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
というファイルを作りました。
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してやりましょう。もちろん相対パスは使わずに。
@@ -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
に追記
@@ -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. */
@@ -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
所感
import周りで1日ほどハマりました。楽をしたかったはずなのに。
玄人だけでなく、エンジニアにとっても真理ではなかろうか。
参考
TypeScript in CRNA
React-Native App using Expo CRNA and Typescript - Part 1
babel-plugin-module-resolverでimportを快適にする(eslint-plugin-import + flow + path-autocomplete)
哲也~雀聖と呼ばれた男~