kame's engineer note

技術関連のメモをブログに記録していく

php メモリ使用量測定メモ

Rails系の記事ばかりだったが、久々にphpでメモリの使用量を計測する機会があったので、やり方をメモっておく。

phpに割り当てられた現在のメモリ使用量を測定

memory_get_usage() を使用する。
http://php.net/manual/ja/function.memory-get-usage.php

<?php

echo "first:".memory_get_usage() / (1024 * 1024)."MB\n";

$arr = [];
for($i=0;$i<10000;$i++) {
    $arr[] = $i;
}

echo "now:".memory_get_usage() / (1024 * 1024)."MB\n";

output

first:0.21486663818359MB
now:1.6373443603516MB

phpに割り当てられたメモリ使用量の最大値を取得

memory_get_peak_usage() を使用する。
http://php.net/manual/ja/function.memory-get-peak-usage.php

<?php

echo "first:".memory_get_usage() / (1024 * 1024)."MB\n";

$arr = [];
for($i=0;$i<10000;$i++) {
    $arr[] = $i;
}

//半分開放する
for($i=0;$i<5000;$i++) {
    unset($arr[$i]);
}

echo "now:".memory_get_usage() / (1024 * 1024)."MB\n";
echo "peak:".memory_get_peak_usage() / (1024 * 1024)."MB\n";

output

first:0.21579742431641MB
now:0.98969268798828MB
peak:1.6398239135742MB

rails 4でransackを使った検索機能を実装した

すごく簡単に検索機能を実装できるというransack
metasearchというGemの後継らしい。
ちょうど検索を実装する案件があったので、試しに使ってみた。

流れ

  • ransack導入
  • searchメソッド(DSL)の実装
  • ransackable_scopesで柔軟な検索実装

前提

  • Authorモデルの検索一覧ページとする
  • 例としてモデルの構成は下記のようにする
Author -has_many-> Post

authors_table
- name
- category

posts_table
- title
- author_id
- entry

ransack導入

Gem 'ransack'

gemからインストールする。

https://github.com/activerecord-hackery/ransack

searchメソッド(DSL)の実装

controller.rb

def hoge
  @q = Author.ransack params[:q]
  @authors = @q.result(distinct: true).includes(:posts)
end

パラメータをransackに投げてあげて、resultで結果を取得する。
今回は関連テーブルがあるので、includesで指定してあげる。

view.slim.html

= search_form_for(@q, url: hoge_path, method: :get) do |f|
  .div
    = f.label :name
    = f.search_field :name_eq
  .div
    = f.label :posts
    = f.search_field :posts_title_eq
  .div
    = f.label :category
    =f.collection_check_boxes :category_in, category_list, :id, :name, {checked:@q.categgory} do |b|
      - b.label {b.check_box + b.text}

基本的には項目名 + 条件でパラメータを渡してあげれば、その条件にそった検索を実行してくれる。
以下のQita記事に簡単に条件がまとめられているので参考にするとよい。
http://qiita.com/nysalor/items/9a95d91f2b97a08b96b0

単純に入力値と等しい項目を探したい場合は、nameeqと続けて記述し、関連テーブルのカラムに対して検索したい場合は、posts_title_eqのようカラムと条件に加え、関連テーブル名を先頭に記述する。
また、チェックボックス等で選択した複数の項目を検索したい場合は、inを使うと便利だ。
あとはフリーワード検索なんか↓のように描くことも可

name_or_category_or_posts_title_or_posts_entry_cont

他にも色々な条件がパラメータの書き方を変えるだけで検索可能となる。

ransackable_scopesで柔軟な検索実装

複雑な検索をしたい場合は、scope(あるいはmethod)を定義することで実現可能となる。

author.rb

# custom scope
scope :custom_scope, -> (boolean=true) {
  joins(:posts).where(active: boolean)
}

private

# for ransack scope
def self.ransackable_scopes(auth_object=nil)
  %i(custom_scope)
end

view.slim.html

= search_form_for(@q, url:hoge_path,method: :get) do |f|
  .div
    = f.label :custom_scope
    = f.search_field :custom_scope

model内にscopeの実装を記述し、そのscope名をransackable_scopesにシンボルとして定義してあげる。
あとは定義したscope名をinputメソッドのオプションに記述するだけでよい。

ひとこと

そこまで複雑な検索機能を実装する必要性がないのであれば、自分でゴリゴリかくよりもいいかもしれない。 というか、結構複雑な検索でもscopeとか使えば実装できるし、特に縛りもないのでコレ一本でやっていけるかもですね。

参考URL

https://github.com/activerecord-hackery/ransack
http://qiita.com/ruzia/items/f6003547ca18377a1508
http://qiita.com/nysalor/items/9a95d91f2b97a08b96b0

Rails ajaxでhogehoge.js.slim(erb)を使わずにHTMLを書き換える

概要

前回の記事では、ajaxを実現するためのファイル構成は以下のようになっていた。

