はじめに
某案件でしばらくReact Native + Flow を使ってたのですが、やっぱりTypeScriptの方がIDEの補完や型安全性の面で良いなと思い、React NativeをTypeScript化してみました。
それぞれのファイルの意味なども可能な限り説明していければと思います。
この記事のゴール
- React NativeにTypeScriptを入れる
- Reduxも入れる
- それらのテストや動作確認をしやすくする。
環境
- react: 15.4.2
- React Native: 0.41
- TypeScript: 2.2
- NodeJS: 7.6~
- mocha
- enzyme
構成
今回の構成は以下です。
基本的には、tscでビルドして生成されたファイルをindex.jsに喰わせる形を取っています。
TSのソースマップをdebug時に引き継げてないのが難点ですが、そこはTS2.2から入った "jsx": "react-native"
を使うことでデバッグにちょうど良いJSを吐いてくれるので、そんなに困らないかもと思っています。
コード全体はこちらです。
(androidしか載せてないですが、iosも同様に動かせます。)
$ tree .
.
├── android(色々)
├── ios(色々)
│
├── src
│ ├── app.tsx
│ ├── counter
│ │ ├── Container.ts
│ │ ├── Counter.tsx
│ │ ├── module.ts
│ │ └── __tests__
│ │ ├── Container-test.ts
│ │ ├── Counter-test.tsx
│ │ └── module-test.ts
│ └── Store.ts
├── testConfig
│ ├── require.js
│ └── tsconfig.json
├── app.json
├── index.android.js
├── index.ios.js
├── package.json
└── tsconfig.json
大きく、
- src
- TSコード&テストコード
- testConfig
- テスト用のpolyfillや設定
- 設定ファイル群
という感じで、そこまで複雑ではないかと。
順番に見ていきましょう。
設定ファイル群
まずは設定ファイルから。
package.json
{
"name": "react-native-android-sample",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start --skipflow",
"android": "node node_modules/react-native/local-cli/cli.js run-android",
"test:all": "mocha ./testConfig/require.js ./src/**/*-test.tsx ./src/**/*-test.ts",
"test:unit": "mocha ./testConfig/require.js ",
"build": "tsc",
"build:watch": "tsc --watch"
},
"dependencies": {
"react": "15.4.2",
"react-native": "0.41.2",
"react-redux": "5.0.3",
"redux": "3.6.0"
},
"devDependencies": {
"@types/chai": "3.4.35",
"@types/enzyme": "2.7.5",
"@types/mocha": "2.2.39",
"@types/react": "15.0.12",
"@types/react-native": "0.37.13",
"@types/react-redux": "4.4.36",
"@types/sinon": "1.16.35",
"chai": "3.5.0",
"enzyme": "2.7.1",
"mocha": "3.2.0",
"react-addons-test-utils": "15.4.2",
"react-dom": "15.4.2",
"react-native-mock": "0.3.1",
"react-test-renderer": "15.4.2",
"sinon": "1.17.7",
"ts-node": "2.1.0",
"typescript": "2.2.1"
}
}
ちょっと多いですがこんな感じです。
(ちなみに前提として、React NativeはbundleされたJSをアプリが呼び出すことで動作しています。
開発時には、毎回全部バンドルするのは面倒ということで、最初からwatch的な機能が備わっています。これは変更分を差分ビルドしてjsコードを配信するサーバーを立てて、アプリがそこからbundle.jsを呼ぶことで実現しています。なお、デフォルトではport:8081でやりとりします。)
- scripts
- start
- react nativeのコードをバベってアプリに喰わせるサーバーを立ち上げます。
- android
- androidアプリを立ち上げます。
- 内部で上記サーバーを参照するためのport forwardingも行います。
- test:all
- 後述しますが、mochaで全部のテストを行います。
- build
- TypeScriptコードをJSへビルドします。
- 依存解決はReact Nativeに組み込まれているbabelがやるので不要です。
- start
- dependencies
- ReactやReact Nativeをやる上での最小セットを選びました。
- devDependencies
- 型定義系やテストライブラリ、ビルドツールなどが入っています。
- (もう少しスリムにできるかも。。)
tsconfig.json
{
"compilerOptions": {
"strictNullChecks": true,
"noUnusedLocals" : true,
"target": "ES2017",
"module": "ES2015",
"noImplicitAny": true,
"lib": ["dom", "ES2017"],
"moduleResolution": "node",
"rootDir": "src",
"outDir": "./dist/",
"jsx": "react-native"
},
"exclude": [
"index.android.js",
"dist",
"node_modules"
]
}
チェックを厳しくする系は好みで入れてもらうとして、
- "jsx": "react-native"
- react-native向けのJSを生成してくれます。
- 簡単に言うと、jsxの記法を維持して吐き出しているように見えます。
- target / module
- babelでes6記法を読めるので、そのまま吐き出します。
- babelがasync/awaitまでサポートしちゃってるので、ES2017で吐き出します。
- (ついで言うとfetchも入ってるので、polyfill不要です。)
- rootDir / outDir
- 対象はsrc以下なのと、アプリのエントリポイントのindex.jsがdistを読みに行くようにしてるのでこんな形。
ちなみに、 "jsx": "react-native"
で吐いたコードはこんな感じです。
import * as React from 'react';
import {StyleSheet, Text, View, Button} from 'react-native';
import {CounterState} from './module';
import {ActionDispatcher} from './Container';
const styles: any = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
interface Props {
value: CounterState;
actions: ActionDispatcher;
}
export class Counter extends React.Component<Props, {}> {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
{`Point: ${this.props.value.num}`}
</Text>
<Button
onPress={() => this.props.actions.increment(3)}
title="Increment"
/>
<Button
onPress={() => this.props.actions.decrement(2)}
title="Decrement"
/>
</View>
);
}
}
import * as React from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
export class Counter extends React.Component {
render() {
return (<View style={styles.container}>
<Text style={styles.welcome}>
{`Point: ${this.props.value.num}`}
</Text>
<Button onPress={() => this.props.actions.increment(3)} title="Increment"/>
<Button onPress={() => this.props.actions.decrement(2)} title="Decrement"/>
</View>);
}
}
十分読みやすいかなと思いました。
ES2017でビルドしているので、async/awaitもそのまま出力してくれます。
ソースコード
特に解説することもないコードなので、必要があれば見ておいてください。
ボタンが2つ出てきて、加算/減算を行うことが出来ます。
テストコード
設定系
package.jsonにこんな記述がありました。
"test:all": "mocha ./testConfig/require.js ./src/**/*-test.tsx ./src/**/*-test.ts"
confg系のコードを読み込んだあとに、-test.ts(x)
という名前のコードを全てテストする用になっています。1ファイルだけテストしたければ test:unit
の後にファイル名を選択するとテストできます。
さて、ここのrequire.jsの定義は以下です。
require('ts-node').register({project: './testConfig/tsconfig.json'});
require('react-native-mock/mock');
- ts-node
- TypeScriptコードをそのままNode上で実行できるツール。
- ここでは、テスト用のtsconfigを読ませて実行している。
- react-native-mock
- react-nativeのコードは内部で
__DEV__
などによる分岐をしていたりするため、それらをモックしてNode上でテストしやすくしているもの。
- react-nativeのコードは内部で
ちなみに、テスト用のtsconfgとありますが、その定義はこちらです。
なぜ必要かというと、こちらではbabelを噛ませないためES Modulesとjsx記法が使えないことにあります。
"module": "commonjs"
と、"jsx": "react"
が大きな変更点です。
{
"compilerOptions": {
"strictNullChecks": true,
"noUnusedLocals" : true,
"target": "ES2017",
"module": "commonjs",
"lib": ["dom", "ES2017"],
"noImplicitAny": true,
"moduleResolution": "node",
"jsx": "react"
},
"include": [
"../src"
],
"exclude": [
"index.android.js",
"dist",
"node_modules"
]
}
テストコード
こちらもコード自体は普通です。
reducer/Component/ActionDispatcher(非同期処理も含めたロジック)のテストを含んでいます。
https://github.com/uryyyyyyy/react-native-android-sample/tree/redux/src/counter/__tests__
まとめ
ソースマップを引き継ぐ方法がわからないことが懸念でしたが、TS2.2のおかげでトランスパイル後のjsでも可読性に問題が無さそうなのでこれで行けそうな気がしています。