Minstrel

Ruby, JavaScript, Haskell, Math, Music, Design

【初心者向け】ajaxハンズオン with Rails5.1/ jQuery3.0

初心者向け

プログラミング学びたての方から、ajaxがよくわからないという相談を Web上を見るとajaxの詳しい解説はあっても、ハンズオン形式の記事は少ないと感じました。 ということで、細かい説明は抜きにしてとりあえず動くことを重視した記事を作成しました。

対象

ターゲット

  • Ruby on Rails学習中のプログラミング初学者(累計学習時間50h ~ 100h)
  • jQueryは少しだけ触ったことがある。

環境

  • Ruby on Rails(5.1系)
  • MySQL5.6 ※4系向けにも少し細くします。

作成するもの

最小限のTodo管理アプリを作成します。 細かいことは説明しない代わりに、はじめての方でも作りきれるように 過程を細かく載せていきます。

キーワード

gif

Image from Gyazo

完成系のコード(Ajaxの部分)

$(document).ready(function() {
  $("#submit").on('click', function(e) {
    e.preventDefault();
    var text = $('#task_title').val()

    $.ajax({
      type: 'POST',
      url: '/tasks.json',
      data: {
        task: {
          title: text,
        }
      }
    }).done(function(data){
      $('#task-list').append(
        `<tr>
          <td>
            ${data.title}
          </td>
        </tr>`
      )
    })

    $('#task_title').val('')
  })
})

1. アプリを作成する(rails new)

作成

rails new!

まずはアプリの型、作成からいきましょう。

rails newをしていきます。

rails new todo -d mysql

続いてデバッグ用に 'pry-rails' というgemをGemfileにいれておきましょう。

# Gemfile
gem 'pry-rails'
bundle install

scaffoldで簡単にアプリを作成しよう!

Railsにはscaffoldという、アプリケーションの枠組みだけをコマンド一発で作成してくれる機能があります。 今回はajaxの練習であるため、サーバーサイドの記述は極力簡略化するために、このscaffoldを使いましょう!

scaffoldについての詳細はこちら Rails scaffoldを初心者向けに解説!実際にアプリを…|Udemy メディア

以下のコマンドを順に実行しましょう!

rails g scaffold task title:string #todoモデルを作成(titleというカラムをもたせる)
rails db:create # データベース作成
rails db:migrate # マイグレーション実行

2. jQueryCDNでインストールする(Rails5向け)

CDNによるjQueryインストール

※Rails4には デフォルトでjQueryが入っているため、この作業は飛ばしてください。

まずは、jQueryの公式サイトにいきましょう! jQuery CDN

Image from Gyazo

jQuery 3.x のuncompressedを押すと表示されるリンクをコピーして、application.html.erbに貼り付けましょう! gyazo.com

3. jQueryの練習

JavaScriptファイルの作成

assets/javascripts 配下に tasks.js というファイルを作成しましょう! ※tasks.coffeeというファイルが自動生成されている場合は tasks.coffeeを削除してください。

$(document).ready(function() {})

tasks.jsに以下のように記述しましょう。

>||js||

$(document).ready(function() {

console.log("Hello World");

})

|

GoogleChromeの検証機能などで、確認しましょう!

Image from Gyazo

$(document).ready(function() {})

この記述については詳細は割愛しますが。 "DOM要素(HTML)を全てよみこんでから、JavaScript実行する"ための記述です。

4. Ajaxについて

HTMLを書き換える

index.html.erbを以下のように書き換えましょう
<p id="notice"><%= notice %></p>

<h1 id="title">Tasks</h1>

<!-- scaffoldで自動生成したformを呼び出しましょう -->
<%= render 'form', task: @task %>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th colspan="3"></th>
    </tr>
  </thead>
   
  <!-- jQueryでDOMを取得できるようidを付与しましょう -->
  <tbody id="task-list">
    <% @tasks.each do |task| %>
      <tr>
        <td><%= task.title %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>
_form.html.erbを以下のように書き換えましょう
<%= form_with(model: task, local: true) do |form| %>
  <% if task.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(task.errors.count, "error") %> prohibited this task from being saved:</h2>

      <ul>
      <% task.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :title %>
     <!-- idを付与しましょう -->
    <%= form.text_field :title, id: :task_title %>
  </div>

  <div class="actions">
    <!-- idを付与しましょう -->
    <%= form.submit "Submit", id: "submit" %>
  </div>