- controllers
  - photo_controller.rb
- views
  - photo
    - new.html.slim
    - new.js.slim
    - _add_photo_form.html.slim

例えばviews内にjsファイルを配置するのではなく、なるべくassets内のjsファイルにまとめて記述したほうがシンプルで良いという場合があると思う。 今回は、このajaxアクションのために必要なnews.js.slimをなくしてajaxを行う方法を書いてみる。

今回のファイル構成は以下のようになる。

- controllers
  - photo_controller.rb
- views
  - photo
    - new.html.slim
    - \_add_photo_form.html.slim
- assets
  - javascript
    - photo.js.coffee

viewファイル

viewファイルは前回とほとんど同じ。

new.html.slim

#選択したファイルのプレビューを表示
javascript:
  function readUploadImage(input) {
    if (input.files && input.files[0]) {
      var reader = new FileReader();
      reader.onload = function (e) {
        $(input).next()
          .attr('src', e.target.result)
          .width(100);
      };
      reader.readAsDataURL(input.files[0]);
    }
  }

div
  = form_for(@user, url: form_path, html: {multipart: true}) do |f|
    .div
      =link_to 'ボタンを追加する', new_path(@user.id),remote: true,data: {role: 'add_input'}
    .div data-role="photo_area"
      - @user.photos.each do |p|
        .div
          = image_tag p.image.url(:thumb)
          = check_box 'delete',"#{p.id}"
          = label_tag "delete[#{p.id}]", '削除'
      = render partial: 'photo_upload'
    .div
      = f.submit "登録する"

_add_photo_form.html.slim

.div
  = fields_for "user[photos_attributes][#{Time.now.to_i}]" do |ff|
    = ff.file_field :image, onchange: "readUploadImage(this)"
    = image_tag ''

render json(or text) でhtmlコンテンツを返す

photo_controller.rb

def new
  if request.xhr?
    content = render_to_string(:partial => 'add_photo_form')
    render json: {html: content}, status: :ok
  end
end

ajax通信があった場合、まずはrender_to_stringで読み込みたいviewファイルをstring化する。 それをjson形式で返してあげる。(textでも可)

photo.js.coffee

$(window).on 'load' ,->
  $('[data-role="add_input"]').bind 'ajax:beforeSend', () ->
    console.log 'start'
  .bind 'ajax:complete',(e,data,status,xhr) ->
    if data.responseJSON?
      $('[data-role="photo_area"]').append data.responseJSON.html

javascriptajaxのcallbackを使って、上記のcontroller側から返したjsonデータを取得し、指定の箇所にappendする。

以上で終わり。

Railsでhas_manyの複数の画像をajaxを使って動的に一括登録する

概要

表題の通りのことをしたい。
ユーザーモデルと写真モデルは下記の様なhas_manyの関係とする。

ユーザー(user) ==has_many=⇛ 写真(photo)

簡単にやることをまとめると

  • field_forを使用して、複数の子モデルを同時に保存するフォームを作成する。
  • 削除用のcheckboxを実装する。
  • 追加ボタンを押したら動的にfile inputを増やす。

なお、画像登録はpaperclipを使用する。

formの作成

photo_controller.rb

def new
  @user = User.find(params[:id])
  if request.xhr?
    respond_to do |format|
      format.js
    end
  end
end

new.html.slim

#選択したファイルのプレビューを表示
javascript:
  function readUploadImage(input) {
    if (input.files && input.files[0]) {
      var reader = new FileReader();
      reader.onload = function (e) {
        $(input).next()
          .attr('src', e.target.result)
          .width(100);
      };
      reader.readAsDataURL(input.files[0]);
    }
  }

div
  = form_for(@user, url: form_path, html: {multipart: true}) do |f|
    .div
      =link_to 'ボタンを追加する', new_path(@user.id),remote: true
    .div data-role="photo_area"
      - @user.photos.each do |p|
        .div
          = image_tag p.image.url(:thumb)
          = check_box 'delete',"#{p.id}"
          = label_tag "delete[#{p.id}]", '削除'
      = render partial: 'photo_upload'
    .div
      = f.submit "登録する"


それぞれ説明をしていくと

=link_to 'ボタンを追加する', new_path(@user.id),remote: true

file inputをajaxで追加するリンクを実装する。
railslink_toremote: trueを設定するだけで、ajax通信が可能になる。


- @user.photos.each do |p|
  .div
    = image_tag p.image.url(:thumb)
    = check_box 'delete',"#{p.id}"
    = label_tag "delete[#{p.id}]", '削除'

更新時に既に写真が登録されていれば、その写真を表示し、同時に削除ボタンも実装する。


= render partial: 'add_photo_form'

_add_photo_form.html.slim

.div
  = fields_for "user[photos_attributes][#{Time.now.to_i}]" do |ff|
    = ff.file_field :image, onchange: "readUploadImage(this)"
    = image_tag ''

