自己紹介
- uryyyyyyy: しばたこ
- 本業はアドテクでScala/TypeScript
- 副業でReact Native(@ CureApp)
今回話すこと
- 最近のフロントエンドの問題意識
- (普通の話をします。。。)
- フロントエンド間(Redux)でのロジック共有
- (そこそこ過激なことを言います。。)
- サーバーとのロジック共有
- (とても無茶なことをして失敗した話をします。。。)
- まとめ
最近のフロントエンド事情の問題意識
結論を三行で
- フロントにもドメインロジックが求められている
- ドメインロジックが散らばるのを避けたい
- React Nativeでしょ!
フロントでも複雑なロジックを扱うことが求められている
(元ネタ:クライアント主権時代にJSのモデルはどう共有すればいいのか)
近年のスマホファーストな時代では、WPAやネイティブアプリのようにスマホのみ(オフライン)でも動作することが求められてくるように思います。
するとフロントエンドは、これまでのようにただJSONを受け取ってそれを描画するだけでなく、ドメインロジックや画面遷移を含む一通りの動作ができるようになる必要があります。
複雑なロジックが散らばるのを避けたい
複雑なロジックが各プラットフォームごとに独立してあると、バグなく全てをメンテナンスし続けることは大変です。
複雑なロジックの散らばりを避けるには!
全部同じ言語で書き、同じコードベースを参照することです!
そこでReact Nativeですよ!
- Web -> React, Angular, Vue
- スマホアプリ -> React Native
- デスクトップ -> Electron
これで、ロジックは完全に共通化して、プラットフォーム特有の事情(UI層)だけを各プラットフォームで実装すれば済むようになりました。
フロントエンド間でのロジック共有
ここから、まずは各プラットフォームのフロントエンド間でのロジック共有を考えます。
(とりあえずRedux前提で考えます。他に良いアーキテクチャがあれば教えてください。)
結論を三行で
- サーバー通信などの非同期処理も共有したい
- そうするとReducerにロジックを集約できない
- ActionCreatorで全部やってしまおう
共通化できそうなロジック
- できる
- Domain Logic
- サーバーとの通信(fetch APIはuniversal)
- 無理にすべきでない
- 永続化など
- View Logic(Loadingモーダルの制御など)
- View Component
とりあえず上2つ、可能なら部分的に後者を進めていきたいところです。
refs: マルチプラットフォーム時代のReact / React Native / Universal JS
どこに集約するか
Reduxだと登場人物は以下です。
Reducer
Actionを受け取って、アプリの状態(State)を更新するための関数。
以下の特徴があるため、ここにロジックを集約するのは難しそうです。(そもそもそういう用途でない)
- 関数内で非同期処理を行えないこと。
- Actionのpayloadは、plain objectを要求されること
- State自体もplain objectが推奨されていること
- 公式では、データ永続化の観点でオススメされてない
- 他にも、UIのテストや事象の再現が面倒になる気がする。
ここに書くべき処理はぶっちゃけ何もないと言っていいくらい薄いと思います。
(View) Component
画面の見た目やユーザー操作を制御します。
ここはReactの思想(Learn once, write anywhere)としてプラットフォーム毎に違う実装になるので、ここで共通化はできません。
ここでは以下の責務があれば十分そうです。
- Propsから降ってくるStateで画面を表示
- Propsから降ってくるDispatcherにユーザー操作を通知
Middleware
(僕はMiddleware不要派なので、redux-thunkにおけるActionCreatorみたいなオブジェクトを使います。)
非同期処理を扱えて、任意のタイミングで画面へ反映(Actionのdispatch)できるため、ここにロジックを集約するのがベストな感じです。
コード例はこんな感じ(ActionDispatcherの場合)
ToDoアプリでの具体的な例はこんな感じ(Actions, Component)
const myHeaders = {
"Content-Type": "application/json",
'Accept': 'application/json'
};
export class ActionDispatcher {
dispatch: (action: any) => any;
bigJSON: ReduxState;
constructor(
dispatch: (action: any) => any,
bigJSON: ReduxState,
) {
this.dispatch = dispatch
this.bigJSON = bigJSON
}
doSome(params: string) {
const bigJSON, params
{/* View Logic*/}
{/* domain Logic*/}
const data = {/* View Logic*/}
this.dispatch({type: SOME_ACTION, payload: data})
}
async saveToServer(): Promise<void> {
try {
{/* View Logic*/}
const response: Response = await fetch('http://localhost:3000/api/todos', {
method: 'PUT',
headers: myHeaders,
body: JSON.stringify(this.todoList)
});
if (response.status === 200) { //2xx
const bigJSON, response
{/* domain Logic*/}
{/* View Logic*/}
this.dispatch({type: SOME_ACTION, payload: data})
} else {
throw new Error(`illegal status code: ${response.status}`);
}
} catch (err) {
console.error(err.message);
}
}
}
Reduxのconnectで渡ってくるはずのstateとdispatchを用いて状態の変更を行います。
サーバーとのロジック共有
結論を三行で
- 基本、やらないほうが良いと思う。
- サーバーサイドが軽いなら全部JSはありかも。
- scalajsツラかったよ...
やるならどうやるか?
- フロント側に寄せる→JSでサーバーサイドを書く
- サーバー側に寄せる→ScalaJSで書く
主に通信部分と、そこからのドメインロジックの共有ができたら嬉しい。
JSでサーバーサイドを書く
個人的にはオススメしない。
サーバーサイドの要件が厳しくなくて、バッチとかインフラの制約合わせても一言語で行けるなら良いかも。
それ以外ならどうせロジックが散らばるのでオススメしない。
ScalaJSで書く
(僕がScalaエンジニアなので。。)
ScalaJSでフロントのDomain Logicまで書く。
やってみた。
ツラかった。。。
- ツラみ1
JSONシリアライズを共通化しようとライブラリ入れると、minifyしても400kBとか近く行ってしまい、React Nativeで読む(ビルドする)のめっちゃ時間かかる。
(ノートPCだと5分とか返ってこなくてtimeoutがザラ。)
自前でシリアライズ書くのはもっとツライ。。
- ツラみ2
minifyしない場合、特定行をコメントアウトしないとimportでコケる。
(良いやり方あるのかもしれない。。。)
- ツラみ3
View側はビルド遅い影響でjsで書くことになる&Redux部分はPlain Objectでやりとりを要求するので、結局型安全じゃないし変換がひたすらダルい。
これ通ると思ってたけど、Actionに凝ったオブジェクト詰めて送ってやろうとしたらReduxに怒られた。
まとめ
- Reduxの仕組みに載せても、フロント側でのロジック共有はできる。
- サーバーとフロントでのモデルの共通化はオススメしない。
- (scalaJSは、もうちょいスリムになるかPCスペックが上がれば検討したい)