Madogiwa Blog

主に技術系の学習メモに使っていきます。

【Rails】Webpackerを使ってRailsにフロントエンド開発環境を作ってみた

みなさん、こんにちは。まどぎわです(・∀・)
今日は、RailsアプリにWebpackerを使って、フロントエンド環境を整えて少し開発してみたので、そのあたりをブログに整理しておこうと思います🙌

WebPackerとは?

WebPackerとは、RailsでWebpackをいい感じに扱うことができる機能です。
私もまだ全然理解が足りてない部分がありますが、公式ドキュメントや下記にまとまってますので、興味がある方は読んでみてくださいm( )m

github.com

techracho.bpsinc.jp

今回やったこと

今回は私の開発している「Quizアプリ Quiq」に評価機能を作ってみました。

こんな感じ
f:id:madogiwa0124:20180826181920g:plain

下記から実際に触ることができます🙌

quiz-quiq.herokuapp.com

Webpackerの導入手順

Gemfileに書きを追記してbundle installを実施

gem 'webpacker', github: 'rails/webpacker'

その後、webpackervueをinstallします。

bundle exec rails webpacker:install
# 下記は導入したいフレームワークに合わせて修正してください。
bundle exec rails webpacker:install:vue

これで、bin/webpackでwebpackのビルドが走るようになり、railsでvueを扱えるようになります(・∀・)

フロントエンドの実装

今回は下記のような機能をRails上にVue.jsを使って実装しました。 デフォルトのルールでは、下記のようなディレクトリ構成で実装していきます。
(vue.jsの実装の仕方等は、tutorial等が充実してますので、そちらをご確認ください。)

# viewから読み込むEndpoint
app/javascript/packs
# その他
app/javascript

実際に私が書いたコードが下記です(お見苦しいかもしれません。。。)

QuizLike.vue

ここでは、実際のVueコンポーネントを実装してます。rails側へのpost処理等を実装してます。

<template>
  <div style="text-align: right">
    <button class="btn btn-success btn-sm" @click="create(1)"><i class="fas fa-thumbs-up"></i></button>
    <button class="btn btn-danger btn-sm" @click="create(-1)"><i class="fas fa-thumbs-down"></i></button>
    <span class="text-secondary">評価 : {{ like_count }} pt</span>
  </div>
</template>

<script>
import axios from 'axios';
import { csrfToken } from 'rails-ujs'
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()
export default {
  data: function () {
    return {
      like_count: this.init_like_count
    }
  },
  props: {
    init_like_count: {
      type: Number,
      required: true
    },
    quiz_id: {
      type: Number,
      required: true
    }
  },
  methods: {
    create: function (value) {
      this.like_count += value
      axios.post(`/quizzes/${this.quiz_id}/like`, {
        like: { value: this.like_count }
      });
    }
  }
}
</script>

packs/quizzes.js

ここでは、作成したコンポーネントの読み込み、Vueインスタンスの作成を行うEndpointを定義してます。

import Vue from 'vue/dist/vue.esm'
import QuizLike from '../QuizLike.vue';

new Vue({
  el: '#like_area',
  components: { QuizLike },
})

result.html.haml

ここでは、javascript_pack_tagを使って、Endpointの読み込みをおこなっています。

= javascript_pack_tag 'quizzes'

point

railsにpostする

railsにpostを送るときには、axiosを使用するの結構メジャーみたいです。
初めて使ってみましたが、下記のようにjsonでパラメーターを作成して簡単にpostがおくれました🙌

axios.post(`/quizzes/${this.quiz_id}/like`, {
    like: { value: this.like_count }
});

postする際の注意点

特に何も考えずにpostメソッドを送ろうとすると、RailsCSRF対策に引っかかり、InvalidAuthenticityTokenが発生してしまいます。Vueコンポーネントからrailsにpostする場合は、下記のように明示的にTokenを作成し、headerに付与してあげる必要があります👀

import axios from 'axios';
import { csrfToken } from 'rails-ujs'
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken()

Tips

上でも少し触れましたが、いろいろハマったところをtipsとして、対応方法を軽くですがメモしておきますφ(・

Uglifier::Error: Unexpected token

下記Issueを参考にproduction.rbを修正して対応しました。

config.assets.js_compressor = Uglifier.new(harmony: true)

github.com

Module build failed: ReferenceError: Unknown plugin "@babel/plugin-transform-destructuring"

下記Issueを参考にyarn add https://github.com/rails/webpacker.gitを実行して対応しました。

Module build failed, Unknown plugin "@babel/plugin-syntax-dynamic-import" after webpacker:install:vue · Issue #1565 · rails/webpacker · GitHub

herokuデプロイ時にnode系のmodule fetchで500Errorで落ちる

何回かリトライしたら正常終了しました。

おわりに

今回はWebpacker使って、フロントエンド開発環境を作ってみました。
良いんだか悪いんだかわからないですが、webpack等フロントエンド関連技術をしらない自分でも結構簡単にフロントエンド環境を作ることができました🙌

これからはフロントエンド技術も学んで、よりユーザーに優しいサービスを作っていきたいです👀

参考

top-men.hatenablog.com

qiita.com

tech.medpeer.co.jp

【開発効率UP】Emacsキーバインドでコンソール操作を快適にしよう!

みなさん、こんにちは。まどぎわです(・∀・)
コンソール操作って結構めんどくさいですよね。。。

例えばコマンドを打ち間違えてしまった場合、普通にやると下記のようになるんじゃないかな?と思います。

f:id:madogiwa0124:20180819183742g:plain

しかしEmacsキーバインドを使うと、下記のように修正できます(・∀・)

f:id:madogiwa0124:20180819183715g:plain

上と比べてみても結構快適そうですよね?🙌

実際に開発してるときは、コンソールで操作してるときも多いと思うので、コンソール操作を効率的に行えると開発効率も上がるんじゃないかなと思います。

Emacsキーバインドは、MacのPCであればで特にインストール等を不要で使えますし、コンソールだけじゃなくてブラウザ操作やEditorでも使えるので、ぜひ使ってみてください(・∀・)

Emacsのよく使うコマンド

よく使いそうなコマンドを下記に記載してみました、Emacsにはまだまだいろいろなコマンドがありますが、とりあえずコレだけ覚えておくだけでも大分快適になるんじゃないかな?と思います👀

コマンド 動作
ctrl + f 次の文字へ
ctrl + b 前の文字へ
ctrl + n 次の行へ
ctrl + p 前の行へ
ctrl + a 行頭へ
ctrl + e 行末へ
ctrl + k カーソル以降を削除

fとかbとかnとか一見覚えにくいなぁと思いますが、fはforward、bはback、nはnextと、意味から推測出来る単語の頭文字を取っているので、使っていると自然に覚えやすいと思います🙌

ちなみに、最初の例は下記のような操作を行っています。

  1. 入力ミス
  2. ctrl + aで行頭へ移動
  3. ctrl + fで修正部分まで移動
  4. 訂正し、再実行

f:id:madogiwa0124:20180819183715g:plain

EmacsキーバインドをEditorで使う

Emacsキーバインドを覚えるとEditorでも使いたくなってしまうのですが、メジャーなEditorにはだいたいEmacsキーバインド拡張機能があるようです。みなさんも使ってみては?👀

おわりに

今日は、コンソール操作を効率化する方法としてEmacsキーバインドを紹介しました。

コマンドも意味のある名前の頭文字になっていて、インストール不要で使えるので、そこまで学習コストを掛けずに覚えられるので、開発効率UPになるんじゃないかと思います!(・∀・)

みなさんも、是非使ってみてください🙌

参考

Emacsのキーバインド覚書

qiita.com

qiita.com

Ruby:ログ出力を支援するsppというgemを作ってみた💎✨

こんにちは、まどぎわです(・∀・)
今回はsppというログ出力を支援するGemを公開したので、それについて書こうと思います🙌
(ちなみに初めてRubyGemsにコードを公開してみました...!)

ちなみに今回リリースしたGemは下記です。

spp | RubyGems.org | your community gem host

github.com

使い方はこんな感じ

Spp::spp('hoge')
=>
========== START ==========
"hoge"
========== E N D ==========

使う前には、Gemfileに下記を追記するか、gem install sppを実行するだけです👀

gem 'spp'

ログを見ながらデバッグするときとか、こんな感じに便利かなぁと思います(..)
下記のような感じでやると取得したユーザー一覧の取得処理をログ上で目立たせることができます🙌

  def index
    @quizzes = if params[:search_text]
                 Quiz.includes(:choices).search(params[:search_text])
               else
                 Quiz.includes(:choices)
               end
    Spp::spp(@quizzes.pluck(:title).uniq)
  end

こんな感じで目立つ(・∀・)

========== START ==========
["テスト", "タイトル", "HUNTERXHUNTERクイズ", "プロレスクイズ", "テストあああ", "私についてのQuiz"]
========== E N D ==========

ちなみに下記のような感じで、修飾をカスタマイズすることもできますφ(..)

  def index
    @quizzes = if params[:search_text]
                 Quiz.includes(:choices).search(params[:search_text])
               else
                 Quiz.includes(:choices)
               end
    Spp::spp(@quizzes.pluck(:title).uniq, '開始', '終了', '' * 5)
  end

こんな感じ(・∀・)

⚡⚡⚡⚡⚡ 開始 ⚡⚡⚡⚡⚡
["テスト", "タイトル", "HUNTERXHUNTERクイズ", "プロレスクイズ", "テストあああ", "私についてのQuiz"]
⚡⚡⚡⚡⚡ 終了 ⚡⚡⚡⚡⚡

内部的にはシンプルで、下記コードが実行されているだけなのですが、毎回書くのがめんどくさかったのでGemにしてみました🙌

puts "========== START =========="
pp 'hoge'
puts "========== E N D =========="

ちなみにこんな感じで書くと、

  def index
    Spp::spp_bench do
      @quizzes = if params[:search_text]
                  Quiz.includes(:choices).search(params[:search_text])
                else
                  Quiz.includes(:choices)
                end
      @quizzes.pluck(:title).uniq
    end
  end

ベンチマークも取れます💪

========== START(2018-08-09 22:12:16 +0900) ==========
   (5.1ms)  SELECT "quizzes"."title" FROM "quizzes" LEFT OUTER JOIN "choices" ON "choices"."quiz_id" = "quizzes"."id"
  ↳ app/controllers/quizzes_controller.rb:12
["テスト", "タイトル", "HUNTERXHUNTERクイズ", "プロレスクイズ", "テストあああ", "私についてのQuiz"]
========== E N D(2018-08-09 22:12:16 +0900) ==========

同じように修飾もできます(・∀・)

⚡⚡⚡ START(2018-08-09 22:15:06 +0900) ⚡⚡⚡
   (1.3ms)  SELECT "quizzes"."title" FROM "quizzes" LEFT OUTER JOIN "choices" ON "choices"."quiz_id" = "quizzes"."id"
  ↳ app/controllers/quizzes_controller.rb:12
["テスト", "タイトル", "HUNTERXHUNTERクイズ", "プロレスクイズ", "テストあああ", "私についてのQuiz"]
⚡⚡⚡ E N D(2018-08-09 22:15:06 +0900) ⚡⚡⚡

みなさんも良かったら使ってみてくださいm( )m

ソースコードや詳しい使い方はこちら github.com

Gemの作り方

Gemの作り方は下記記事を参考にさせて頂きましたm( )m
意外と簡単に作れるので、みなさんも作ってみては?🙌

morizyun.github.io

qiita.com

masarakki.github.io

ハマったところ

一応gem公開時にハマった箇所もメモしておきますφ(..)メモメモ

rake release時の権限Error

bundle exec rake release実行時に下記のようなErrorが出てハマりました(T_T)

You do not have permission to push to this gem. Ask...

原因は、単純にgemの名前が重複していて、既存のgemにpushしようとして権限Errorになってました。。。
重複しないようにコードの内のgemの名前を指定している箇所を置換して解決しました。

Ruby:mini_magickを使って画像に文字(テキスト)を合成する

みなさん、こんにちは。まどぎわです(・∀・)
今日は、mini_magicと使って画像にテキストを合成する方法をメモしておきますφ(..)

mini_magicを使うと某匿名質問サービスみたいに画像に文字を合成する機能を結構簡単に作ることが出来ます🙌

↓公式のリポジトリはこちら↓

github.com

mini_magicの使い方

準備

mini_magicを使うには、ImageMagickというアプリがインストールされている必要があります。 まずはインストールされているかどうか確認してください。

$ convert -version
Version: ImageMagick 6.9.9-40 Q16 x86_64 2018-07-14 http://www.imagemagick.org
Copyright: © 1999-2018 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
Features: Cipher DPC Modules
Delegates (built-in): bzlib freetype jng jpeg ltdl lzma png tiff xml zlib

インストールされていなかったかたは、「ImageMagick インストール」等でググッってインストールしてください(..)

mini_magicのインストール

mini_magicも通常のGemと同じようにGemfileに追記するか、下記コマンドを実行することでインストールすることが出来ます。

gem 'mini_magick'

文字列を受け取って画像を生成してみる

背景画像を準備

まずは文字列を埋め込む背景画像を用意します。今回は、こんな感じのを用意しました。 f:id:madogiwa0124:20180721163042p:plain

文字列を受け取って画像に合成する処理を作ってみる

実際の実装を下記に記載しておきますφ(..)

class ImageHelper
  require 'mini_magick'
  require 'securerandom'

  BASE_IMAGE_PATH = './app/assets/images/bg_image.png'.freeze
  GRAVITY = 'center'.freeze
  TEXT_POSITION = '0,0'.freeze
  FONT = './app/assets/fonts/komorebi-gothic.ttf'.freeze
  FONT_SIZE = 65
  INDENTION_COUNT = 11
  ROW_LIMIT = 8

  class << self
    # 合成後のFileClassを生成
    def build(text)
      text = prepare_text(text)
      @image = MiniMagick::Image.open(BASE_IMAGE_PATH)
      configuration(text)
    end

    # 合成後のFileの書き出し
    def write(text)
      build(text)
      @image.write uniq_file_name
    end

    private

    # uniqなファイル名を返却
    def uniq_file_name
      "#{SecureRandom.hex}.png"
    end

    # 設定関連の値を代入
    def configuration(text)
      @image.combine_options do |config|
        config.font FONT
        config.gravity GRAVITY
        config.pointsize FONT_SIZE
        config.draw "text #{TEXT_POSITION} '#{text}'"
      end
    end

    # 背景にいい感じに収まるように文字を調整して返却
    def prepare_text(text)
      text.scan(/.{1,#{INDENTION_COUNT}}/)[0...ROW_LIMIT].join("\n")
    end
  end
end

こんな感じで、使うことが出来ます(・∀・)

# 生成した画像のFileClassを取得
ImageHelper.build('何かしらの文字列を合成してみる').tempfile.open.read
# 生成した画像の書き出し
ImageHelper.write('何かしらの文字列を合成してみる')

↓出来た画像がこちら↓ f:id:madogiwa0124:20180721164012p:plain

おわりに

設定周りのやり方を調べるのに結構苦労しましたが、実装は意外とシンプルですね(・∀・)
ちなみに私も最近作ったサービスでも使ってみました🙌
↓みたいなQuizを投稿して画像でシェア出来るサービスです(・∀・) quiz-quiq.herokuapp.com

参考資料

keruuweb.com

github.com

RubyonRails:current_userへのメソッド呼び出しで発生したN+1に対応する

まどぎわです(・∀・)今日は、current_userへのN+1問題の対応についてです!
railsで開発していると、N+1問題にぶつかって対応することが多いかと思います。 普通であれば、DBアクセス時にincludes等を使って関連モデルをキャッシュしておくことで対応することが多いかと思います。

しかし、current_userのメソッド呼び出しでN+1問題が発生してしまった場合、だいたいDBアクセス処理はライブラリで行われているため、DBアクセス時に関連モデルをキャッシュしようとしても、「どうすれば、いいんや・・・」という気持ちになり、対応方法を調べたのでメモしておきますφ(..)

前提

今回は、下記のようなUserモデルとQuestionモデルとQuestionAnswerモデルがあり、Userモデルに#answerd?というUserがその質問に回答済みかどうかを判定するメソッドを定義しています。
何も対応しないとQuiz一覧画面等で、このメソッドを呼び出すとN+1問題が発生してしまいます。。。

class User < ApplicationRecord
  has_many :question_answers

  def answerd?(question)
    question.question_answers.to_a.select{ |answer| answer.user_id == id }.present?
  end
end

class QuestionAnswer < ApplicationRecord
  belongs_to :user
  belongs_to :question
end

class Question < ApplicationRecord
  has_many :question_answers
end

対応方法

current_userをもとにUserインスタンスを再生成する

controller内でcurrent_userをもとにUserインスタンスを再生成し、その際にincludesで関連モデルをキャッシュするようにします!これにより、#answerd?を呼び出してもキャッシュされた値から処理が行われるので、N+1問題を防ぐことが出来ます🙌

class QuizzesController < ApplicationController
  def index
    @user = User.includes(:question_answers).find(current_user.id)
    @questions = Quiz.includes(:choices)
  end
end

参考

stackoverflow.com

RubyonRails:rails5.2の新機能「ActiveStrage」で画像アップロード処理をお手軽に実装してみる

Rails5.2でActiveStrageというファイルアップロード系の機能が新しく追加されましたね(・∀・)
ちょっと自分が作っているアプリで使ってみたので使い方をφ(..)メモメモ

ActiveStrageとは?

ActiveStrageとは、Rails5.2から実装されたファイルアップロード機能です。
今までは画像等をアップロードするときにはCarrierWaveShrineといったGemを使うことが多かったと思いますが、それがRailsだけで簡単に出来るようになりました🙌

railsguides.jp

ActiveStrageを使ってみた

ソースだけ見たい方はこちら👀

github.com

準備

まずはActiveStrageを使うために下記コマンドを実行します。

$ rails active_storage:install
$ rails db:migrate

そして、ファイルのアップロード先を設定します。アップロード先はconfig.active_storage.serviceに設定されたkeyでstarage.yamlを参照します。

strage.yml

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

development.rb

  # Store uploaded files on the local file system (see config/storage.yml for options)
  config.active_storage.service = :local

ファイルをアップロードしてみる

まずはModelにファイルのプロパティを定義します。1つのファイルをアップロードする場合はhas_one_attached、複数のファイルをアップロードする場合はhas_many_attachedを使用します。

class User
  has_one_attached :avatar
end

次にcontrollerとviewを次のように実装します。
新規登録の場合は、パラメーターにavatorを追加してnewの引数に渡してあげるだけでいいのですが、既存レコードにファイルを設定する場合は.avatar.attachを使用する必要があります。

controller

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user, notice: message('user', 'create')
    else
      render :new
    end
  end

  def update
    @user = User.find(current_user.id)
    @user.avatar.attach(params[:user][:avatar])
    if @user.update(user_params)
      redirect_to @user, notice: message('user', 'update')
    else
      render :edit
    end
  end

  private

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

view

= form_for @user do |f|
  %div.form-group
    = f.label :avatar
    = f.file_field :avatar, class: 'file-inputs'
  .actions
    = f.submit t('common.save'), class: 'btn btn-primary'

これで、画面からアップロードが出来るようになりました!!(・∀・)

バリデーションを実装してみる

しかし、このままでは画像以外がアップロードされてしまったり、大きなファイルがアップロードされてしまうといったことが防げません。。。
ActiveStrageは、まだvalidationの機能が無いので、自分で実装する必要があります😥

今回は、サイズのチェックと画像のみをアップロード可能とするようなバリデーションをサンプルとして載せましたので、参考までに。

class User < ApplicationRecord
  validate  :file_validation, if: -> { avatar.attached? }

  private

  def file_validation
    if avatar.blob.byte_size > 1_000_000
      file_raise_error('のファイル容量が大きすぎます')
    elsif !avatar.blob.content_type.starts_with?('image/')
      file_raise_error('は、画像以外はアップロード出来ません')
    end
  end

  def file_raise_error(message)
    avatar.purge
    errors.add(:avatar, message)
  end
end

おわりに

ActiveStrageはシンプルなファイルアップロード機能を実装するのであれば、お手軽に実装出来てよさそうに感じました(・∀・)
しかし、バリデーションを独自で実装する必要があるため、あまり複雑なアップロード処理にはまだ向かなそうですね😥

参考

tech.unifa-e.com

railsguides.jp

stackoverflow.com

関連モデルの関連モデルの読み込み処理を行うN+1問題を解決する。

関連モデルの関連モデルを参照する場合のN+1問題の解消方法で少しハマったのでφ(..)メモメモ

問題

例えば、下記のようなClassがあった場合にtask.user.groupを取得しようとするとGroupの読み込み処理によりN+1問題が発生する。
そのためTaskを取得する際にはTask.all.includes(:user, :group)といった対策が考えられるが、groupはタスクにassociationとして定義されていないので、指定出来ない。。。

class Task < ApplicationRecord
  belongs_to :user, required: false
  belongs_to :owner, class_name: 'User'
end

class User < ApplicationRecord
  has_many :tasks
  has_one :user_group
  has_one :group, through: :user_group
end

class UserGroup < ApplicationRecord
  belongs_to :user
  belongs_to :group
end

class Group < ApplicationRecord
  has_many :user_groups
  has_many :users, through: :user_groups
end

解決策

下記のようにincludes(user: :group)とすることで、関連モデルのassociationを使ってincludesを行うことが出来る🙌

Task.all.includes(user: :group)

参考

qiita.com