fields_forを使用し、小モデルのinputを実装する。
なお、親モデルuser.rb内でaccepts_nested_attributes_forで子モデルを指定することを忘れずに。

accepts_nested_attributes_for :photos, allow_destroy: true

image_tag ''new.html.slimの一番上に記述されたjavascriptでプレビュー画像を表示するためのもの。
プレビュー画像はhtml5から実装されたFileRenderを使い、fileオブジェクトが保有するバッファの中身を読み込み、 それを上記のimage_tagsrcに指定することで画像を表示させる。

f:id:ryota12609:20150410164947p:plain


ajaxでHTMLを追加する

=link_to 'ボタンを追加する', new_path(@car.id),remote: true

link_toから、ajax送信でnew actionを実行する。

photo_controller.rb

if request.xhr?
  respond_to do |format|
    format.js
  end
end

ajax送信があった場合は上記の通り、フォーマットはjsファイルでテンプレートを実行する。

new.js.slim

| $('[data-role="photo_area"]').append("#{escape_javascript(render partial: 'add_photo_form')}");

jsではappend()でviewファイルadd_photo_form.html.slimセレクタで指定した箇所に追加する。

以上で下記のように動的にinputボタンを追加できるようになる。

保存する

photo_controller.rb

  def update
    user = User.find(params[:id])
    begin
      User.transaction do
        User.destroy(params[:delete]
          .select{ |k,v| v.to_i > 0}.keys) if params[:delete].present?
        user.update user_params if params[:user].present?
      end
      redirect_to redirect_path
    rescue => e
      p e.message
    end
  end



以上で、↓のように動的に写真を複数枚選択して、一括登録ができるようになる。

f:id:ryota12609:20150410165010g:plain

railsで部分にajaxを使う selectbox編

概要

selectboxの内容をajaxで動的に変更したいとき用のメモ。

AとBの2つのselectboxがあるとして、Aのselectboxで値を選択したら、それに連動した値の一覧をBのselectboxに反映するといった動作だ。

例としてAを都道府県、Bを市区町村とする。
これらはhas_manyの関係とする。

都道府県(prefecture) ==has_many=⇛ 市区町村(city)

簡単に流れをまとめると
1. jquery都道府県のselectboxが変更されたことを探知して、$.getメソッドajaxのactionを選択された都道府県idのparameterを渡して呼び出す。
2. ajax actionで都道府県idを基に市区町村をデータを取得する。
3. javascriptで上記で取得した市区町村データをselectboxに反映する。

formを作成

controller.rb

def new
  @form  = Form.new
  @cities = City.none
end

new.html.slim

javascript:
  $(window).load(function(){
    var prefecture_select = $('[data-role="prefecture_select"]');
    var url = "#{ajax_path}";
    prefecture_select.change(function(){
      $.get(url, {prefecture_id: prefecture_select.has('option:selected').val()});
    });
  });

div
  = form_for(@form, url:form_path) do |f|
    .div
      p= label :prefecture
      p= collection_select :prefecture,:prefecture_id, Prefecture.all, :id, :name,{selected: @prefecture_id.presence || 0,prompt: true},{'data-role'=>'prefecture_select'}
    .div
      p= f.label :city_id
      p= f.select :city_id, @cities,{prompt: true},{'data-role'=>'city_select'}
    .div
      = f.submit "登録"

今回はdata属性からDOM要素を取得するようにしている。


ajaxでselectboxの内容を変更

new.html.slim

javascript:
  $(window).load(function(){
    var prefecture_select = $('[data-role="prefecture_select"]');
    var url = "#{ajax_path}";
    prefecture_select.change(function(){
      $.get(url, {prefecture_id: prefecture_select.has('option:selected').val()});
    });
  });

jsでは、都道府県のselectboxが変更されたら、次に記述するajaxのaction宛に$.getを実行する。
その際にparameterに市区町村のselectboxで選択された値を指定する。


controller.rb

def ajax
  @form  = Form.new
  @cities = City.where(prefecture_id: params[:prefecture_id])
end

contoller内にajax actionを記述。
ajax actionでは、受け取ったparameterを基にselectboxで使用する市区町村オブジェクトを取得する。


ajax.js.slim

| $('[data-role="city_select"]').html("#{escape_javascript(options_for_select(@cities.map{|c| [c.name,c.id]}))}");

html()でhtmlの書き換えを行う。
ajaxのactionで取得した市区町村データをoptions_for_select()都道府県のselectboxの値を更新する。
その際にselectboxで使えるようにobjectからmapを使って配列に変換してあげる。

また、action実行時にhtmlデータがない場合はjsが実行されるが、 下記のように明示的に記述しても良い。

respond_to do |format|
  format.js
end

以上でajaxを使って動的にselectboxを変更することができる。

sinatra+bower+gulpでWEBアプリのスケルトンを作る

概要

