This post is Private. Only a writer or those who know its URL can access this post.

Rails アプリを個人開発からチーム開発に成長させる

自己紹介

image.png


会社紹介


今回のお話


:doughnut: 技術選定のために :doughnut:


まずは1人でサービスを作る

image.png


Rails 以外を触ってみる

image.png

eliact (1).gif


:laughing: ポジとネガ :expressionless:


ネガ

  • Rails + jQuery は「振る舞い」と「表示」をセットにするのが難しい
  • FLOCSS は粒度が粗い
  • Capistrano の読み書きがめんどくさい
  • Elixir はググれないので時間がかかる
  • React + Redux は記述量がめちゃめちゃ増える

ポジ

  • Rails はやっぱり早い
  • Service 層をリッチにするのはテストを書きやす
  • atomic design の設計は可変性高い
  • React + Redux は「振る舞い」を含めたコンポーネント化がコードに量を減らす
  • GCP が使いやすい気がする

:tada: 君に決めたっっ!! :tada:

  • 最初は Rails で開発
  • コンポーネントが見えてきたら Rails + React に変更
  • 初期の頃から atomic design の atoms はちゃんと作る
  • Controller への記述は控えて積極的に Service に切り出す
  • Service に対してのみテストを書く
  • Heroku から初めて docker を使って GCP に移行する

:cookie: まずは1人で view の設計から :cookie:


Sketch でコンポーネント設計 :pencil2:

image.png


Symbol を丁寧に作りました :sunglasses:

image.png


一旦 Rails で実装 :full_moon_with_face:

sign_up.rb
section.section
  .container
    = render 'shared/atoms/heading', title: 'アカウント登録', margin: 25

    = render 'shared/molecules/messages/default',
        title: '必要書類',
        content: '登録には会社代表者の運転免許証情報が必要となります。<br>お手元にご用意の上、登録を進めて下さい。',
        margin: 25

    = form_for resource, as: resource_name, url: registration_path(resource_name) do |f|
      = render 'shared/molecules/inputs/text',
        form: f,
        name: :name,
        label: '名前',
        placeholder: '山田 太郎',
        margin: 25
      = render 'shared/atoms/buttons/default', form: f, text: '登録', margin:25

    = render 'shared/atoms/link', path: unauthenticated_root_path, text: 'アカウントを登録済みの方こちら', margin: 40
  • Symbol をパーシャルに切り出す
  • (CSSModule っぽく)パーシャルに一対一になるように CSS を書く
  • ロジック部分は helper に書き出しておく

Heroku へ :rocket:

  • view ができた段階で一旦 Heroku に上げました
    • ユーザーインタビュー/ユーザービリティテストに使われます
    • 関係各所へのデモに使われます

フィードバックを反映

  • ユーザーに必要な情報とインタラクションが明らかになる
  • コンポーネントの「振る舞い」の設計ができるようになる
  • DB の設計ができるようになる

:lollipop: 裏側実装に移っていきます :lollipop:


複数開発に備えて実装を気をつける


before_action の乱用を控える

  • before_actionredirect_to しまくってるコードは最悪
  • 「インスタンス変数のセット」と「認証・認可」だけに使う
bad.rb
before_action :redirect_to_done_before_admission_passed

:

def redirect_to_done_before_admission_passed
  redirect_to done_sub_sub_dealers_path unless @current_sub_dealer.admission_passed?
end
good.rb
before_action :authenticate_account!
before_action :set_sub_dealer

accept_nested_attributes_for は止めておく

  • 魔法が多すぎる
  • model のリレーションまで読みにいかきゃいけないのでわかりにくい

Service に対しては必ずテスト書く

  • Service 名とテストを見れば何やっているかだいたい分かるようにする
service.rb
class Companies
  class SelfConfirm < ApplicationService
    object :account
    object :company

    validates :account, :company, presence: true

    def execute
      # ココにアプリケーション層のロジックを入れる
      # メールの配信や状態変化など 

      company
    rescue ActiveRecord::RecordInvalid
      errors.add(:state, 'が不正です。')
    end
  end
end
service_spec.rb
RSpec.describe Companies::SelfConfirm, type: :service do
  let!(:company) { create(:company, :with_account) }
  let!(:account) { company.accounts.first }

  subject do
    described_class.run(
      account: account,
      company: company
    )
  end

  it do
    expect { subject }
      .to change { company.hoge.count }.by(1)
      .and change { company.fuga.count }.by(1)
      .and change { company.reload.current_state }.from('old').to('new')
  end
end

:tada: 3人の開発チームになる :tada:


docker の準備ができておらず手こずる :sweat_smile:


