Mastodonを読む

はじめに

前回までで、「/」にアクセスしたときに返されるHTMLがわかりました。しかし、返されるHTMLは空のdivタグがひとつあるだけでした。もちろん、実際に表示される画面はそのような画面ではなく、以下のように表示されます。

&ref(): File not found: "mastdodon-sample.jpg" at page "Mastodonを読む/Reactが動く仕組み";

これを行っているのがReactのようです。では読解を始めましょう。

app/assets/javascripts/application.js

Reactについては「5分で理解する React.js」参照。この記事によると

  1
  2
  3
  4
React.render(
  <CommentBox />,
  document.getElementById('content')
);

のように書くことでReactで定義したコンポーネントが描画されるとありますが、出力HTMLやmastodonソース以下をgrepしてみてもそのような記述は見当たりません。gemで処理されると言ってしまえばそれまでですが、どうにもすっきりしないのでどのように描画のトリガーがかかっているのかまず確認します。

ビューにリンクされるJavascriptファイルのルートはapplication.jsになります。

Everything is expanded.Everything is shortened.
  1
  2
  3
-
|
|
//= require jquery2
//= require jquery_ujs
//= require components

念のため。「//」はJavascriptではコメントですが、Railsでは「//=」に意味を持たせています。requreで指定されたファイルを取り込んでいます。

app/assets/javascripts/components.js

components.jsに移動。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
-
|
!
 
 
 
 
-
|
|
!
 
-
!
 
//= require_self
//= require react_ujs
 
window.React    = require('react');
window.ReactDOM = require('react-dom');
window.Perf     = require('react-addons-perf');
 
if (!window.Intl) {
  require('intl');
  require('intl/locale-data/jsonp/en.js');
}
 
//= require_tree ./components
 
window.Mastodon = require('./components/containers/mastodon');

Reactの記述が出てきました。Mastodonの文字も出てきましたが、ここでもまだ「じゃあ結局どのようにして動いているのか」がわかりません。

react-rails/lib/assets/javascripts/react_ujs.js

Rails的にrequireされているreact_ujsに進みましょう。ここからreact-railsのgemに入ります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
-
|
|
|
|
|
|
//= require react_ujs_mount
//= require react_ujs_turbolinks
//= require react_ujs_turbolinks_classic
//= require react_ujs_turbolinks_classic_deprecated
//= require react_ujs_pjax
//= require react_ujs_native
//= require react_ujs_event_setup

たくさんrequireされていますが、turbolinks~nativeは実はバリエーションで記述はほぼ同じです。

react_ujs_turbolinks.js

react_ujs_turbolinks.jsを見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
-
-
-
-
|
|
!
!
!
;(function(document, window) {
  window.ReactRailsUJS.Turbolinks = {
    // Turbolinks 5+ got rid of named events (?!)
    setup: function() {
      ReactRailsUJS.handleEvent('turbolinks:load', function() {window.ReactRailsUJS.mountComponents()});
      ReactRailsUJS.handleEvent('turbolinks:before-render', function() {window.ReactRailsUJS.unmountComponents()});
    }
  };
})(document, window);

初めのセミコロン何なんだろうと思ったのですが、前のファイルがセミコロンで終わってない時対策なのかな。それはともかく、Turbolinksオブジェクトにsetupメソッドが定義されています。handleEvent呼び出しを見ると画面が表示されるときに処理が行われるであろうことが予想できます。

react_ujs_event_setup.js

react_ujs_event_setup.js。前半でhandleEventを定義し、後半で利用可能なものを確認して対応するsetupメソッドを呼び出しています。

Everything is expanded.Everything is shortened.
  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