簡単なWEBアプリを作りたい時にsinatraをよく使っている。
素のままでも便利なのだが、開発効率をもっと向上させるために、色々とカスタマイズした状態のスケルトンを作ることにした。

具体的に言うと下記のようなことをしたい。

  • slim,sass,coffeeの自動コンパイル
  • jqueryやbootstrapなどのパッケージを管理
  • livereload的なファイルを保存したらブラウザを自動リロード

sinatraと合わせて、フロントエンド界では必須ツールでもあるbowerやgulpを使うことにする。

使用する技術と役割

sinatra

sinatraはshotgunを使ってサーバーを立ち上げる。
slimはsinatra側でコンパイルする。
データベースを使いたい場合があるので、sqlite3とactiverecordを入れておく。
foremanはshotgunの実行とgulpの実行の際に使用する。

bower

とりあずjqueryとbootstrapをbowerで管理する。

gulp

  • ビルドツール
  • http://gulpjs.com/
  • gruntも考えたけど、今はgulpの方がイケてる感??
  • tasks
    • sass&coffeeのcompile
    • gulp-bower
    • browsersync

complieしたり、bowerで管理しているパッケージを扱ったりする。
あとはファイルとブラウザの同期。

sinatraの導入

bundle init


Gemfile

source "https://rubygems.org"

gem 'sinatra'
gem 'slim'
gem "sqlite3"
gem 'activerecord'
gem 'sinatra-config-file'

group :test do
    gem 'shotgun'
    gem 'foreman'
    gem 'rspec'
    gem "rack-test"
end
bundle install --path vendor/bundle

Gemfileを生成し、sinatra等をインストールする。


次にsinatraで使用するディレクトリやファイルを作っていく。
まずは以下のような構造にする。

  • app.rb
    • 説明: ルーティングの実行(controller的な役割)
  • config.rb
    • 説明: アプリケーションの起動
  • public
    • css
    • js
      • 説明: 静的ファイルの置き場所
  • views
    • layout.slim
    • index.slim
      • 説明: viewファイル
  • models
    • init.rb
    • hoge.rb
      • 説明: modelファイル
  • spec
    • spec_helper
    • models
      • hoge_spec.rb
        • 説明: specファイル
  • config.yml
  • dev.db


上から説明していく。

app.rb

# encoding: utf-8

require 'sinatra/base'
#ymlファイルを扱う
require "sinatra/config_file"
#slimを使用
require 'slim'
#activerecordを使用
require 'active_record'

#activerecordで使用するdbを指定
ActiveRecord::Base.establish_connection(
    adapter: 'sqlite3',
    database: 'dev.db'
)

#models/init.rbの読み込み
require_relative 'models/init'

class App < Sinatra::Base
    register Sinatra::ConfigFile
    config_file 'config.yml'

    get '/' do
        slim :index
    end

end

slimはsinatraで扱う必要があるので、Gemで対応。
話は変わるが、scssとcoffeeも以下のような記述をapp.rbにすることでsinatraで使えるようになる。

get %r{^/(.*)\.css$} do
    scss :"scss/#{params[:captures].first}"
end
get %r{^/(.*)\.js$} do
    coffee :"coffee/#{params[:captures].first}"
end

だが、gulpでbowerのcss,js系のパッケージ管理をしたり、minifyしたりする想定なので、cssとjsはすべてgulpで管理することにした。


config.ru

require 'bundler'
Bundler.setup

root = ::File.dirname(__FILE__)
require ::File.join(root, 'app')

run App

sinatraの起動のために必要なファイル。


views/layout.slim

doctype html
html
  head
    meta charset='utf-8'
    title sample app
    script src="js/program.js"
    link href="css/style.css" rel="stylesheet"
  body
    .wrap.container-fluid
      .main.row
      == yield

views/index.slim

top page

slimで書く。
ファイル名をlayoutにすれば勝手にlayoutファイルと認識してくれる。
yieldで個別のviewを読み出す。


models/init.rb

Dir[File.dirname(__FILE__) + '/*.rb'].each do |file|
  next if file == 'init'
  require_relative file
end

app.rbmodels/init.rbを読み込み、そこからmodels配下にあるrbファイルを全て読み込む形にした。


簡単ではあるが、sinatraの構築は以上で終了。

bowerの導入

npm install -g bower

パッケージ管理を行うbowerをインストールする。
node.jsが入ってない場合はインストールする。


bower init

bowerの初期化を行う。
色々と質問されるのでテキトウに答えていくと、bower.jsonが生成される


bower install --save jquery bootstrap

jqueryとbootstrapをインストールする。


bower.json

{
  "name": "sinatra-skelton",
  "version": "0.0.0",
  "authors": [
    "kame"
  ],
  "license": "MIT",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ],
  "dependencies": {
    "bootstrap": "~3.2.0",
    "jquery": "~2.1.1"
  }
}

bower.jsonにbootstrapとjqueryが記述されているのと、 bower_componentsディレクトリとその中にファイルが生成されていればOK。