ぼくの仕事

  • github にひたすら issue を積んで、コードをレビュー
  • Rails の API 化も進めてる(issue 書いただけ)
    • graphql も考えてます(issue 書いただけ
  • JS でコンポーネントに「振る舞い」の実装
  • atomic, molecules の追加
  • サービス内の文言、ページ遷移、フィードバック、マージンの修正

:candy: JS のコンポーネント設計 :candy:


グラデーションをつけて React に移行

  • rails_script でロジックが散らからない用に整理
  • atomic design の molecules に「振る舞い」が結びつくようにパーシャルを修正
sample.js
import Base from '../Base'
import autoFocus from 'common/utilities/autoFocus'

export default class AccountsConfirmations extends Base {
  show() {
    autoFocus()
  }

  confirm() {
    autoFocus()
  }
}
sample.slim
-  margin ||= 0

.m-inputs_passcode class="u-mt#{margin}"
  .field.m-inputs_passcode__field.u-mb0
    .control
      = telephone_field_tag "#{form.object.class.name.underscore}[passcodes][]",
        nil,
        maxlength: 1,
        id: 'payment_request_passcodes_1',
        class: 'input m-inputs_passcode__input js-inputs_passcode__input'
  .field.m-inputs_passcode__field.u-mb0
    .control
      = telephone_field_tag "#{form.object.class.name.underscore}[passcodes][]",
        nil,
        maxlength: 1,
        id: 'payment_request_passcodes_2',
        class: 'input m-inputs_passcode__input js-inputs_passcode__input'
  .field.m-inputs_passcode__field.u-mb0
    .control
      = telephone_field_tag "#{form.object.class.name.underscore}[passcodes][]",
        nil,
        maxlength: 1,
        id: 'payment_request_passcodes_3',
        class: 'input m-inputs_passcode__input js-inputs_passcode__input'
  .field.m-inputs_passcode__field.u-mb0
    .control
      = telephone_field_tag "#{form.object.class.name.underscore}[passcodes][]",
        nil,
        maxlength: 1,
        id: 'payment_request_passcodes_4',
        class: 'input m-inputs_passcode__input js-inputs_passcode__input'

:tada: 来週からもうひとり Join :tada:


インフラ系の準備を進める

  • docker file の作成
  • Heroku の無料プランだと限界が見えてきたので GCP に移行準備中
    • BigQuery を使ってみたい
    • 良いユーザー体験は、良い分析からだと思う

おまけ :smile:


image.png


終わり :beer:


APPENDIX


やっと React :sun_with_face:

sample.jsx
export const IconPresenter = ({
  iconName,
  height = 20,
  width = 20,
  ...props,
}) => (
  <img
    src={ iconName }
    alt=''
    height={ height }
    width={ width }
    { ...props }
  />
);

export const IconContainer = ({
  presenter,
  className = '',
  onClick,
  ...props,
}) => {
  if (onClick) className += ` ${ styles.clickable }`;
  return presenter({ onClick, className, ...props });
};
  • もう作るものは大体わかっている
    • Rails のパーシャル単位でコンポーネント作成
    • CSS はコピーしてくる
    • Container Component に Rails の helper に書き出したロジックを書くようにする

いい感じ :laughing:


でも、よくわからないこともある :sweat_smile:


midleware がどんどん大きくなる :pizza:

action.js
import api from '../api';
import { reset } from 'redux-form';
import { Presence } from 'phoenix';

const syncPresentUsers = (dispatch, presences) => {
  const presentUsers = [];
  Presence.list(presences, (id, { metas: [first] }) => first.user)
          .map(user => presentUsers.push(user));
  dispatch({ type: 'ROOM_PRESENCE_UPDATE', presentUsers });
};

export function connectToChannel(socket, roomId) {
  return (dispatch) => {
    if (!socket) { return false; }
    const channel = socket.channel(`rooms:${roomId}`);
    let presences = {};

    channel.on('presence_state', (state) => {
      presences = Presence.syncState(presences, state);
      syncPresentUsers(dispatch, presences);
    });

    channel.on('presence_diff', (diff) => {
      presences = Presence.syncDiff(presences, diff);
      syncPresentUsers(dispatch, presences);
    });

    channel.on('message_created', (message) => {
      dispatch({ type: 'MESSAGE_CREATED', message });
    });

    channel.join()
      .receive('ok', (response) => {
        dispatch({ type: 'ROOM_CONNECTED_TO_CHANNEL', response, channel });
      })
      .receive("error", resp => { console.log("Unable to join", resp) })

    return false;
  };
}

export function leaveChannel(channel) {
  return (dispatch) => {
    if (channel) {
      channel.leave();
    }
    dispatch({ type: 'USER_LEFT_ROOM' });
  };
}

export function createMessage(channel, data) {
  return dispatch => new Promise((resolve, reject) => {
    channel.push('new_message', data)
      .receive('ok', () => resolve(
        dispatch(reset('newMessage'))
      ))
      .receive('error', () => reject());
  });
}

export function loadOlderMessages(roomId, params) {
  return (dispatch) => {
    dispatch({ type: 'FETCH_MESSAGES_REQUEST' });
    return api.fetch(`/rooms/${roomId}/messages`, params)
      .then((response) => {
        dispatch({ type: 'FETCH_MESSAGES_SUCCESS', response });
      })
      .catch(() => {
        dispatch({ type: 'FETCH_MESSAGES_FAILURE' });
      });
  };
}
  • 非同期の通信が多いから、midleware の部分がどんどん太っていく

テストってどこまで書くの :green_heart:

  • まだ、テストぜんぜん書いてません。。。笑
  • React はテストライブラリーがたくさんある
    • ストラクチュラル・リグレッション・テスト
    • ビジュアル・リグレッション・テスト
    • インタラクション・テスト
    • レイアウト・テスト
    • パフォーマンス・テスト
    • クロスプラットフォーム・テスト
    • アクセシビリティ・テスト
    • etc

テスト好きだけど、こんなに書くのはしんどい。。。


コンポーネント管理 :wrench:

  • 実装中に「これ molecules だなー」っていうコンポーネントもある
  • 今はまだ僕だけので、Storybook で管理

どんどん、Sketch と Storybook が乖離していく。。。


それでも、やっぱりいい感じ :laughing:

  • 非同期でヌルヌル動く体験はユーザーにわかりやすいと思う
  • React Native もあるので、ネイティブアプリ化も捗る(もともと、それが目的)
  • 懇親会でいろいろ教えてくださいー!笑

おわり :beer:

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.