[[Mastodonを読む]] #contents *はじめに [#kaf93939] 前回までで、「/」にアクセスしたときに返されるHTMLがわかりました。しかし、返されるHTMLは空のdivタグがひとつあるだけでした。もちろん、実際に表示される画面はそのような画面ではなく、以下のように表示されます。 &ref(mastodon-sample.jpg); これを行っているのがReactのようです。では読解を始めましょう。 *app/assets/javascripts/application.js [#fe35250a] Reactについては[[「5分で理解する React.js」>http://qiita.com/tomzoh/items/7fabe7cb57dd96425867]]参照。この記事によると #code{{ React.render( <CommentBox />, document.getElementById('content') ); }} のように書くことでReactで定義したコンポーネントが描画されるとありますが、出力HTMLやmastodonソース以下をgrepしてみてもそのような記述は見当たりません。gemで処理されると言ってしまえばそれまでですが、どうにもすっきりしないのでどのように描画のトリガーがかかっているのかまず確認します。 ちなみに、これを読んでたちょうどそのころぐらいにJavascriptまとめるのはWebpack使うように変更したらしくて以降の記述はv1.3.2では当てはまりますが、HEADではファイルはそのままの位置にはありません_(┐「ε:)_ ビューにリンクされるJavascriptファイルのルートはapplication.jsになります。 #code(javascript){{ //= require jquery2 //= require jquery_ujs //= require components }} 念のため。「//」はJavascriptではコメントですが、Railsでは「//=」に意味を持たせています。requreで指定されたファイルを取り込んでいます。 ちなみに、これを読んでたちょうどそのころぐらいにJavascriptまとめるのはWebpack使うように変更したらしくて以降の記述はv1.3.2では当てはまりますが、HEADではファイルはそのままの位置にはありません_(┐「ε:)_ *app/assets/javascripts/components.js [#pd31dd8d] components.jsに移動。 #code(javascript){{ //= 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 [#k913f46f] Rails的にrequireされているreact_ujsに進みましょう。ここから[[react-rails>https://github.com/reactjs/react-rails]]のgemに入ります。 #code(javascript){{ //= 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 [#tcd9927b] react_ujs_turbolinks.jsを見てみましょう。 #code(javascript){{ ;(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 [#d5e91e89] react_ujs_event_setup.js。前半でhandleEventを定義し、後半で利用可能なものを確認して対応するsetupメソッドを呼び出しています。 #code(javascript){{ ;(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 [#affe04d8] ではここまでわかったところで飛ばしたreact_ujs_mount.jsを見てみましょう。 #code(javascript){{ ;(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の出力に含まれていました。 #code(html){{ <body class='app-body'> <div data-react-class="Mastodon" data-react-props="{"locale":"ja"}" class="app-holder"></div> </body> }} mountComponentsの定義 #code(javascript){{ 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 #code(javascript){{ // 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で #code(javascript){{ window.Mastodon = require('./components/containers/mastodon'); }} と初期化されていたものが使われていると思われます。 *おわりに [#p5eb7873] 今回はReactで画面描画がされる前段階として、そもそもReactがどのように呼び出されているのかを見てきました。動くのだろうけど、じゃあ実際どのような仕組みで動いているのかを見ることでアプリからの情報の読み込み、また、利用可能なライブラリを確認して分岐するのような泥臭い処理も確認できました。 次回はいよいよMastodonコンポーネントの中に踏み込んでいって実際の画面描画処理を見ていきます。ちょっと見た感じではReactというかReduxの処理を確認していくのがめんどくさそうでしたが(笑)