gulpの導入

npm init

まずは初期化コマンドでpackage.jsonを生成する。


今回、gulpで使用するプラグインは下記の通り。

  • coffee-script
    • gulpをcoffeeで書けるようにする
  • gulp
    • gulp本体
  • gulp-coffee
  • gulp-sass
  • gulp-filter
    • フィルタリング
  • gulp-plumber
    • コンパイルエラー時でも監視を中断させないようにする
  • main-bower-files
    • bowerのパッケージを扱う
  • browser-sync
    • ファイルとブラウザ同期
  • del
    • ファイルの削除
npm install --save-dev coffee-script gulp gulp-coffee gulp-sass gulp-filter gulp-plumber main-bower-files browser-sync del

installコマンドで一括インストールする。
--save-devを付けることで自動的にインストールしたプラグインをpackage.jsonに書き出してくれる。


gulpfile.coffee

g = require 'gulp'
browserSync = require 'browser-sync'
gulpFilter = require 'gulp-filter'
mainBowerFiles = require 'main-bower-files'
sass = require 'gulp-sass'
coffee = require 'gulp-coffee'
plumber = require 'gulp-plumber'
del = require 'del'

g.task 'bower-files', ->
  jsFilter = gulpFilter('**/*.js')
  cssFilter = gulpFilter('**/*.css')
  g.src mainBowerFiles()
      .pipe jsFilter
      .pipe g.dest('public/js/libs/')
      .pipe jsFilter.restore()
      .pipe cssFilter
      .pipe g.dest('public/css/libs/')

g.task 'css', ->
  g.src 'assets/sass/*.scss'
      .pipe plumber()
      .pipe sass()
      .pipe g.dest('public/css')

g.task 'js', ->
  g.src 'assets/coffee/*.coffee'
      .pipe plumber()
      .pipe coffee()
      .pipe g.dest('public/js')

g.task 'bs', ->
  browserSync.init(null, {
      proxy: 'yourdomain'
  })

g.task 'bsReload', ->
  browserSync.reload()

g.task 'clean' , ->
  del [
      "public/js/libs/*"
      "public/css/libs/*"
      "public/js/*.js"
      "public/css/*.css"
  ]

g.task 'build'  ,
    [
        'bs'
        'bower-files'
        'js'
        'css'
    ]

g.task 'default', ['clean','build'], ->
  g.watch 'assets/coffee/*.coffee' ,['js']
  g.watch 'assets/sass/*.scss' ,['css']
  g.watch 'app.rb', ['bsReload']
  g.watch 'views/*.slim', ['bsReload']

上から順に説明していく。

require

g = require 'gulp'
browserSync = require 'browser-sync'
gulpFilter = require 'gulp-filter'
mainBowerFiles = require 'main-bower-files'
sass = require 'gulp-sass'
coffee = require 'gulp-coffee'
plumber = require 'gulp-plumber'
del = require 'del'

最初に必要なプラグインを読みだしておく。


bower

g.task 'bower-files', ->
  jsFilter = gulpFilter('**/*.js')
  cssFilter = gulpFilter('**/*.css')
  g.src mainBowerFiles()
      .pipe jsFilter
      .pipe g.dest('public/js/libs/')
      .pipe jsFilter.restore()
      .pipe cssFilter
      .pipe g.dest('public/css/libs/')

bowerで管理しているパッケージを指定のディレクトリに配置する処理だ。
基本的な流れはgulp.src()でファイルを指定し、何らかの処理をした後にgulp.dest()でファイルの書き出し先を指定する。
ここでは、gulp.src()でmainBowerFilesを指定し、そこからgulpFilterでjsとcss読み込み、gulp.dest()でpublicディレクトリ配下に書き出すといった流れだ。

gulpの基本的な使い方は以下の記事がわかりやすい。
タスクランナーgulp.js最速入門


sass,coffeeのcompile

g.task 'css', ->
  g.src 'assets/sass/*.scss'
      .pipe plumber()
      .pipe sass()
      .pipe g.dest('public/css')

g.task 'js', ->
  g.src 'assets/coffee/*.coffee'
      .pipe plumber()
      .pipe coffee()
      .pipe g.dest('public/js')

bowerの処理とほとんど一緒で、scssとcoffeeファイルを読み出し、コンパイルして、publicディレクトリ配下に書き出している。 plumber()コンパイルエラーが起きても監視(watch)を続行するためのもの。


browsersync

g.task 'bs', ->
  browserSync.init(null, {
      proxy: 'yourdomain'
  })

g.task 'bsReload', ->
  browserSync.reload()

BrowserSyncの設定タスクとファイルのwatchにつかうタスクを記述。
今回のようにプログラムを使用する場合はそれが動くサーバーを立ち上げて、設定タスクに使用するドメインをproxyで指定する必要がある。

livereloadというのもあるんだが、BrowerSyncはファイルとブラウザの同期だけではなくて、ブラウザ間同期も可能なのと、なんとスクロールやクリック等の操作も同期される。
なので、1つのブラウザを操作するだけで他のブラウザ(端末を選ばず)も同時に動作チェックすることができるので、かなり効率があがりますね。
実行時にBrowserSyncの管理画面が本ちゃん以外のポートで立ち上がって、色々カスタマイズできたり、変更履歴を簡単に確認できたりととても便利そう。

BrowserSync


clean

g.task 'clean' , ->
  del [
      "public/js/libs/*"
      "public/css/libs/*"
      "public/js/*.js"
      "public/css/*.css"
  ]

cleanタスクの定義。 cssやjsが生成される前に、以前のデータを削除しておく。


build

g.task 'build'  ,
    [
        'bs'
        'bower-files'
        'js'
        'css'
    ]

buildタスクとして、各実装をまとめる。


watch

g.task 'default', ['clean','build'], ->
  g.watch 'assets/coffee/*.coffee' ,['js']
  g.watch 'assets/sass/*.scss' ,['css']
  g.watch 'app.rb', ['bsReload']
  g.watch 'views/*.slim', ['bsReload']

cleanタスクとbuildタスクをdefaultタスク統合。
defaultタスクは、文字通りデフォルトで実行されるタスクなので、gulpコマンドだけで実行できる。
同時にcompileとBrowserSyncの実装をファイル変更のたびに行うために、watchで監視対象ファイルを指定する。


以上でgulpの設定は完了。

実行

foremanを使う

ローカルサーバーの立ち上げも一緒にやってしまいたいので、shotgunとgulpコマンドをforemanを使って一括で実行する。
その際にshotgunで生成されるIPアドレスgulpfile.coffeeのBrowserSyncのドメインに設定する。

Procfile

application: bundle exec shotgun -o localhost --port=6000
gulp: gulp


gulpfile.coffee

g.task 'bs', ->
  browserSync.init(null, {
      proxy: 'localhost:6000'
  })

shotgunでport6000でサーバーを立ち上げ、そのサーバーのproxyの設定をBrowserSyncで行う。


bundle exec foreman start
20:14:40 application.1 | started with pid 14604
20:14:40 gulp.1        | started with pid 14605
20:14:40 gulp.1        | [20:14:40] Requiring external module coffee-script/register
20:14:41 application.1 | [2015-03-25 20:14:41] INFO  WEBrick 1.3.1
20:14:41 application.1 | [2015-03-25 20:14:41] INFO  ruby 2.1.2 (2014-05-08) [x86_64-linux]
20:14:41 application.1 | [2015-03-25 20:14:41] INFO  WEBrick::HTTPServer#start: pid=14604 port=6000
20:14:43 gulp.1        | [20:14:43] Using gulpfile ~/skelton/sinatra-skelton/gulpfile.coffee
20:14:43 gulp.1        | [20:14:43] Starting 'clean'...
20:14:43 gulp.1        | [20:14:43] Finished 'clean' after 8.16 ms
20:14:43 gulp.1        | [20:14:43] Starting 'bs'...
20:14:43 gulp.1        | [20:14:43] Finished 'bs' after 11 ms
20:14:43 gulp.1        | [20:14:43] Starting 'bower-files'...
20:14:43 gulp.1        | [20:14:43] Starting 'js'...
20:14:43 gulp.1        | [20:14:43] Starting 'css'...
20:14:43 gulp.1        | [BS] Proxying: http://localhost:6000
20:14:43 gulp.1        | [BS] Now you can access your site through the following addresses:
20:14:43 gulp.1        |
20:14:43 gulp.1        | [BS] Local (this machine):
20:14:43 gulp.1        | [BS] >>> http://localhost:3002
20:14:43 gulp.1        | [BS] External (other devices etc):
20:14:43 gulp.1        | [BS] >>> http://133.xxx.xxx.xxx:3002
20:14:43 gulp.1        |
20:14:43 gulp.1        | [20:14:43] Finished 'js' after 152 ms
20:14:43 gulp.1        | [20:14:43] Finished 'css' after 146 ms
20:14:43 gulp.1        | [20:14:43] Finished 'bower-files' after 207 ms
20:14:43 gulp.1        | [20:14:43] Starting 'build'...
20:14:43 gulp.1        | [20:14:43] Finished 'build' after 26 μs
20:14:43 gulp.1        | [20:14:43] Starting 'default'...
20:14:43 gulp.1        | [20:14:43] Finished 'default' after 24 ms

あとはforemanを実行すればshotgunとgulpが実行され、サーバーが立ち上がるはずだ。

git cloneして使う

git cloneして、いくつかコマンドを打てば、すぐに使用できるようになっている。 

npm install

package.jsonに書かれたパッケージをインストール


 npm install -g bower

bowerをインストールしていなければインストール。


bower install

bower.jsonに書かれたパッケージをインストール。