<% end %>
tasks_controllerを以下のように編集しましょう
class TasksController < ApplicationController
  def index
    @tasks = Task.all
    @task  = Task.new # _form.html.erbで使用する変数を定義
  end
end

onでイベント発火

JavaScript/jQueryでは〇〇を押したら~、◯◯の上にカーソルをのせたら、のようなタイミングをとることができます。 それをイベントの発火と呼びます。 「submitボタンを押したら、~~する 」を表すコードを on を使用して書いてみましょう。

tasks.jsを以下のように編集しましょう。
$(document).ready(function() {
  $("#submit").on('click', function(e) { //①
    e.preventDefault(); //②
    var text = $('#task_title').val() //③
    console.log(text)
  })
})

①'submit'というidがついたDOMがクリックされたときに、第二引数にあたえられた関数(function)を実行します ②e.preventDefaultはsubmitボタンがDefaultでもっているページ遷移しようとする機能をキャンセルします ③'task_title'というidがついたDOM(今回の場合はinput)のvalueを取得します

ajaxでリクエスト

ajaxでtasks_controller#createアクションにリクエストを送っていきましょう!

tasks.jsを以下のように編集します
$(document).ready(function() {
  $("#submit").on('click', function(e) {
    e.preventDefault();
    var text = $('#task_title').val()

    $.ajax({
      type: 'POST', // ①HTTPメソッドを指定します。
      url: '/tasks',    // ②パスを指定します
      data: {            // ③HTTPリクエストのボディを指定します。Railsではここで指定した値がparamsに入ります。
        task: {
          title: text,
        }
      },
      dataType: 'json' // ④HTTPリクエストで要求するdataのタイプを指定します。
    }).done(function(data){ // ⑤リクエストに成功した場合 .done以下の関数が実行されます。
      console.log(data)
    })
  })
})

④ dataType: 'json'

dataType: 'json' は 「リクエストでhtmlではなくjsonを返してね」という意味になります。 これはtasks_controller#createの以下と対応しています。

def create
    @task = Task.new(task_params)

    respond_to do |format|
      if @task.save
        format.html { redirect_to @task, notice: 'Task was successfully created.' }
        format.json { render :show, status: :created, location: @task } # ここ!
      else
        format.html { render :new }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end

format.json というのが、jsonを求めるリクエストが来た場合です。 今回の場合はhtmlではなくjsonという形式でレスポンスを返す記述となっています。 jsonを生成するためにjbuilder(show.json.jbuilder)というものが使われていますが、今回はjbuillderに関する記述は割愛します。 jbuilderはjsonを整形して生成するために必要なもの、ぐらいに抑えておいてください。

⑤ done

リクエストが成功した場合にdone以下に書かれた関数が実行されます またその際、引数にはレスポンスが入ってきます。このレスポンスを活用してこのあとHTMLを書き換えていきます。

$.ajax({
  省略
}).done( function(data) {
  console.log(data) // <- dataにレスポンスが入っている
})

HTMLを書き換える

最後にajaxで取得してきたレスポンスデータを使って TodoListを追加していけるようにHTMLを書き換えましょう!

$(document).ready(function() {
  $("#submit").on('click', function(e) {
    e.preventDefault();
    var text = $('#task_title').val()

    $.ajax({
      type: 'POST',
      url: '/tasks',
      data: {
        task: {
          title: text,
        }
      },
      dataType: 'json'
    }).done(function(data){
      $('#task-list').append( 
        `<tr>
          <td>
            ${data.title}
          </td>
        </tr>`
      ) // task_listに新しいtodoデータをつかった<td>を追加
    })

    $('#task_title').val('') // formの値をリセット
  })
})

こんな風にうごいたら完成です! Image from Gyazo

Reference

以下の記事を目に通すとよりajaxやjQueryへの理解が進みます。 とはいえ今はES6 + React/Vueなどのフレームワークの時代です。 かんたんなjQueryでDOM操作になれたあとは しっかりとJavaScriptの基礎を学び → ES6を学び → React/Vueなどのフレームワークに挑戦 もしていけるといいですね! もちろんjQueryも正しく使えばまだまだ使っていける技術だと思っています!

Ajaxについて https://qiita.com/zakiyamaaaaa/items/bdda422db2ccbaea60d9 jQueryによるDOM操作 https://qiita.com/nekoneko-wanwan/items/227ccad5f8cc449e91e9

Ruby2.5.1のObservableモジュールを写経した

