エンジニア特化型転職支援サービス
terascout
絶賛開発中
Backend
全面的にfirebase
(cloud functions および firebase hosting)
firebase hositing でnextjsをhosting
frontend
React Native(with haul)
typescript
redux(redux-saga,recompose,redux-actions)
CI
fastlane
appcenter
Redux
Type Definition (firestoreのデータ構造)
i18n辞書定義ファイル(ts,json,js)
その他 native web関係ない処理関数
firebase由来のデータ構造のRedux stateとmutationは
全てで共通化できる
ex)
redux
├── User
│ ├── actions.ts
│ ├── index.ts
│ ├── reducers.ts
│ └── sagas.ts
├── actions.ts
├── reducers.ts
├── rootSaga.ts
├── sagas
└── state.ts
Web でも、Nativeでも共通化可能
import * as model from 'src/redux/$modelPath'で
でinitialState,Action,Reducerが全部exportされる状態を作る
ex)
redux
├── User
│ ├── actions.ts
│ ├── index.ts
│ ├── reducers.ts
│ └── sagas.ts
├── actions.ts
├── reducers.ts
├── rootSaga.ts
├── sagas
└── state.ts
import {default as _actions } from './actions'
import { sagaActions } from './sagas'
export const actions = {..._actions, ...sagaActions}
export {
default as reducers,
initialState
} from './reducers'
export {default as sagas} from './sagas'
ただし、reduxのstateの中には、webや、モバイルでだけ
使いたいものも存在している。
ex)
redux
├── Search
└── User
このStateは、Web管理画面でしか使いたくない
reducers.ts,actions,rootSaga.ts,state.tsで使用したい
Stateだけ、結合してあげれば、treeShakingで脱落する
ex)
redux
├── Search
├── User
├── actions.ts
├── reducers.ts
├── rootSaga.ts
└── state.ts
ex) actions.ts
export { actions as User } from './User';
ex) reducers.ts
import { reducers as user } from './User';
export default combineReducers({
user,
...others
});
Searchを未使用のまま放置できる
WebとNativeで使用するreduxを別リポジトリで管理
(ついでに、firebaseのtaskも突っ込んでinitialStateを使って
firebase の開発用のバッチなどに利用できるようにした)
repo
├─ redux
│ ├── SomeCommon
│ └── User
└─ FirebaseTasks
submodule管理
app mainプロジェクト
├─ redux
│ ├── Search
│ └── MainState
└─ repo
そのまま import {someReduxStructure} from repo/$statePath
をしても良いがSymbolic Linkを利用するとあたかも、その階層にファイルが存在するかのようにEditorで編集できる。ex) vscode
SubModuleの実体
メインレポの
シンボリック
リンク
firebase由来のデータ構造のRedux stateとmutationは
全てで共通化できる
ex)
redux
├── User
│ ├── actions.ts
│ ├── index.ts
│ ├── reducers.ts
│ └── sagas.ts
├── actions.ts
├── reducers.ts
├── rootSaga.ts
├── sagas
└── state.ts
Web とNativeで専用の非同期処理のライブラリが異なる可能性があるため共通化には工夫が必要
Pattern 1: sagasの処理を抽象化して実体をDIする(推奨)
export function*
updateDocument
service/Firebase.web.ts
service/Firebase.native.ts
export function*
updateDocument
違うライブラリや型を使用するが同名のinput outputが同じになるようにラッパーを実装する。
import { updateDocument } from 'service/Firebase';
haul(RN bundler with webpack)or Webpack(for Web)やBabelのresolve extension機能を使って、importを制御
redux/someState/sagas.ts
Pattern 1: sagasの処理を抽象化して実体をDIする(推奨)
import { updateDocument } from 'service/Firebase';
haul(RN bundler with webpack)or Webpack(for Web)やBabelのresolve extension機能を使って、importを制御
function* updateUserProfile () {
yield takeLatest(UPDATE_PROFILE, function* ({ payload: user }: Action<User>) {
...
yield updateDocument(payload);
}
}
ex)User/sagas.ts
sagasのコードは一切変えずに挙動を変更できる。
Pattern 2: sagas.web.ts,sagas.native.tsでそもそも分岐させる。
(非推奨)
sagaの実装が異なるため拡張子分岐で普通にimportできる。
export function*
updateDocument
redux/State/saga.web.ts
redux/State/saga.native.ts
export function*
updateDocument
ただしこれには、以下のような罠があるので注意
- typescriptを使ってる場合、alias設定でしか拡張子分岐ができないため、相対パスが使えなくなる(wrapper分岐よりも分岐が増える)
- 以上によりaliasコンフィグが増えるほどjestのalias設定も荒れる
(そもそも分岐しないといけないものには共通化しない決断も必要)
詳しくは https://github.com/Microsoft/TypeScript/issues/21926#issuecomment-437341322
Firebaseのデータ構造はwebとNativeで共通なので、
どちらか一方の変更が追従しない事態は避けたい
=>共通レポジトリで管理
ユーザーのデバイスの外国語設定に対応して表示言語を
変更するための仕様。辞書ファイルをjson objectで持てれば良い。
(TS,でもJSでもyamlでも最終的にjson変換可能ならゴリ押しで
共通化ができる)ライブラリによって必要なjsonのフォーマットが違うためマネジメントが大変なのでぶっちゃけ日本でしかアプリを出さない場合は、使わなくてすがなるべく対応しましょう。
terasoutチームでは複数分割したobjectをts fileで保持し
ライブラリが欲している仕様に合わせて型補完が効くように自動で型情報を生成してくれるbuildスクリプトを書いています。
参考: https://github.com/quipper/i18n-dts
(quipper株式会社さんのレポで非常に参考になります。)
react-native-i18n(react-native-languages)
next-i18next
完全なjsonとしてUnion型情報を生成
分割したtsごとにjsonを生成し型Union情報を生成
i18n/ja/index.ts
i18n/ja/$namespace
(.ts or /index.ts)
型がついてるととても嬉しい。なぜならば、Editor
が勝手に登録済みkeyを列挙してくれるようになるから。
react-navigationにセットしてあるroutingも全部補完されるようにしてます。
typoしやすいstring型のデータを突っ込む時にunion literalで補完や警告を効かせるととても開発が快適になります。
どうやってるか知りたい方はこちら ↓
https://qiita.com/tkow/items/90c448416fa3945f62ab
一応componentも再利用するためにreact-native-webも使えるかどうかも検証しました
ここがNativeComponentのTextViewを使い回し
参考:https://github.com/tkow/react-native-web-test
reduxと結合する前のコンポーネントの管理を別軸に切り離す設計を考慮する必要があり、一旦お蔵入りに
・なんでSwift,Kotlinで開発しないの?
A, Swift、Kotlinでもいいけど人が揃えられませんでした。また、未経験少人数だとレイアウトやロジックの共通管理が難しいため。
・なんでFlutterで開発しないの?
A, 技術選定当初は今ほど話題になってなかったので、また、
Dartは学習コストが低いものの、未知かつライブラリなどの対応状況から現状まだ熟していない印象。これからは検討の余地があります。
・ぶっちゃっけどうだった?
A, なんでもそうですが、いいところ悪いところはあります。
ただ、一度デプロイフローを確立すると想定以上に良いところ
が多かった印象(AndroidとWeb開発経験有りからの見解)。
・iOSとAndroidのデプロイフローがネイティブと同一であるため、同一のツールが使える。(Fastlane,Crashlytics,Appcenter)
ReactNativeはiOS、Androidのネイティブアプリの起動中に差し込まれたjavascriptを実行するという単純な仕組みで動いているため、ビルド手順はネイティブアプリと同じです。(つまり、ネイティブアプリの知識があると有効です)
補足:
実行されたJavascriptの中でNativeのAPIが呼び出されると制御がNativeに移り、javascriptは実行待機を行います。
・iOSとAndroidのネイティブコードを書くことが出来る
採用した理由はこれ、最悪ネイティブコードによってゴリ押しができる防衛ラインを確保した。
また、Nativeアプリ開発の知見が活きる。
ただし、思った以上にJSのAPIが優れているため、ほぼ使わないで済みました。
・もちろんjsの資産が使える
React、Redux、Jest、ReactDevtools、Styled-Components
これによって、React Native APIの仕様に沿って設計すれば、JSの資産だけでネイティブアプリを作成することも十分可能に(Expo)。
特にネイティブアプリのスタイリングにはかなりの専門知識が必要だが、JSのスタイリングに置き換えることで、レイアウトデザインの敷居は下がっていい感じになっている。
・学習コスト
結局iOS,Androidに詳しい人が最低一人ずついないと開発は進まない。情報が少ない。(開発者がほぼ一人の場合(僕みたいな人)はネイティヴアプリのビルドやリリース手順まで全部に詳しくなる必要があり地獄圧倒的成長が得られます。)
・質が悪いライブラリが出回ってる
勝手にアニメーションが付いてたり、固定レイアウトだったり、融通効かないのが多いので自分で作り変える事が多かった。=> forkしたりソースコードを改変して対処
・バージョンアップが大体破壊的変更
最もバージョンアップが難しいフレームワークと言っても過言ではないのではないかというくらい圧倒的破壊的な変更が起こります。大事なbugfixとメジャーアップデートを同時に含めた運営を時に恨めしく思うことも(ごめんなさい)。
一回目の開発コストは高いが、再利用可能なUIをコンポーネント化してしまえば、次からの再利用コストはとても低くできる。
WEBとのシームレスな結合という意味ではFlutter
より進んでいる(Googleが手のひら返さなければ)
(※ 一人開発を先導できる人がいれば)