-
-
!
-
-
|
!
-
-
|
!
!
-
-
-
-
!
-
-
!
-
|
!
-
|
-
|
!
!
;(function(document, window) {
  // jQuery is optional. Use it to support legacy browsers.
  var $ = (typeof window.jQuery !== 'undefined') && window.jQuery;
  if ($) {
    ReactRailsUJS.handleEvent = function(eventName, callback) {
      $(document).on(eventName, callback);
    };
  } else {
    ReactRailsUJS.handleEvent = function(eventName, callback) {
      document.addEventListener(eventName, callback);
    };
  }
  // Detect which kind of events to set up:
  if (typeof Turbolinks !== 'undefined' && Turbolinks.supported) {
    if (typeof Turbolinks.EVENTS !== 'undefined') {
      // Turbolinks.EVENTS is in classic version 2.4.0+
      ReactRailsUJS.TurbolinksClassic.setup();
    } else if (typeof Turbolinks.controller !== "undefined") {
      // Turbolinks.controller is in version 5+
      ReactRailsUJS.Turbolinks.setup();
    } else {
      ReactRailsUJS.TurbolinksClassicDeprecated.setup();
    }
  } else if ($ && typeof $.pjax === 'function') {
    ReactRailsUJS.pjax.setup();
  } else {
    ReactRailsUJS.Native.setup();
  }
})(document, window);

react_ujs_mount.js

ではここまでわかったところで飛ばしたreact_ujs_mount.jsを見てみましょう。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
-
-
!
|
-
-
|
!
|
-
|
!
;(function(document, window) {
  // jQuery is optional. Use it to support legacy browsers.
  var $ = (typeof window.jQuery !== 'undefined') && window.jQuery;
 
  window.ReactRailsUJS = {
    // This attribute holds the name of component which should be mounted
    // example: `data-react-class="MyApp.Items.EditForm"`
    CLASS_NAME_ATTR: 'data-react-class',
 
    // This attribute holds JSON stringified props for initializing the component
    // example: `data-react-props="{\"item\": { \"id\": 1, \"name\": \"My Item\"} }"`
    PROPS_ATTR: 'data-react-props',

data-react-class属性は前回見たhome#indexの出力に含まれていました。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
<body class='app-body'>
<div data-react-class="Mastodon" data-react-props="{&quot;locale&quot;:&quot;ja&quot;}" class="app-holder">div>
 
body>

mountComponentsの定義

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
-
|
|
-
|
|
|
|
|
|
-
|
|
|
|
-
|
!
!
!
    mountComponents: function(searchSelector) {
      var nodes = window.ReactRailsUJS.findDOMNodes(searchSelector);
 
      for (var i = 0; i < nodes.length; ++i) {
        var node = nodes[i];
        var className = node.getAttribute(window.ReactRailsUJS.CLASS_NAME_ATTR);
        var constructor = this.getConstructor(className);
        var propsJson = node.getAttribute(window.ReactRailsUJS.PROPS_ATTR);
        var props = propsJson && JSON.parse(propsJson);
 
        if (typeof(constructor) === "undefined") {
          var message = "Cannot find component: '" + className + "'"
          if (console && console.log) { console.log("%c[react-rails] %c" + message + " for element", "font-weight: bold", "", node) }
          var error = new Error(message + ". Make sure your component is globally available to render.")
          throw error
        } else {
          ReactDOM.render(React.createElement(constructor, props), node);
        }
      }
    },

renderありました。findDOMNodesは省略してgetConstructor

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
-
-
-
|
|
|
!
|
-
!
|
-
-
|
!
|
-
-
|
!
|
|
!
    // Get the constructor for a className
    getConstructor: function(className) {
      // Assume className is simple and can be found at top-level (window).
      // Fallback to eval to handle cases like 'My.React.ComponentName'.
      // Also, try to gracefully import Babel 6 style default exports
      //
      var constructor;
 
      // Try to access the class globally first
      constructor = window[className];
 
      // If that didn't work, try eval
      if (!constructor) {
        constructor = eval.call(window, className);
      }
 
      // Lastly, if there is a default attribute try that
      if (constructor && constructor['default']) {
        constructor = constructor['default'];
      }
 
      return constructor;
    },

いろいろやっていますが、components.jsで

Everything is expanded.Everything is shortened.
  1
 
window.Mastodon = require('./components/containers/mastodon');

と初期化されていたものが使われていると思われます。

おわりに

今回はReactで画面描画がされる前段階として、そもそもReactがどのように呼び出されているのかを見てきました。動くのだろうけど、じゃあ実際どのような仕組みで動いているのかを見ることでアプリからの情報の読み込み、また、利用可能なライブラリを確認して分岐するのような泥臭い処理も確認できました。

次回はいよいよMastodonコンポーネントの中に踏み込んでいって実際の画面描画処理を見ていきます。ちょっと見た感じではReactというかReduxの処理を確認していくのがめんどくさそうでしたが(笑)


トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS