TECH EXPERT 30日目

TECH::EXPERT

カリキュラム1日ビハインドのままだが気にしない。
非同期通信関係でjqueryやajaxなど横断的な内容が多い。
1日振り返って考えると大した分量ではないのだが、脳みそに定着させるには時間がかかる。

ここ数日の吸収量が多いので今日はちょっとカリキュラムは止めて、まとめ作業へ。
普段はつくばの自宅に戻った時にやるんだけれど、あと1週間は戻る予定がないのでここらで纏めないとオーバーフローしちゃう。

クエリ文字列(URLパラメーター)

URLの後ろにつける ?hoge=fugafufga&hoge2=fuagfua のこと。
アクセス解析に使うようなパッシブのものと、
表示内容に影響を与えるアクティブなものがある。

rails

AddIndexToTable

  def change
    add_index :products,  :title
  end

test code

changeマッチャ

引数が変化したかどうかを確かめる。
change(Message, :count).by(1)と記述し、Messageモデルのレコードの総数が1個増えたかどうかを確かめる

.to .not_to

以下と同じことを示す場合は.toを。
違うことを示す場合には.not_toを。

it 'does not count up' do
  expect{ subject }.not_to change(Message, :count)
end

メモ

冗長ですがcontrollerのテストの雛形を作成するためにベタ張り。
後ほど切り出して分解します。

require 'rails_helper'

describe MessagesController do
  let(:group) { create(:group) }
  let(:user) { create(:user) }

  describe '#index' do

    context 'log in' do
      before do
        login user
        get :index, params: { group_id: group.id }
      end

      it 'assigns @message' do
        expect(assigns(:message)).to be_a_new(Message)
      end

      it 'assigns @group' do
        expect(assigns(:group)).to eq group
      end

      it 'redners index' do
        expect(response).to render_template :index
      end
    end

    context 'not log in' do
      before do
        get :index, params: { group_id: group.id }
      end

      it 'redirects to new_user_session_path' do
        expect(response).to redirect_to(new_user_session_path)
      end
    end
  end

  describe '#create' do
    let(:params) { { group_id: group.id, user_id: user.id, message: attributes_for(:message) } }

    context 'log in' do
      before do
        login user
      end

      context 'can save' do
        subject {
          post :create,
          params: params
        }

        it 'count up message' do
          expect{ subject }.to change(Message, :count).by(1)
        end

        it 'redners index' do
          subject
          expect(response).to redirect_to(group_messages_path(group))
        end
      end

      context 'can not save' do
        let(:invalid_params) { { group_id: group.id, user_id: user.id, message: attributes_for(:message, body: nil, image: nil) } }

        subject {
          post :create,
          params: invalid_params
        }

        it 'does not count up' do
          expect{ subject }.not_to change(Message, :count)
        end

        it 'renders index' do
          subject
          expect(response).to render_template :index
        end
      end
    end

    context 'not log in' do
      before do
        get :index, params: { group_id: group.id }
      end

      it 'redirects to new_user_session_path' do
        post :create, params: params
        expect(response).to redirect_to(new_user_session_path)
      end
    end
  end
end

FactoryBot

attributes_for

FactoryBotによって定義されるメソッドで、オブジェクトを生成せずにハッシュを生成する

let(:invalid_params) { { group_id: group.id, user_id: user.id, message: attributes_for(:message, body: nil, image: nil) } }

統合テスト

フィーチャスペック

Rspecを使って統合テストを行うためのスペック。
アプリケーションの肝となる動作を総合的にテストしたい場合に用いる。

一部の記述の名前が単体テストと異なる

フィーチャスペックでは、
itの代わりにscenarioを、
beforebackground
describefeature
letgivenと記述する慣習がある。

これらは全てエイリアスメソッドなので、名前が異なるだけで内部的な実装は全く同じ。
フィーチャスペックでもit・before・describe・letを使える。
どっちで書くかは現場次第。

同一テスト内に複数のexpectを記述。

have_content / have_no_contentマッチャ

引数に指定したバリューを持つHTML要素がそのページに存在する/しないことを確かめるためのマッチャ

Capybara

gem。
ブラウザの操作を再現するのに必要。
特定の要素をクリックしたり、フォームに値を入力したり、特定の要素が画面に表示されているかなど、様々なブラウザ上の動きをテストできる。
Capybaraを導入することで、テストを記述するためのヘルパーメソッドが複数追加される。

visitメソッド

引数にURL、 もしくはプレフィックスを指定し、そのページに移動する。

visit("/hogehoge")
visit hogehoge_path

