Phoenix プラクティスを探してコードリーディング

フォルダ構造
印象に残ったファイルとやっていることの抜粋。

livebook_web/helpers/ansi.ex
def ansi_string_to_html(string) do
def ansi_string_to_html_lines(string) do

livebook_web/helpers/codec.ex
def pcm_as_wav_size(pcm_size) do
def encode_pcm_as_wav_stream!(path, file_size, num_channels, sampling_rate, offset, length) do
def encode_annotated_binary!(meta, binary) do

livebook_web/helpers.ex
ANSIの処理などを分けているのは明確なコンテキストがあるからで、helpers.ex
にもいくつか処理があった。
defdelegate ansi_string_to_html(string), to: LivebookWeb.Helpers.ANSI
def platform_from_user_agent(user_agent) when is_binary(user_agent) do
def live_dashboard_process_path(pid) do
def names_to_html_ids(names) do
def pluralize(1, singular, _plural), do: "1 #{singular}"
def format_datetime_relatively(date) do
def upload_error_messages(upload) do
def upload_error_to_string(:too_large), do: "Too large"
platform_from_user_agent
はsession_liveでしか使っていないが、helpers.exにあるのは興味深い。privateな関数ではダメだったのだろうか。
Helpers 自体は、最初から共通のhtml_helpersに突っ込んでいた。
# Custom helpers
import LivebookWeb.Helpers
コンテキストに依存しない IN:一般的なデータ, OUT: 一般的なデータ、であれば、helpers.ex
だろうか。で、その中でも大きいものや、そのうち増えてコンテキストが固まってきたもの(例えばエラー生成とか)があれば、helpers/something.ex に落として、importなり、defdelegateなりを行う。

livebook_web/hooks/
app_auth_hook.ex
auth_hook.ex
sidebar_hook.ex
...
いずれも router.ex での on_mount なモジュールの様子。beacon_live_adminにもあったし、hooksが一般的なのかもしれない。
フォルダ構造とは別に、
- sidebar_hookでは、
attach_hook
でイベントハンドラ(handle_event
)を差し込んでいた。ここではPubSubのHookをしているが、それ用のHTMLもないのでLiveComponentなしで、イベントハンドルしたかったのだろうか。ユーザー情報変更をbroad_cast
で受ける処理をまとめている、など。- 話がそれて、
attach_hook
使うとimportじゃなくてもaliasするようなモジュール内で共通イベントの処理ができそう。
- 話がそれて、
- 途中で、
LivebookWeb.Confirm
というComponentを見つけたが、ここにon_mount
が定義されていた。なんとなくわちゃわちゃ感を覚えたが、そのcomponentに付属するhookだからありなんだろうか。LiveComponentにせずに、importもせずに、独立して関心のイベントハンドラを扱う(定義する)ためのテクニックという感じに見えた。