Procfile内のshotgunのドメインgulpfile.coffee内のBrowserSyncのproxyドメインを指定のドメインに変更。


bundle exec foreman start

shotgunとgulpの実行


以上でカスタマイズされたsinatraでの開発環境が整う。

sinatra-skelton

最後に

下記のQitaの記事にもあるように、railsとか使う場合でもjsとかのパッケージ管理はgemとか素でいれるんじゃなくて、bower-railsとか使ってbowerで管理したほうが良さげな感じですね。
gemだと対応遅い場合もあるし、素でいれるとしても更新作業とか面倒ですしね。
http://qiita.com/reikubonaga/items/5a6037e067b79e5f9849

gulpに関しては今回のようにちょっとしたWEBアプリや静的サイトを作る場合に適してるのかなー。
ただ下記のような記事もあった。

Gulp on Rails: Replacing the Asset Pipeline

railsの場合、assetsは時代遅れで、gulpをrailsに統合して使うべきだと。
railsとgulpを統合してて、railsの仕様通りapp/assetsを読みに行って、そこからgulpでbuildされたファイルを見に行く感じかな。

また、記事の中でフロントエンドよりの最新で便利な技術使いたいなら、railsに任せるのではなくて、そこは分けて使いましょうよと、言っていますね。

For me, there's no better tool than Gulp to glue these incredible build tools together. Unfortunately, Rails disagrees and reeeally wants us to use its tech stack to handle assets. Sure, there are a few gems out there that let you cobble together some of the technology above, but I've found that they quickly fall behind, don't expose all the same options, and some things just aren't possible.

あとはこんな記事も。
http://qiita.com/oreshinya/items/3d025dde2edc56622cc4

この辺に関しては、もう少し考えてみることにする。

参考URL

rails4.2でAuthlogicを使ってみた

railsでユーザー登録機能をつける際は勝手に色々とユーザー登録周りの機能を付けてくれるDeviseを普段よく使ってる。 しかし、少しレールから外れたことをしようとすると、Deviseのソースをoverrideしたり、直接さわらないといけなかったりと結構面倒くさいことをしないといけない。それによりコードが複雑になることによる管理コストも上がってきてしまう。
仕事でDeviseを使っていて、痒いところに手が届かなくなってしまったので、Deviseより機能がシンプルなAuthlogicを使うことにした。
簡単ではあるが、その際の構築メモをまとめておく。

導入

authlogic(github)

gem 'authlogic'
bundle install

DB/Migrate

db/migrate/create_users.rb

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string    :email,               null: false
      t.string    :crypted_password,    null: false
      t.string    :password_salt,       null: false
      t.string    :persistence_token,   null: false
      t.integer   :login_count,         null: false, default: 0
      t.integer   :failed_login_count,  null: false, default: 0
      t.datetime  :last_request_at
      t.datetime  :current_login_at
      t.datetime  :last_login_at
      t.string    :current_login_ip
      t.string    :perishable_token,    null: false

      t.timestamps
    end
    add_index :users, :email,unique: true
    add_index :users, :perishable_token
  end
end
rake db:migrate

上記のカラムは全てauthlogicで使用するカラム。
また、perishable_tokenについては、パスワードの再設定機能をつける際に必要なカラムなので、必要に応じて設定すると良い。
ここでの説明は省くので下記を参照
authlogic-password-reset-tutorial

ファイル構成

  • app
    • controllers
      • user
        • registrations_controller.rb
        • sessions_contrller.rb
    • views
      • user
        • registrations
          • new.html.slim
        • sessions
          • news.html.slim

ユーザー周りの機能はuserディレクトリにまとめる。

Routeの設定

config/routes.rb

get "sign_up" => "user/registrations#new"
get "sign_in" => "user/sessions#new"
delete "sign_out" => "user/sessions#destroy"
namespace :user do
  resources :registrations, only: :create
  resources :sessions, only: :create
end

登録(registration)とログイン(session)のそれぞれのルーティングの設定をする。

Modelの実装

app/models/user.rb

acts_as_authentic do |c|
  c.login_field = :email
  c.validates_uniqueness_of_email_field_options({value: true})
  c.merge_validates_length_of_password_field_options({minimum: 6})
end

ユーザーモデルに上記を設定する。
基本的には中の記述は必要ないが、どの項目でログインするかだとか、validationの細かい設定だとかを設定できる。 とりあえず、emailuniqueにしたり、passwordの最小値を設定したりした。
何が出来るかは下記参照。

ドキュメント - Module: Authlogic::ActsAsAuthentic
ドキュメント見難い・・・もっとわかりやすくまとめてほしい

ControllerとViewの実装

