TCPのウィンドウ制御とコネクション識別
ウィンドウ制御
TCPは送信と応答確認によってデータ送信の信頼性を高めているが、パケット毎にそれをやっていると効率が悪い。
この問題を解決するために、ウィンドウ制御という概念を取り入れている。
具体的には、応答確認をする単位を1パケットではないく、ある一定サイズ(ウィンドウサイズ)ごとに行うようにすることで、通信を効率化している。
サイズの目安
1パケット 1.5KB (イーサネットの最大パケットサイズ)
ウィンドウサイズの最大 64KB このサイズをスケールさせる規約もあり、近代のOSは対応しているらしい(気が向いたら調べる)
ASCII.jp:帯域を効率的に利用するTCPの仕組みとは? (1/2)|TCP/IPまるわかり
コネクション識別
TCPヘッダには接続IDのフィールドがないので、どうやっているのか気になったので調べてみた。
2つのendpointのIP:portで識別している。
The TCP/IP Guide - TCP Ports, Connections and Connection Identification
initialPropsとstate - React.js
Props in getInitialState Is an Anti-Pattern | React
getInitialState内でpropsを用いてstateを生成するやり方は、どのデータが本筋のデータかがわかりづらくなってしまう状況になってしまうことがあるので、注意して使ったほうがいい。
コメントフォームを例に考えてみる。
var CommentBox = React.createClass({ getInitialState: function(){ return { comment: this.props.comment } }, handleChange: function(e) { this.setState({comment: e.target.value}) }, render: function() { return <div className='comment-box'> <textarea onChange={this.handleChange} defaultValue={this.state.comment} /> </div> } })
上記のようなコードの場合、props.commentとstate.commentがあり、コードの複雑性が増してくると、どちらがどのケースで本筋のデータかがわかりづらくなってしまう可能性がある。
なので、下記のようにpropsは明示的に初期化用のデータということを表す名前にしたほうが良い。
var CommentBox = React.createClass({ getInitialState: function(){ return { comment: this.props.initialComment } }, handleChange: function(e) { this.setState({comment: e.target.value}) }, render: function() { return <div className='comment-box'> <textarea onChange={this.handleChange} defaultValue={this.state.comment} /> <button onClick={this.handleUpdateClick} </div> } })
Model周り - helpy code reading
共通部分を切り出す
CakePHPでいうBehaviorやComponent, Helper
Rails 共通部分を切り出す (View, Controller, Model) メモ - Qiita
関連したモデルのフォームでの扱い
Rails ネストした関連先のテーブルもまとめて保存する (accepts_nested_attributes_for、fields_for) - Qiita
helpyではtopicが複数のpostを持つ事ができる構造で、topic.rb内で 'accepts_nested_attributes_for :posts' と記述し、
viewでは下記のように定義している。
<%= f.simple_fields_for :posts do |p| %> <%= p.input :body, input_html: {:rows => 8, :cols => 60, placeholder: I18n.t(:how_can_we_help), label: 'Message', class: 'disable-empty form-control topic-placeholder'} %> <% end %>
model attachments
こういったプラグインはどのMVCフレームワークでも頻繁にお世話になりそう。
GitHub - assembler/attachinary: Attachments handler for Rails that uses Cloudinary for storage.
postgresに特化にした検索用のGem
# モデル内で下記ように定義すると include PgSearch pg_search_scope :admin_search, against: [:id, :name, :user_name, :current_status, :post_cache] # 下記のように呼び出せる @topics = Topic.admin_search(params[:q]).page params[:page]
polymorphicな関連
helpyではtopicsとpostsに投票機能が付いていて、どちらもvoteモデルの関連で実現されている。
# vote.rb class Vote < ActiveRecord::Base belongs_to :voteable, :polymorphic => true ... # topic.rb class Topic < ActiveRecord::Base ... has_many :votes, :as => :voteable
ログイン後の初期設定 - helpy code reading
初期ログイン後に表示される初期設定画面周りのコードを見ていく。
流れ
ログイン後のリダイレクト
admin's root admin/dashboard#index to admin/topics#index
def index #@topics = Topic.mine(current_user.id).pending.page params[:page] if current_user.is_admin? || current_user.is_agent? redirect_to admin_topics_path elsif current_user.is_editor? redirect_to admin_categories_path else redirect_to root_url end end
ダイアログの表示
初期設定 = onboardingというキーワードで表現されていて、済みかの判定は下記のメソッドで定義されている。
# Admin::BaseController def show_onboarding? User.first.email == 'admin@test.com' && current_user.email == 'admin@test.com' && current_user.is_admin? end
上記のメソッドをhelper_methodに登録して、html側で呼び出している。
# views/admin/topics/index.html.erb <% if show_onboarding? %> <div id='modal' class="modal" tabindex="-1" role="dialog" data-backdrop="static"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <iframe src="/admin/onboarding/index" width="100%" height="900" frameborder="no" scrolling="no"></iframe> </div> </div> </div> <script> // Fire the onboarding modal if needed $('#modal').modal('toggle'); </script> <% end %>
onboarding modal
上記のmodal内のiframeに設定されているところを見ていく。
登録のステップとして、メッセージ表示、サービス設定、アカウント設定がある。
htmlは全てonboarding/index.html.erbに記載されていて、class onboard-panel を遷移させていく形で実装してある。
panel遷移のJS処理は assets/javascripts/admin.js に描かれている
var Helpy = Helpy || {}; ... Helpy.showPanel = function(panel) { var currentPanel = panel-1; $('.onboard-panel').addClass('hidden'); $('#panel-' + panel).removeClass('hidden'); $('li.step-' + currentPanel).html("<span class='glyphicon glyphicon-ok'></span>").addClass('filled-circle'); $('li.step-' + panel).addClass('active-step'); return true; }; ...
サービス設定のフォームはAjax化されている。
# onboarding/index.html.erb ... <div id="panel-2" class="onboard-panel text-center hidden"> <div class="onboard-message"> <h2>Great! Lets Customize your Helpdesk:</h2> </div> <div class="text-left"> <%= bootstrap_form_tag url: admin_onboard_settings_path, method: 'patch', remote: true do |f| %> ...
コントローラー側は下記のようになっている。
# admin/onboarding_controoer.rb def update_settings 設定保存処理 respond_to do |format| format.html { redirect_to(admin_settings_path) } format.js { render js: "Helpy.showPanel(3);$('#edit_user_1').enableClientSideValidations();" } end end
アカウント設定のフォームはAjaxではなくて、onboarding#update_user で設定保存後にonboarding#complete リダイレクトして動作完了。
参考になる箇所
routes namespace
routes as
全てのpathにasを設定していて、****_path でpathを取得している。
form_tag remote => trueで簡単にAjaxフォーム化
controller#helper_methodo
Ruby/Ruby on Rails/コントローラーのメソッドをビューから呼び出す方法 - TOBY SOFT wiki
Rails Setting Cached
Decorator Pattern - Design Pattern in Ruby
あるオブジェクトの機能に追加の機能を付け足したい、
さらにいろいろな組み合わせで追加機能を実装したいという時に使えるパターン。
Interface
元の機能を持ったオブジェクト(ConcreteComponent)と追加機能を実装するオブジェクト(Decorator)という登場人物がいて、同じインターフェース(Component)を持つことで、クライアントからは実際にはどのオブジェクトを扱っているかを気にせずに使用できるようになる。
Traditional implementation
DecoratorオブジェクトにConcreteComponentオブジェクトの参照を渡し、追加機能以外の部分をConcreteComponentに委譲する。
Easing the Delegation
forwardableモジュールをextendすることで、delegateの処理を簡潔に書くことができる。
In Ruby
Wrapping Methods
w = SimpleWriter.new('out') class << w alias old_write_line write_line def write_line(line) old_write_line("#{Time.new}: #{line}") end end
元のメソッドの参照をaliasキーワードで old**に移して、old**をdecorateするメソッドを新たに定義する。
ちょっとした処理を簡単に追加できるが、複数のdecorate処理を買いたい時に、aliasに設定する名前が難しくなってくる。
Decorating with Modules
instance.extend(module) の方法でインタンスにmoduleを設定することで実現する。
簡単なテキストデコレーションを書いてみた。
class TextPrinter def output(str) print str end end module TextEmphasizeDecorator def output(str) print "** " print super(str) print " **" end end module TextBracketsDecorator def output(str) print '(' super str print ')' end end to = TextPrinter.new to.extend(TextEmphasizeDecorator) to.extend(TextBracketsDecorator) to.output('hoge')
redux/examples/real-world を見て学ぶ
GitHub - reactjs/redux: Predictable state container for JavaScript apps
Githubのアカウント名を入力すると、 starしているリポジトリと作者の一覧が表示される。
一覧表示のリポジトリ名やアカウント名をクリックすると、それぞれ対応したページに切り替わる
ページ遷移
redux-router を活用してアカウント、リポジトリをURLで表現することで、ページの遷移を実現している。
以下のコードで、アカウント名を入力してgoボタンを押すと、"/アカウント名" に遷移する処理を実現。
// container/App.jsから対象部分を抜粋 import { browserHistory } from 'react-router' import Explore from '../components/Explore' ... class App extends Component { ... handleChange(nextValue) { browserHistory.push(`/${nextValue}`) }, ... render() { const { children, inputValue } = this.props return ( <div> <Explore value={inputValue} onChange={this.handleChange} /> <hr /> {this.renderErrorMessage()} {children} </div> ) } ... }
// container/Explore.js内 export default class Explore extends Component { ... handleGoClick() { this.props.onChange(this.getInputValue()) }, ... }
ページ遷移後のデータ取得
処理の流れは下記のようなイメージ。
- URLからアカウント名 or リポジトリ名を取得して、github apiをcallするアクションを実行
- api callを処理するmiddlewareでアクションを type:REQUEST, type:SUCCESS or FAILURE に分割したアクションを生成
- SUCCESSを処理するreducerからstateを生成
URLからアカウント名 or リポジトリ名を取得して、github apiをcallするアクションを実行
redux-routerの機能で、URLに応じて、読み込むコンポーネントを切り分けて、さらにURLからコンポーネントへ渡すPropsを定義している。
// ./routes.js export default ( <Route path="/" component={App}> <Route path="/:login/:name" component={RepoPage} /> <Route path="/:login" component={UserPage} /> </Route> )
この記述により、例えばUserPageの場合、mapStateToProps内で、 アカウント名を取得することが可能
// containers/UserPage.js ... function mapStateToProps(state, ownProps) { ... const login = ownProps.params.login.toLowerCase() // アカウント名を取得 ... }
UserPage#componentWillMount でアカウント名、Github APIからデータを取得するアクションをdispatchする
// containers/UserPage.js ... import { loadUser, loadStarred } from '../actions' ... function loadData(props) { const { login } = props props.loadUser(login, [ 'name' ]) props.loadStarred(login) } ... class UserPage extends Component { ... componentWillMount() { loadData(this.props) } ... } ... export default connect(mapStateToProps, { loadUser, loadStarred })(UserPage)
CALL API アクションのdispatch
上記のUserPageコンポーネントでimportされる loadUser の定義.
// actions/index.js ... function fetchUser(login) { return { [CALL_API]: { types: [ USER_REQUEST, USER_SUCCESS, USER_FAILURE ], endpoint: `users/${login}`, schema: Schemas.USER } } } // Fetches a single user from Github API unless it is cached. // Relies on Redux Thunk middleware. export function loadUser(login, requiredFields = []) { return (dispatch, getState) => { const user = getState().entities.users[login] if (user && requiredFields.every(key => user.hasOwnProperty(key))) { return null } return dispatch(fetchUser(login)) } } ...
fetchUserで定義されているフォーマットは、
GitHub - agraboso/redux-api-middleware: Redux middleware for calling an API.
で定義されているAPI CALLを行うアクションのフォーマット。
アクションを処理するmiddleware
storeにmiddlewareを設定することで、特定のフォーマットのアクションへの処理を追加することができる。
例えば
GitHub - agraboso/redux-api-middleware: Redux middleware for calling an API.
を導入すると、上記のAPI CALL フォーマットのアクションを適切に処理してくれるようになる。
今回のexampleでは.middleware/api.js に自前で実装されている。
やっていることは
- actionが対象のフォーマットかどうかを確認. 対象でなければ何もしないで終了
- REQUEST typeのアクションを実行
- api call後にSUCCESS or FAILURE typeのアクションをレスポンスをくっつけて実行
感想
stateの持ち方はかなりシンプルにして、middlewareやcomponentのcomponentWillMount, componentWillReceivePropsを活用することで、複雑な実装をクリアにしている感じが参考になった。
redux/examples/shopping_cart を見て学ぶ
redux/examples/shopping-cart at master · reactjs/redux · GitHub
仕様
商品リストとカートがあり、商品をカートに入れることができる
商品は在庫があり、在庫がなくなると、sold out と表示される
カートでは合計金額を表示する
state間の連携について
stateとして、商品情報のproductsとカート情報の cartがある。
カートでは商品IDと個数を保持している。
カートで合計金額と取得したい時に、state:cart内には金額の情報がないので、productsから商品の情報を取得する必要がある。
こういった処理をどこに、どうやって書いていくかというのがreduxでよく解っていないポイント。
今回の例では, reducers/index.js に getTotal関数が実装されていて、exportされ、コンポーネントのmapStateToPropsから呼び出されている。
// reducers/index.js内の合計金額を取得する実装 function getQuantity(state, id) { return fromCart.getQuantity(state.cart, id) } function getProduct(state, id) { return fromProducts.getProduct(state.products, id) } export function getTotal(state) { return getAddedIds(state).reduce((total, id) => total + getProduct(state, id).price * getQuantity(state, id), 0 ).toFixed(2) }
reducerを単純にアクションからstateを生成する役割に加えて、stateからデータを取得する役割を負っている。
複数のstateをまたがったデータを取得する際は、index.jsに定義し、各子stateから必要な情報を取得し、調整して返すようにされている。
この程度の規模のプログラムだと問題なさそうだけど、もう少し大きくなってくるとindex.jsがカオスになりそうなので、もう少し工夫する必要がありそう。
stateの持ち方の工夫
state:productsが単純な配列ではなく、下記のように定義されている。
{ byId: {}, // id => product のオブジェクト。 state.byId[product_id] で商品データにアクセスできる visibilityIds: [] // 商品IDのリスト }
これは一長一短ありそう。
感想
基本理念以外の部分は結構自由に実装されている印象。
FWに従って思考停止ではなく、作るプログラムに合わせて色々考えて工夫する必要がありそう。