場所

~/.rbenv/versions/2.5.1/lib/ruby/2.5.0/observer.rb

大きな流れ

①Subject(変更する側)からadd_observerを呼び出してConcreateObserver(変更通知を受け取る側)のObjectとインターフェイス(デフォルトは:update)を登録する ②changed(true)を呼び出し変更フラグを立てる。これがtrueでなければnotify_observersは動かない(return falseされる) ③notify_observersを呼び出し、Observerの:updateメソッドが呼ばれる

呼び出し側のコード

require 'observer'


class Employee
  include Observable

  attr_reader :name, :title, :salary

  def initialize(name, title, salary)
    @name   = name
    @title  = title
    @salary = salary
    #①add_observerメソッド →  Employeeの変更を通知したいObserverObjectを注入する
    add_observer(Payroll.new) 
    add_observer(TaxMan.new)
  end

  def salary=(new_salary)
    @salary = new_salary
    #changed(state=true)を書くと、notify_observersが呼ばれる。
    # つまり明示的に通知するかどうかを示せる
    changed(true) # ②observer_stateをtrueに変更する(これがfalseだとnotify_observerを呼んでも通知されない)
    notify_observers(self) # ③登録したObserverObjectに通知をする
  end
end

class Payroll
  def update(changed_employee)
    puts "給料あがった#{changed_employee.salary}"
  end
end

class TaxMan
  def update(changed_employee)
    puts "#{changed_employee.name} 請求書"
  end
end

john = Employee.new('John', 'Senior Vice President', 5000)
john.salary = 6000

写経したObservableのコード

module Observable
  def add_observer(observer, func=:update)
    @observer_peers = {} unless defined? @observer_peers
    unless observer.respond_to? func
      raise NoMethodError, "observer does not respond to `#{func}'"
    end
    @observer_peers[observer] = func
    #①{ ObserverObject, :update } を配列にセットする
  end

  #.... 省略(delete_observer, count_observersなど補助的メソッド)

  def changed(state=true) #②変更がなされたかどうかのステータス
    @observer_state = state
  end

  def changed?
    if defined? @observer_state and @observer_state
      true
    else
      false
    end
  end

  def notify_observers(*arg) #③通知
    if defined? @observer_state and @observer_state
      if defined? @observer_peers
        @observer_peers.each do |k, v|
          k.send v, *arg
        end
      end
      @observer_state = false
    end
  end
end

感想

実装がとてもシンプル。初中級社のソースコードリーディングの練習に良いかも。 2.5.1のdefined便利。(nilガードとかではなく、変数やメソッドが定義されるかどうかを判別できる)

30秒でつくれるAPI Server

参照記事

公式 たった30秒でREST APIのモックが作れる JSON Serverでフロントエンド開発が捗るserver/

必要なもの

Node.js npm

手順

インストール

ターミナルにて

$ npm install -g json-server

リソースの作成

data.jsonというファイルを作成

{
  "products": [
     { "id": 1, "title": "少林サッカー", "price": "1200" },
     { "id": 2, "title": "北野国柄", "price": "1500" }
  ]
}

起動

ターミナルより

$ json-server --watch db.json

あとはなんでも好きにござれ

curl

curl -X GET "http://localhost:3000/products"
curl -X GET "http://localhost:3000/products/1"
curl -X POST -H "Content-Type:application/json" -d '{
  "id": 3,
  "title": "ららららららら",
  "price": "1000"
}'

VueとかReactのajax試す時に便利だよ

便利だよ

ほかにも

sortとかoffsetぽいこととかデータ大量投入とかも簡単にできる模様 公式はこちら: https://www.npmjs.com/package/json-server

# ActiveRecordアンチパターン写経

参考記事

https://speakerdeck.com/toshimaru/active-record-anti-patterns

事例1 全Userの中から2017年以降の登録Userへ100ptを付与する。

アンチパターン① 全件取得&ループ

User.all.each do |user|
  if user.created_at >= Date.new(2017)
    user.point += 100
    user.save
  end
end

Model.allで全レコードを取得し、それにeachをかける。 →全件取得でメモリ逼迫 →ループ回数が増えCPUリソース消費

Better Way

  1. User取得件数をフィルターして減らす all → where

  2. 少しずつUserを取得してメモリフレンドリーに each → find_each

User.where('created_at >= ?', Date.new(2017)).find_each do |user|
  user.point += 100
  user.save
end

アンチパターン② N+1 Update