livebookは、ほかにも livebook_web/*_helpers.ex
がある。
なるべくLiveViewやLiveComponentをコンテナコンポーネントにしておいて、プレゼンテーショナルなコンテキストに依存するinを取るものを集めている感じには見える.... ? :thinking 違うかもしれない? 微妙に線引きが不明。
名前は *_components.ex
じゃないんだ。 *_component.ex
(LiveComponent用)とややこしいからだろうか。

livebook_web/output/
output用途なLiveComponentが複数あるディレクトリ。output_live
じゃなくてoutput
か。確かに、中にLiveViewなファイルがないから、この方がわかりやすい気がする。

live/components/
以下に置いているもの。
livebookはほぼない。既存のlayoutsと唯一Confirm
というコンポーネントがある。
(livebook自体は昔からあるので設計思想的に反映されていない可能性はある)
todo_trekはTimeline
というコンポーネントがある。
これはHomeLiveでしか使っていないので、そっちでも良さそうだが切り離してある。
Presentationalなコンポーネントを、live/components/
に集めるべきなんだろうか。そうするとおそらくlive/hoge_live/index.ex
とlive/components/hoge.ex
がセット的に発生しそうではある。いや、Contextsは文字通りコンテキストであり、Componentsは文字通りコンポーネントであるので(特にAtomic的な意味合いで)混ぜちゃいけないか。live/components
内のコンポーネントなモジュールの命名や関数名にコンテキストが入り込むときは、たぶん迷子になっている。
(最初からある CoreComponentはサンプルとしてはいいけど、まとめすぎていてミスリードでは...)

LiveComponentをどこにおく?
共通使用しないもの(画面構成要素で独自に状態を持つ系)
=> live/<foo>_live/*_component.ex
でよさそう(rootなLiveViewと同じフォルダ)。
共通使用するもの
=> live/<foo>/*_component.ex
でよさそう。このフォルダには、root LiveViewなファイルがない想定。
PresentationalなComponent に関わるものをどこにおくか?
難しい。Livebookプロジェクトも統一感はなかったかもしれない。session_live.ex
にあるもの(button_item
)とsession_helpers.ex
にあるもの(cell_icon
)の線引きが不明。
- 共通利用になっていないものは、個人的には構造的に閉じていた方が良いと思う。それはデータ依存かどうかは関係なく、コンポーネントとして切り出せてもそこにあった方が嬉しい。そうしないと(なんでも移すと)components/以下の命名が破綻して「どこに何が?」の迷子になる(ワンクッション必要と考える)。
- データに直接依存するものは、LiveView(LiveComponent)モジュールでそのまま扱う。
- データに直接依存しない形に無理なくできた(る)ものは、コンポーネントとしては成立しているので、
attr
を必ず使う形で、LiveView(LiveComponent)モジュールでそのまま扱う。さすがに多すぎると判断するならば、ファイルを分ける(live/<foo>_live/<foo>_parts.ex
)。
- 共通利用になっている(なった)ものは、
live
ではなくcomponents
でフォルダ側に移すことになると思われる。- 特定のコンテキストがあるもの ~
components/organisms/<foo>.ex
- (templates相当はLiveViewのrenderで扱われる)
- あるいは、
components/<foo>.ex
+components/<foo>/<organisms_name>.html.heex
にしてembed_templatesを使う。- こっちかな。明らかに巨大でイベントを扱うものも1ファイル(e.g.
modal.ex
,form.ex
,conform.ex
)にできる。
- こっちかな。明らかに巨大でイベントを扱うものも1ファイル(e.g.
- 特定のコンテキストがあるもの ~
atomsとmoleculusなコンポーネント
-
components/atoms.ex
とcomponents/moleculus.ex
に入れて、デフォルトでimportしてしまう。-
core_component.ex
は消す。coreというのは意味が通らない。modal
ならmodal.ex
を作ること。
-

参考プロジェクト
Livebook
BeaconCMS/
TodoTrek

app/ にかくか、app_web/ に記述するか

livebookをみるとPubSubの処理が lib/livebook/users.ex
とかにある。lib/livebook_web/
以下っぽい感覚だけどそうでもないのだろうか。必ずしも...だからPubSubは別にweb
じゃない、という感覚だろうか。

iexからそれを行うケースを思いつくかどうか、とか。

Livebookは、sessionというコンテキストまで lib/livebook/ 以下にある。

例えば、ライブラリパッケージを提供するとき、各機能はwebとか関係なしにlib/package_name/
に定義するのだから、lib/app_web
の方が例外的とみるほうがいいのだろうか。webに関するものではなく、ただのwebのUI置き場くらいの感じだろうか。
別デザインのwebが生えた(lib/app_web_2
)として仮定して、どちらでも使うものは置かない、くらいのイメージとか。いや、このイメージだと逆にwebに膨れ上がる気もする。
なので、*_web
はクライアントサイド相当のコード、それ以外はコンテキストとかの心持ちだろうか。

todo_trekは、current_userをもつScopeなるモジュールもコンテキストにあった。
defmodule TodoTrekWeb.Scope do
def on_mount(:default, _params, _session, socket) do
current_user = socket.assigns[:current_user]
{:cont, Phoenix.Component.assign(socket, :scope, TodoTrek.Scope.for_user(current_user))}
end
end
for_user
という名前は関数っぽさはないな、と思ったらstructだった。トップレベルにstructを置いているのか。

コンテキスト内の共通処理

データベースモデル
本当は、Ectoのスキーマはコンテキストごとに定義があってもいい気もする。
コンテキストで検証対象が違うこともあるだろうし、createはなくupdateだけのコンテキストもあり得る。
ただ、それらの共通処理をどこに書くべきか?と考えると、わざわざ別に作ってimportなりuseなりするよりも1つにしたくなるし、やむを得ないのかなという気持ちになる。ということで、モデルs/モデル.ex
ができることが多い。
いっそのこと、ほかのコンテキストからは明確に分ける観点で、基本的なCRUD(と基本的なQueryチェイン系)以外の記述のみをlib/app/models/モデルs/モデル.ex
に詰め込んでもいいのでは、と思ったりもする。models
はモジュールにはしない、ただの箱。専用で必要であればそれはそれで用意すればいいわけなので。
「ほかのコンテキスト」は複数形を使わない、くらいのルールがあるといいかもしれない。
=> ただ実はそんなに大変でもない気もしてきたので、一度はっきりと各スキーマを混戦しないでコンテキストの下に置く思考で作ってみるのがいいかもしれない。つまり冒頭の「本当は、Ectoのスキーマはコンテキストごとに定義があってもいい気もする」に戻る。
ただし、確実にコンテキストが依存している場合はスキーマ関連において参照するのはあり、くらいだろうか。マスタと紐づくデータみたいなもの。そうしないと各マスタのスキーマを、各コンテキストに置く必要がでてくるため。...スキーマはもう1コンテキストでいいのでは、、、(ループ)

コンテキスト内部で生じる共通処理
- 部品的なデータを扱うとき
- 他サービスのAPIをたたくとき
- ...
独立したコンテキストとして、トップレベルに作っておいても良いかもしれない。

preloadやページング
画面都合で生じるロジックをどこで行うか
=> コンテキストを強く出すとおそらく_web/
側ではなく、コンテキスト側になる。ただ、実際にはどちらもオプション的に扱いたい。