click_onメソッド

指定したHTML要素をクリックする。
引数にはHTML要素のvalue属性を指定する。

find('input[type="submit"]').click

fill_inメソッド

値を入力する。
入力したいvalueを指定し、with以下に入力する値を入れる。

fill_in 'user_email', with: user.email
fill_in 'text', with: 'フィーチャスペックのテスト'

capybaraの使い方

gemファイルに以下を記述してbundle install。

group :test, :development do 
  gem 'capybara'
end

spec/rails_helper.rbに以下を追記。

require 'capybara/rspec'

featureSpec用のディレクトリを準備し、以下を作成。

spec/features/tweet_spec.rb

require 'rails_helper'
feature 'tweet', type: :feature do
# このブロックの内部にscenarioを記述していく
end

APIとは

必要なデータだけをJSONなどの形式で返すサーバの仕組みのことをAPI、もしくはJSON APIと呼ぶ。

APIの作り方(rails)

  • HTMLを返すためのコントローラーを共用してAPIとして使えるようにする
  • API専用のコントローラーを作成する
  • APIを作成するためのgemを利用する

コントローラーでの振り分け

Railsには、コントローラーのアクションの中でHTMLとJSONといったフォーマット毎に条件分岐できる仕組みがある。

respond_to

コントローラーで利用できる。
JSONを返す場合はRubyのハッシュの状態のままrenderメソッドに渡すだけでJSONに変換してくれる。インスタンス単体ならそのまま渡してもOK。

respond_to do |format|
  format.html { render ... } # この中はHTMLリクエストの場合に呼ばれる
  format.json { render ... } # この中はJSONリクエストの場合に呼ばれる
end
respond_to do |format|
  format.json { 
    render json: { id: @user.id, name: @user.name }
  }
end
def create
  @todo = Todo.new(todo_params)
   if @todo.save
    respond_to do |format|
      format.html { redirect_to :root }
      format.json { render json: @todo}
    end
  else
    render :index
  end
end

jbuilder

rails newした際にgemfileにデフォルトで記述されているgem。
入力データをJSON形式で出力するテンプレートエンジン。

jbuilderを使用してJavaScriptファイルに返すデータを作成

viewを同じように該当するアクションと同じ名前にする必要がある。
viewと同じように該当コントローラの該当アクション名で.json.jbuilderファイルを作成する。


hogehoges_controllerの#createアクションに対応するjbuilderファイルを
view/hogehoges/create.json.builderに作成する。

json.jbuilderファイル内の記法

基本的にjson.KEY VALUEと記述する。

json.text  @comment.text
json.user_id  @comment.user.id
json.user_name  @comment.user.nickname

ちなみにcontroller側の記述で
@comment = Comment.new(params) とせずに
comment = Comment.new(params)
@抜きにした場合、
json.jbuilderファイル内でjson.text comment.textのように記述しても出力できない。
createは通るけれど、viewに投げるには@が符号になっているためこのような処理は通らない。
viewファイルを経由せずにcontroller側でハッシュにしてしまえば通る。

これは通る
@comment = Comment.new()
略
format.json
@をつけない場合。コントローラ内でハッシュにして渡せば通る。
view経由させるなら@は必須。

def create
  comment = Comment.create(comment_params)
  hoge = {text: comment.text, user_id: comment.user_id }
  respond_to do |format|
    format.html { redirect_to tweet_path(params[:tweet_id])  }
    format.json { render json: hoge}
  end
end

json.array!

json形式のデータを配列にしたい場合に使う。

json.array! @products do |product|
  json.id product.id
  json.title product.title
  json.image product.image_url
  json.detail product.detail
end

Ajax

$.ajax()は以下の4つの要素を送信する。
この要素の記載に続けて、.doneと.failを記載して送信後の処理を行う。

typeHTTP通信の種類を記述する。通信方法は、GETとPOSTの2種類がある。
urlリクエストを送信する先のURLを記述する。
dataサーバに送信する値を記述する。
dataTypeサーバから返されるデータの型を指定する。
$(function() {
  function buildHTML(todo) {
    var html = $('<li class="todo">').append(todo.content);
    return html;
  }

  $('.js-form').on('submit', function(e) {
    e.preventDefault();
    var textField = $('.js-form__text-field');
    var todo = textField.val();
    $.ajax({
      type: 'POST',
      url: '/todos.json',
      data: {
        todo: {
          content: todo
        }
      },
      dataType: 'json'
    })
    .done(function(data) {
      var html = buildHTML(data);
      $('.todos').append(html);
      textField.val('');
    })
    .fail(function() {
      alert('error');
    });
  });
});

