Mastodonを読む
Mastodonを読む
はじめに  †
Reactを利用して画面描画するところまで来ました。ここからは個々のコンポーネントについて確認していきましょう。具体的にはホームのタイムラインがどのように表示されているかを見ていきます。
app/assets/javascripts/components/features/home_timeline/index.jsx  †
ホームのタイムラインを表示しているコンポーネントはHomeTimelineです。ちなみに、ローカルタイムラインはCommunityTimeline、連合タイムラインはPublicTimelineです。なんでずれてるんだろ、歴史的経緯かな。
HomeTimeline以降のコンポーネントの階層は以下のようになっています。トゥートと呼んでいる「つぶやき」は内部ではStatusという名前で呼ばれているようです。これはOStatusに対応した名前でしょう。
HomeTimeline
  StatusListContainer
    StatusList
      StatusContainer
        Status
なお、Containerは対応するHTMLが出力されるわけではなく、イベントハンドラなどの処理が付与されているようです。ここら辺はまあそんなものなんだろうなと眺めればいいと思います。
ファイルの場所は次の通り。いろんなところに飛んでいますがよく確認すれば迷いはしません。
- app/assets/javascripts/components/features/home_timeline/index.jsx
- app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
- app/assets/javascripts/components/components/status_list.jsx
- app/assets/javascripts/components/containers/status_container.jsx
- app/assets/javascripts/components/components/status.jsx
ところで、これらのファイルを見ていってもデータをどこから取得しているかわかりません。というより、「いつの間にか設定された」データを表示しているだけです。まあ確かにViewという点で言うと正しい動作になっています。
app/assets/javascripts/components/features/ui/index.jsx  †
Reduxの動作を思い出してみましょう。Reduxではユーザ入力に対してActionが生成され、それをStoreにdispatch、Store内のReducerがStateの更新を行う(新しいStateを返す)という実行フローでした。というわけで、ユーザ入力ではありませんが「初めに表示されたときにタイムライン情報を取得して表示」みたいな処理がどこかにあると思われます。
というか、見出しでネタバレしていますが(笑)、UIクラスに書かれています。componentWillMountメソッド、
|   1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 |   componentWillMount () {
    window.addEventListener('resize', this.handleResize, { passive: true });
    document.addEventListener('dragenter', this.handleDragEnter, false);
    document.addEventListener('dragover', this.handleDragOver, false);
    document.addEventListener('drop', this.handleDrop, false);
    document.addEventListener('dragleave', this.handleDragLeave, false);
    document.addEventListener('dragend', this.handleDragEnd, false);
 
    this.props.dispatch(refreshTimeline('home'));
    this.props.dispatch(refreshNotifications());
  }
 | 
refreshTimeline、明らかに当たりです。
app/assets/javascripts/components/actions/timelines.jsx  †
refreshTimelineはactionsの下、timelinesに書かれている関数です。
|   1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 | export function refreshTimeline(timeline, id = null) {
  return function (dispatch, getState) {
    if (getState().getIn(['timelines', timeline, 'isLoading'])) {
      return;
    }
 
    const ids      = getState().getIn(['timelines', timeline, 'items'], Immutable.List());
    const newestId = ids.size > 0 ? ids.first() : null;
    let params     = getState().getIn(['timelines', timeline, 'params'], {});
    const path     = getState().getIn(['timelines', timeline, 'path'])(id);
 
    let skipLoading = false;
 
    if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) {
      if (id === null && getState().getIn(['timelines', timeline, 'online'])) {
        // Skip refreshing when timeline is live anyway
        return;
      }
 
      params          = { ...params, since_id: newestId };
      skipLoading     = true;
    }
 
    dispatch(refreshTimelineRequest(timeline, id, skipLoading));
 
    api(getState).get(path, { params }).then(response => {
      const next = getLinks(response).refs.find(link => link.rel === 'next');
      dispatch(refreshTimelineSuccess(timeline, response.data, skipLoading, next ? next.uri : null));
    }).catch(error => {
      dispatch(refreshTimelineFail(timeline, error, skipLoading));
    });
  };
};
 | 
初回、二回目以降(すでに取得してるのより新しいものだけ取得)で処理が分かれている雰囲気ですが要約すると、State中のpathを取得し、API呼び出し、結果を設定、ということをしているようです。
app/assets/javascripts/components/reducers/timelines.jsx  †
pathの情報はreducersの下のtimelinesに書かれています。
|   1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 | const initialState = Immutable.Map({
  home: Immutable.Map({
    path: () => '/api/v1/timelines/home',
    next: null,
    isLoading: false,
    online: false,
    loaded: false,
    top: true,
    unread: 0,
    items: Immutable.List()
  }),
 | 
routes.rbを読んだのはだいぶ前だったので忘れかけていますが、このようなパスがあったはずです。というか再掲
|   1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 | -
|
|
-
-
|
|
|
|
|
|
|
!
!
 |   namespace :api do
    省略
 
    namespace :v1 do
      省略
 
      get '/timelines/home',     to: 'timelines#home', as: :home_timeline
      get '/timelines/public',   to: 'timelines#public', as: :public_timeline
      get '/timelines/tag/:id',  to: 'timelines#tag', as: :hashtag_timeline
 
      省略
    end
  end
 | 
app/assets/javascripts/components/api.jsx  †
apiはcomponents直下のapi.jsxで定義されています。その実体はaxiosというHTTPクライアントライブラリのようです。
おわりに  †
というわけでタイムラインのトゥート(Status)を取得する流れを見てきました。実際にはサーバにリクエストしてそれが返ってきた後、ReducerによりStateへの設定、Reactのコンポーネントに移ってコンポーネントの表示設定が行われるわけですが、淡々とがんばっているだけなのでサクッと省略します。
言語とか立場とかが切り替わるので一旦ここまで。