Mastodonを読む

はじめに

ここまでのおさらい。

  • 「/」(home#index)にアクセスしたときに返されるのは空のdivタグ一つ
  • MastodonはReactとReduxを使用している
  • react-railsを使うことでdata-react-class属性を付けたコンポーネントに対してrenderが行われる

今回はこのrenderの先を見ていきます。

app/assets/javascripts/components/containers/mastdon.jsx

読解のスタートとなるのはmastodon.jsxです。ファイルの初めはimport祭ですが、よく見ると2種類あることがわかります。

  • パス指定のないもの。react-reduxやreact-routerなど。これらは外部ライブラリと思われます。
  • パス指定のあるもの。../store/configureStore、../actions/timelines、../features/uiなど。これらはMastodonで定義されているものと思われます。

importが終わると次の行があります。

  1
  2
  3
  4
  5
  6
  7
const store = configureStore();
const initialState = JSON.parse(document.getElementById("initial-state").textContent);
store.dispatch(hydrateStore(initialState));
 
const browserHistory = useRouterHistory(createBrowserHistory)({
  basename: '/web'
});

Mastodonではローカルタイムライン、連合タイムラインなどをクリックすると画面全体を再描画することなく表示が切り替わります。browserHistoryはそれに関わってそうです。

storeはReduxで出てくる概念です。「Redux入門【ダイジェスト版】10分で理解するReduxの基礎」あたりを読むとReduxとは何ものなのかがわかります。

その後のロケールの処理と思われるところは飛ばして、Containerクラスの定義も一旦飛ばして、Mastodonクラスのrenderメソッドを見てみましょう。

  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
 34
  render () {
    const { locale } = this.props;
 
    return (
      <IntlProvider locale={locale} messages={getMessagesForLocale(locale)}>
        <Provider store={store}>
          <Router history={browserHistory} render={applyRouterMiddleware(useScroll())}>
            <Route path='/' component={Container}>
              <IndexRedirect to="/getting-started" />
 
              <Route path='getting-started' component={GettingStarted} />
              <Route path='timelines/tag/:id' component={HashtagTimeline} />
 
              <Route path='statuses/new' component={Compose} />
              <Route path='statuses/:statusId' component={Status} />
              <Route path='statuses/:statusId/reblogs' component={Reblogs} />
              <Route path='statuses/:statusId/favourites' component={Favourites} />
 
              <Route path='accounts/:accountId' component={AccountTimeline} />
              <Route path='accounts/:accountId/followers' component={Followers} />
              <Route path='accounts/:accountId/following' component={Following} />
 
              <Route path='follow_requests' component={FollowRequests} />
              <Route path='blocks' component={Blocks} />
              <Route path='mutes' component={Mutes} />
              <Route path='report' component={Report} />
 
              <Route path='*' component={GenericNotFound} />
            </Route>
          </Router>
        </Provider>
      </IntlProvider>
    );
  }

(。´・ω・)?

Router

PrroviderはReduxにより提供されているコンポーネントです。IntlProviderも国際化処理と思われるのでまあいいでしょう。

というわけで、調べる必要があるのはRouterとRouteです。なんとなくイメージはつきますが想像が正しいのか確認していきましょう。

Routerコンポーネントは次のようにimportされています。

  1
  2
  3
  4
  5
  6
  7
  8
import {
  applyRouterMiddleware,
  useRouterHistory,
  Router,
  Route,
  IndexRedirect,
  IndexRoute
} from 'react-router';

React RouterはReactで画面遷移(特に画面の一部が切り替わるような画面遷移?)を実装する際によく利用されるライブラリのようです。特徴として、各画面に対して個別のURLが割り振られる(ブックマークできる)ということがあるようです。

ところで、「/」にアクセスしているのにいつの間にかアドレスバーは「/web/getting-started」になっていることに気付いているでしょうか。これは大きなヒントになりそうです。該当部分だけ抜き出すと、

  1
  2
  3
  4
            <Route path='/' component={Container}>
              <IndexRedirect to="/getting-started" />
 
              <Route path='getting-started' component={GettingStarted} />
  1. 画面全体を表示するコンポーネントとしてContainerを使う
  2. 「/」へのアクセスは「/getting-started」にリダイレクトする
  3. 「getting-started」にアクセスされたらGettingStartedコンポーネントを使う

という動きをするようです。

react-routerのバージョン

ところで、React Routerのページに行ってドキュメントを見てもIndexRedirectの記述はありません。 実は、Mastodonが利用しているreact-routerのバージョンは2.8で結構古めです。利用しているバージョンの確認には、ルートにあるpackage.jsonを参照します。

  1
  2
  3
  "dependencies": {
    省略
    "react-router": "^2.8.0",

Container

さて、画面全体にContainerクラスが使われることがわかったのでContainerクラスの方に移りましょう。renderメソッドは以下のようになっています。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
  render () {
    // Hide some components rather than unmounting them to allow to show again
    // quickly and keep the view state such as the scrolled offset.
    const persistentsView = this.state.renderedPersistents.map((persistent) =>
      <div aria-hidden={persistent.hidden} key={persistent.pathname} className='mastodon-column-container' style={persistent.hidden ? hiddenColumnContainerStyle : null}>
        <persistent.component shouldUpdateScroll={persistent.hidden ? Function.prototype : getTopWhenReplacing} />
      </div>
    );
 
    return (
      <UI>
        {this.state.mountImpersistent && this.props.children}
        {persistentsView}
      </UI>
    );
  }

前半部分はコメントにあるように一回作成したコンポーネントを再利用するための処理です。mountImpersistentの値を決定する処理はrenderメソッドの上にあるcomponentWillMountメソッドで行われており、なかなか難解ですが(主に無名関数を使いまくっているため)今回は初回ということで単純にthis.props.childrenが描画されると考えていいでしょう。ちなみに、this.props.childrenにはこのコンポーネントの子、今の場合はGettingStartedが使われます。(参考:「ReactJS入門@ES6:ReactRouter編」

app/assets/javascripts/components/features/ui/index.jsx

では次にUIコンポーネントに移りましょう。UIコンポーネントはfeatures/uiにあります。index.jsxがあるとそれが読み込まれる形式なのかな?

例によってrenderメソッドです。(divのstyleのところは本来実際のコードでは{{ }}とスペースは空いてませんが、そのままだとWiki的に表示が死ぬので空けています。この後のGettingStartedも同様)

  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
 34
 35
 36
  render () {
    const { width, draggingOver } = this.state;
    const { children } = this.props;
 
    let mountedColumns;
 
    if (isMobile(width)) {
      mountedColumns = (
        <ColumnsArea>
          {children}
        </ColumnsArea>
      );
    } else {
      mountedColumns = (
        <ColumnsArea>
          <Compose withHeader={true} />
          <HomeTimeline shouldUpdateScroll={() => false} />
          <Notifications shouldUpdateScroll={() => false} />
          <div style={ {display: 'flex', flex: '1 1 auto', position: 'relative'} }>{children}</div>
        </ColumnsArea>
      );
    }
 
    return (
      <div className='ui' ref={this.setRef}>
        <TabsBar />
 
        {mountedColumns}
 
        <NotificationsContainer />
        <LoadingBarContainer className="loading-bar" />
        <ModalContainer />
        <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
      </div>
    );
  }

画面幅に応じて表示するコンポーネントを変えています。これにより、PCからアクセスされた時とスマホからアクセスされた時で見た目を変えているようですね。

app/assets/javascripts/components/features/getting_started/index.jsx

最後に、GettingStartedコンポーネントです。って、クラスじゃなくて関数オブジェクト?

  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
 34
 35
 36
 37
const GettingStarted = ({ intl, me }) => {
  let followRequests = '';
 
  if (me.get('locked')) {
    followRequests = <ColumnLink icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />;
  }
 
  return (
    <Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile={true}>
      <div className='getting-started__wrapper'>
        <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)}/>
        <ColumnLink icon='users' hideOnMobile={true} text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />
        <ColumnLink icon='globe' hideOnMobile={true} text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
        <ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
        {followRequests}
        <ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
        <ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
        <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)}/>
        <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
        <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
        <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
      </div>
 
      <div className='scrollable optionally-scrollable' style={ { display: 'flex', flexDirection: 'column' } }>
        <div className='static-content getting-started'>
          <p>
            <FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' 
              values={ { 
                github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, 
                apps: <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> 
              } } />
          </p>
        </div>
      </div>
    </Column>
  );
};

最後のnoticeのところ、FormattedMessageの表示要素としてまたFormattedMessageが使われててわかりにくいですね(実際にはこんなきれいに改行されていません)、pull request投げようかな。

ともかく、これにより表示されているのはPCだと右端のカラム、スマホだと「ナビゲーション」から下の部分です。スマホで一番上に表示されているのはTabsBarコンポーネントです。(PCだとTabsBarコンポーネントはCSSで非表示にされています)

getting-start_pc.jpg

getting-start_smartphone.jpg

おわりに

今回はコンポーネントのレンダリングについて見てきました。ReactRouterを使い画面の表示制御が行われていました。画面全体はUIコンポーネントになっており、画面幅により表示するコンポーネントの切り替えを行っていました。ここら辺までわかれば後は個別のコンポーネントを追っかけていって見た目のカスタマイズもできるでしょう。

一方、以下の処理はまだ手付かずです。以降、これらの処理について見ていきたいと思います。

  • タイムラインの表示
  • トゥートした時の処理
  • Mastodonクラスに書いてあるストリーミングの記述

添付ファイル: filegetting-start_smartphone.jpg 689件 [詳細] filegetting-start_pc.jpg 666件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-05-06 (土) 10:41:04 (2774d)