# 1select
User.where('created_at >= ?', Date.new(2017)).find_each do |user|
  user.point += 100
  user.save # N update
end

1回select + n回updateが走る Nの数が多くなればなるほどパフォーマンス悪化

Better Way

  1. 複数レコード一括更新

update -> update_all

User.where('created_at >= ?', 
  Date.new(2017)).update_all('point = point + 100')
end

実行速度爆速化!!!

※update と update_allは等価でない(callbackとか) ※テーブルロックに注意 →適切なトランザクションの設定・ロックの設定を!

事例2 ユーザー毎の記事のいいね数 合計が多い順にTOP100を出す!

アンチパターンRuby Aggregation Pattern

user_like_counts = []
User.all.each do |user|
  user_like_counts << {
    name: user.name,
    total_like_count: user.posts.sum(&:like_count)
  }
end

user_like_counts
  .sort_by! { |u| u[:total_lik_count] }
  .reverse!
  .take(100).each do |u|
    puts "#{u[:name]} #{u[:total_like_count]}"
  end

Better Way

Post.group(:user_id)
  .select("user_id SUM(like_count) AS like_count")
  .order(like_count DESC)
  .limit(100).each do |post|
    puts "#{u[:name]} #{u[:total_like_count]}"
   end
end

アンチパターン④ N+1あるでえええええ

Post.group(:user_id)
  .select("user_id SUM(like_count) AS like_count")
  .order(like_count DESC)
  .limit(100).each do |post|
    # ここやで!↓
    puts "#{post.user.name} #{post.like_count]}"
   end
end

Better Way

includesやで! joins, includes, preload, eager_loadで使い分けれるとよいね コードは省略

Rubyでデザインパターン① デザインパターン概要

参考記事

事例で学ぶデザインパターン 第1回 | オブジェクトの広場

デザインパターンとは

クラスの責務分割のパターン

GoFデザインパターン

生成

Abstract Factory Factory Method Singleton Builder Prototype

構造

Adaptor Bridge Composite Decorator Facade Flyweight Proxy

振る舞い

Command Interpreter Iterator Mediator Memento Observer State Strategy Template Method Visitor

デザインパターンの効果

1. 変更容易性

2. 理解容易性

前提知識

継承

is-aの関係

コンポジション

has-aの関係 moduleとかオブジェクトの注入とか

ポリモーフィズム

問題解決の着眼点

クラスの責務分割はおおまかに3パターン

1. 変更要素の分離

アプリケーション内で固定化している部分と変動している部分をクラスとして分離する。 例1) Stateパターン: 「状態」を変動要素として切り出す 例2) Strategyパターン: 「アルゴリズム」を変動要素として切り出す

2. 間接層の道入

目的は、クラスとクラスの結合度を弱めることで 一方のクラスの変更が他方のクラスに影響を与えにくいようにすること。 例1) Proxyパターン 例2) Adaptorパターン 本来使用したい機能をもっているクラスと、それを使用するクラスの直接結合を避け、結合度を下げている。

3. 複雑さの軽減

分割をすることで、一つのクラスの責務を小さくする。 →理解容易性、変更容易性があがる

Ruby: 素数判定機

def is_prime?(num)
  raise "Invalid input" if num < 1 
  return false if num < 2
  i = 2
  while i < num do
    return false if num % i == 0
    i += 1
  end
  return true
end

素数判定メソッドつくった。(nは自然数)

Ruby: 約数を求めるプログラム

# 約数を求めるプログラム書いた by Ruby "数学ガール"という書籍を読んでいたら、 自然数nの約数の総和を表す一般式を書け、という問題が出てきたので 数学のニガテな私はRubyちゃんでやってみた。

def factor(num)
  raise "Invalid Input. Num must be over 1" if num < 1
  result = []
  i = 2
  while i <= num do
    if num % i == 0
      result << i
      num = num / i
    else
      i += 1
    end
  end
  return result
end

def sum(num_ary)
  num_ary.reduce(:+)
end


p factor 10
# => [2, 5]
p factor 100
# => [2, 2, 5, 5]
p factor 1000
# => [2, 2, 2, 5, 5, 5]
p factor 48
# => [2, 2, 2, 2, 3]
p factor 570
# => [2, 3, 5, 19]

ネストが深いのと、変数を多用していてプロセスを追いづらいのが微妙ではある。 次回はHaskell再帰をつかってもっと美しく読みやすく描いてみたい。