まずは基板から

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  #ログインユーザーのセット
  helper_method :current_user, :logged_in?

  protected
    #UserSessionから現在ログインしているユーザーを取得
    def current_user_session
      return @current_user_session if defined?(@current_user_session)
      @current_user_session = UserSession.find
    end

    #現在のログインユーザーの値を設定
    def current_user
      return @current_user if defined?(@current_user)
      @current_user = current_user_session && current_user_session.user
    end

    #ログインが必要
    def require_login
      unless current_user
        redirect_to sign_in_path
        return false
      end
    end

    #ログイン状態か判定
    def logged_in?
      current_user_session != nil
    end

    #ログイン後のパス
    def after_login_path
      mypage_path
    end

end

ログインユーザーの情報取得機能やログインのフィルター機能等を記述する。
説明はコメントで書いている通り。
UserSessionは後ほどログイン機能追加のところで説明。

app/controllers/user/base_controller.rb

class User::BaseController < ApplicationController
  layout "application"
  before_action :require_login
end

registrations_controller.rbsessions_controller.rbの親となるcontrollerを作成。
今後、registrations_controller.rbにユーザー情報の変更機能等を付けることを想定し、before_actionrequire_loginを設定。
登録時とログイン時以外は上記で権限の制限をする。

ユーザー登録

やっと本題。

app/controllers/user/registrations_controller.rb

class User::RegistrationsController < User::BaseController
  skip_before_action :require_login, only: [:new, :create]

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    begin
      User.transaction do
        if @user.valid?
          @user.save!
          flash[:notice] = '会員登録が完了しました。'
          redirect_to after_login_path
        else
          render action: :new
        end
      end
    rescue => e
      p e.message
    end

    private
      def user_params
        params.require(:user).permit(:email,:password,:password_confirmation)
      end

  end

app/views/registrations/news.html.slim

= form_for(@user,url: user_registrations_path,method: :post, html: {class: 'form-horizontal'}) do |f|
  .form-group
    p= f.label :email,class: 'col-sm-4 control-label'
    .col-sm-8
      .error= errors_for @user, :email
      p= f.text_field :email, autofocus: true, class: 'form-control'
  .form-group
    p= f.label :password,class: 'col-sm-4 control-label'
    .col-sm-8
      .error= errors_for @user, :password
      - if @validatable
        em
          | (
          = @minimum_password_length
          |  文字以上)
      p= f.password_field :password, autocomplete: "off", class: 'form-control'
  .form-group
    p= f.label :password_confirmation,class: 'col-sm-4 control-label'
    .col-sm-8
      .error= errors_for @user, :password_confirmation
      p= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control'

  .form-group
    .col-sm-offset-4.col-sm-8
      = f.submit "登録",:class => 'btn btn-default'

newアクションとcreateアクションはskip_before_actionrequire_loginを実行しないようにする。
viewに関してはbootstrapを使っているのであしからず。

以上でユーザー登録ができるようになる。
パスワードの入力確認や暗号化は勝手によろしくやってくれる。

ユーザーログイン

app/controllers/user/sessions_controller.rb

class Account::SessionsController < Account::BaseController
  skip_before_filter :require_login, only: [:new, :create]

  def new
    @user_session = UserSession.new
  end

  def create
    @user_session = UserSession.new(user_session_params)

    if @user_session.save
      flash[:notice] = 'ログインしました。'
      redirect_to after_login_path
    else
      render action: :new
    end
  end

  def destroy
    current_user_session.destroy
      flash[:notice] = 'ログアウトしました。'
    redirect_to root_url
  end

  private
    def user_session_params
      params.require(:user_session).permit(:email, :password)
    end
end

app/views/user/sessions/new.html.slim

h2 ログイン
div
  = form_for(@user_session, url: user_sessions_path, method: :post, html: {class: 'form-horizontal'}) do |f|
    = errors_for_session(@user_session)
    .form-group
      p= f.label :email,class: 'col-sm-4 control-label'
      .col-sm-8
        = errors_for @user_session, :email
        p= f.text_field :email, autofocus: true, class: 'form-control'
    .form-group
      p= f.label :password,class: 'col-sm-4 control-label'
      .col-sm-8
        = errors_for @user_session, :password
        p= f.password_field :password, autocomplete: "off", class: 'form-control'
    .form-group
      .col-sm-offset-4.col-sm-8
        = f.submit "ログイン",:class => 'btn btn-default'

ログインの管理はUserSessionで行う。
UserSessionはデータベースに登録するものではないが、ActiveRecordのようにsaveメソッドが使えたりする。

その他

app/views/application.html.erb

- if logged_in?
  = link_to "マイページ", mypage_path
  = link_to "ログアウト", sign_out_path,:method => :delete
- else
  = link_to "ログイン", sign_in_path
  = link_to "新規登録", sign_up_path

上記のようにリンクを追加する。

最後に

上記の場所以外のどこからでも簡単に登録・ログイン機能が実装可能だ。
ただ、メールの送信やパスワード再発行などの細かい機能は自分で実装しなければいけないので、凝ったことをしないのであればDeviseを選ぶのが無難かなあ。

参考URL

【Rails】 Authlogicでユーザー認証機能
Rails4でauthlogicを試してみる(導入編)