dataは基本的にハッシュで渡す。
formの場合はハッシュのネストで渡すと通常のフォームと同じ形式になるが、
FormDataオブジェクトで渡すのが普通。

data: { keyword: input },

$.ajax({})の後の.done(function(DATA){})のDATA部分はajaxでリクエストを投げた結果、返ってきた値が入る。
入れる値を指定しているのではなくて、返ってきた値をDATAと命名し、以降の関数内でDATAという変数名で用いる。
通常の引数を渡すという感覚ではなく、引数はすでに持っていてそれに名前をつける感覚。

$.ajax({})
.done(function(DATA){})

JavaScript

window.location.href

現在のURLを返す。

テンプレートリテラル記法

バックティック文字で囲むことで、複数行文字列や文字列内挿入機能を使用できる。

  function buildHTML(comment){
    var html = `<p>
                  <strong>
                    <a href=/users/${comment.user_id}>${comment.user_name}</a>
                    :
                  </strong>
                  ${comment.text}
                </p>`
    return html;
  }

較的新しい記述の仕方のため、アプリケーションをデプロイする際にエラーになる。
その場合、config/environments/production.rb
の下記の設定をコメントアウトする。

config.assets.js_compressor = :uglifier
↓
# config.assets.js_compressor = :uglifier

jQuery

gemfileに以下を追記してbundle install

# Use jquery as the JavaScript library
gem 'jquery-rails'

application.jsに以下の記述があるかを確認。

//= require jquery
//= require rails-ujs 

注意点として、require tree .より上にjqueryを書くこと。
require tree以下でjsファイルが読み込まれるため、下に書くとjqueryがロードされる前にjqueryの内容を含むjsファイルが読まれ、Uncaught ReferenceErrorを吐く。

FormData

フォームのデータの送信に使用できる。フォームの情報を取得するのに使う。
フォームとは独立して使用することもできます。

$(function(){
  $('#new_comment').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    console.log(formData)
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
  })
})

attrメソッド

要素が持つ指定属性の値を返す。
要素が指定属性を持っていない場合、関数はundefinedを返す。

processDataオプション

デフォルトはtrue。
dataに指定したオブジェクトをクエリ文字列に変換する役割がある。
FormDataを扱う場合にはfalseにする。

contentTypeオプション

サーバにデータのファイル形式を伝えるヘッダ。
デフォルトでは「text/xml」でコンテンツタイプをXMLとして返す。

ajaxのリクエストがFormDataのときはどちらの値も適切な状態で送ることが可能なため、falseにすることで設定が上書きされることを防ぐ。
FormDataで扱う場合にはfalseにする。

$(function(){
  function buildHTML(comment){
    var html = <div> hoge </div>
    return html;
  }
  $('#new_comment').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      var html = buildHTML(data);
      $('.comments').append(html)
      $('.textbox').val('')
    })
    .fail(function(){
      alert('error');
    })
  })
});

Uncaught ReferenceError: $ is not defined

jqueryを読み込む前にjqueryの記法を使っている場合に起こるエラー。
javaファイルの読み込み順の上位にjqueryを読み込ませると解決する。

emptyメソッド

指定したDOM要素の子要素のみを削除する。
removeメソッドは指定した要素を削除する。

forEachメソッド

配列に含まれる各要素に対して一度ずつ呼び出して処理をする。

products.forEach(function(product){
  appendProduct(product);
});

インクリメンタルサーチ

jQueryを有効にする。
jsファイルにonメソッドでアクションを起こし、(form系の処理ならe.priventDefault)
ajaxでurl, type, data, datatypeを投げる。
controllerでrespond_to do |format|で条件分岐。
json.jbuolderファイルが必要なら作成。
viewファイルを参考に返ったデータを再びjqueryで処理するための記述を追加。done
・表示内容を.empty()で初期化し、新たな内容を表示させる。append
・適合内容の有無を条件判定させる。array.length!==0 
エラー時の挙動を追加。fail

bundler

can’t find gem bundler (>= 0.a) with executable bundle (Gem::GemNotFoundException)

bundlerのバージョン違いで出るエラー。
bunler2系のエラーだそう。

Bundler: An update on the Bundler 2 release

gem.lockファイルに記載のbundled_withのバージョンを確かめるか、以下でバージョンを確認。

 cat Gemfile.lock | grep -A 1 "BUNDLED WITH"

あとはバージョンを指定してgemをインストールすればOK

 gem install bundler -v '2.0.1'

コメント