Railsの基本(+α)Part1
そもそも、一度しっかりRailsの基本についてメモしておこうと考え、これを書き始めたのですが、form_withのremote: trueを少し調べたり、Rails5.1からはWebpackが利用できるとかを知ったので、興味がVuejsに移っていき、Axiosでajaxする方向で検討したり、だいぶ基本から逸脱することも調べながら書いているうちに(迷走したというのが正しいかもですw)、かなり文章の量も増えてしまったので、いつくつかに分割することになってしまいました。
なので、全体の内容(およびポイント)としては
- Rails5.1でのWebpackの使い方
- ちょっぴりWebpackを勉強(私にとっては初めて)
- RailsでjQueryを利用しない
- sprocketやturbolinkを使わない
- Vuejsの基本
- vee-validaeの基本(特にajaxによるユニーク確認ができるように)
- deviseの基本
- Blogの登録や閲覧部分、およびプラスα(markdownの組み込みなど)
あたりになります(結果なりました)。そして、今回は6までです。
確認環境
筆者の環境は以下のようです。
- MacBook Pro (15-inch, Late 2016)
- MacOS Sierra 10.12.6
- ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin16]
- Rails 5.1.4
- mysql Ver 14.14 Distrib 5.7.18, for osx10.12 (x86_64) using EditLine wrapper
- node v8.1.0
前提
上記の環境でRailsはバージョン5.1(必須)、rubyは少なくとも2.3以上(多少違っていてもOKと思われます)、mysqlは5.6以上(これも多少違っていてもOKと思われます)とかは必要かもです。rails5.1からwebpackを利用できるようになったので、そのためnodeが、さらに、yarnもあると便利なので、これもインストールしておきます。
もしご自分のMacとかに上記のようなプログラムがない場合で、あまりサーバ系のインストールとかに慣れていない方は、http://qiita.com/etsracas/items/53f1230e13cf5f9c40baあたりを参考にして、brewとrbenv(rubyを色々なバージョンでインストールできるツール)とrubyをインストールします。
また、https://qiita.com/tabolog/items/da18143e70f40e356b5dを参考に、rbenv同様、nodebrewを利用してnodeをインストールし、yarnはターミナル上でnpm install -g yarn
を実行してインストールしてください(brewでもインストールできるようです。ただしrails5.1系ではproject_dir/binディレクトリにyarnをインストールしてくれるので、必須ではありません)。
さらに、次のように、先ほどインストールしたbrewを使ってmysqlをインストールし、そして最後に以下のようにして、mysqlのユーザを1人作成しておきます。
※sqliteを使うという手もあるので、その場合はインストールは必要ありません。
brew install mysql
brew tap homebrew/services
brew services start mysql
とこれで、mysqlがインストールされ、自動起動するようになっているはず。以下で確認。
brew services list
Name Status User Plist
...
mysql started chikkun /Users/chikkun/Library/LaunchAgents/homebrew.mxcl.mysql.plist
...
という行が出て入れば大丈夫です。
次にrootのパスワードを設定(パスワードまで真似しないでください<m(__)m>)。
mysqladmin -u root password 'yourpassword'
次のようなコマンドを叩いて、mysqlクライアントを立ち上げます。パスワードが聞かれるので、上記のパスワードを入力してください。
mysql -u root -p
mysql上で
CREATE USER hoge IDENTIFIED BY 'hogechan';
GRANT ALL PRIVILEGES ON *.* TO 'hoge'@'localhost' IDENTIFIED BY 'hogechan' WITH GRANT OPTION;
FLUSH PRIVILEGES;
を実行して、hogeというユーザ(もちろんユーザやパスワードは任意)を作成します。
さて、開発開始
Railsの入門というとまずはScaffoldから、というのが多いのですが、これだと今ひとつ何をやるべきことなのか、ということがブラックボックスになってしまい「う〜ん・・・?」という中途半端な理解で終わってしまうことが多い気がします。
そこで、何かしらフォームを使ってのデータの登録を行う簡単なアプリを、Scaffold機能を(その他コマンドも)使わず行ってみようと思います。
しかも、若干基本よりはみ出して、実用性があることまで持って行きたいと考えております。
どんなアプリを作成するか
usersテーブルを作成し、そこに入るデータは管理者のユーザ名、パスワードなどを登録します。そして、この管理者に登録された人でないと、その管理者自身を登録できないという仕様とします(次回deviseで実装)。
また、この管理者は簡易ブログをタグとカテゴリを伴って登録することができ、そのブログは不特定多数の人が見ることができる、そんな極簡単なブログシステムです。一応プロジェクト名をshinraとします(森羅万象を記す、的な安易な名前ですw)。
どんな作業をするかを外観
- Railsのプロジェクトを作成、database.ymlの設定とかを変更
-
テーブルのmigrationファイル作成とmigrate
要するに、テーブル(およびフィールド)の作成です。あとでDeviseを入れ込むときに、usersテーブルがぶつかりますが、Deviseが結構賢く、よしなに扱ってくれるらしい(これは次回)。
- Controller コントローラの作成です。今回はAjaxでのアクセスがメインになるので、api的な使い方がメインになります。index等を定義します。
-
Model
上記1.で作成したテーブルに対応したモデルの作成です。バリデーションはクライアント側で行うので、何もないようなModelになってしまいそうです。
-
View
最終的にはブラウザにhtmlとして出力される、Rail標準のテンプレートの作成です。今回は、クライアントでバリデーションするので、このテンプレートの中にJavaScriptでガリガリ書くことになるかもしれません。
ただ、始めのページで読み込まれた後は、VuejsでAjax通信させるので、index.html.erbのみ書きます。その分Javascriptの量がさらに増えそうですが・・・。
- routes どんなURL(とメソッド—POSTやGET)だったら、どのコントローラのどのメソッドを実行するかを定義したものです。例えば、http://sample.rails.jp/ にGETでアクセするとusers_controller.rbのindexというメソッドを実行する、などという記述をすることになります。
Railsプロジェクトの作成
まずは、ターミナルを立ち上げて、プロジェクトを作成する1つ上のディレクトリを作成します(mustじゃありません)。
cd ~
mkdir projects
cd projects
次にRailsプロジェクトを作成します。–database=mysqlオプションでmysqlを指定して、作成します(mysqlがインストールできていない場合は、この指定をやめます→するとsqliteが使われ、これはインストール等が必要ありませんので、単に試すには便利です)。
さらにwebpackというオプションも付けて、Javascript等はWebpackに管理させます。
- ※
-
rails new appli_name –database=database_name というように-dでデータベースの種類を指定すると、少し幸せになります。
- ※
-
rails new appli_name -d database_name –webpack というようにwebpackを利用する設定にします。
$ rails new shinra --webpack --skip-sprockets --skip-javascript --database=mysql
.....
cd shinra
また、スプロケットやrails-ujs(以前はjquery-ujs)等も使わないため、上記のような–skip-sprockets –skip-javascriptという2つのオプションも付けました。
色々、標準出力が出てきますが、とりあえずスルーして、shinraというプロジェクトディレクトリにcdします。
Vuejsのインストール
インストールと言っても、railsコマンドで一発です。ついでにAjaxで利用するAxiosもついでにインストールしておきます。
$ rails webpacker:install:vue
$ yarn add axios
これで終了w
ただ、一応この後、vuejsでHello worldをしてみます。しかも、実用性の高そうな単一ファイルコンポーネント(Single File Components)を試してみます。
/app/views/layout/application.html.erb等の修正
さすがに–skip-sprockets –skip-javascriptを指定しているので、/app/views/layout/application.html.erb内に見慣れたapplication.js部分もありません(何故かCSSだけはありますね)。わーお、すっきりしたぁ。
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<title>Wakaran</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all' %>
</head>
<body>
<%= yield %>
</body>
</html>
さて、これを次のような変更を加えます。
- CSS関係
- stylesheet_link_tagをstylesheet_pack_tagに変更。
- /app/assets/stylesheets/application.cssの拡張子を変えて、/app/javascript/pack/application.scssに移動させます。
-
中の
*= require_tree . *= require_self
となっている、イコールをとって、単なるコメントにします(もしくは、削除してしまう)。そして、次のものを書き込みます(暫定)。
p { color: red; }
- javascript関係
-
/app/javascript/pack/application.jsをworldwide.jsにリネームします。そして、もともとのを少しだけ変えて、次のようにします(in worldswide.jsを加えただけ)。
console.log('Hello World from Webpacker in worldwide.js');
- ※
- application.jsだと、何故かうまくいかない・・・。なぜだかは追えていません・汗。
-
/app/views/layout/application.html.erbに以下を加えます(CSSの上あたり)。
<%= javascript_pack_tag 'worldwide' %>
-
/app/controllers/welcome_controller.rb
コントローラが1つもないと確認のしようもないので、ランディングページ的な/app/controllers/welcome_controller.rbを作成します。
1
2
3
4
5
6
class WelcomeController < ApplicationController
def index
end
end
何もないindexメソッドしかありませんが、次のようなルールで一応javascriptの確認はできそうです。
- ※
-
controller内のrenderのルールは「app/views/コントローラ名/メソッド名.html.erb」を描画します。
つまり、/app/views/welcome/index.html.erbを次で作成します。
/app/views/welcome/index.html.erb
/app/views/welcome/index.html.erbというファイルを作成して、次のような内容を書き込みます。
1
2
3
4
5
6
7
<%= stylesheet_pack_tag 'welcome', media: 'all' %>
<div id="page-header" class="page-header">
<h4>トップページ</h4>
</div>
<p>Hello Rails 5.1</p>
<div id="MyAppRoot"></div>
<%= javascript_pack_tag 'welcome' %>
- 1行目はwelcome.cssを読み込め(下にあるindex.vueファイルから自動作成される)、というERBへの指示
- 2〜4行はただのhtml
- 6行目は、Vue.jsと紐付けるところ
- 7行目はwelcome.jsを読み込め(後で作成)、というERBへの指示
ここで上記3のところに、Hello Vue!を表示させます。
/app/javascript/packs/index.vue
/app/javascript/packs/index.vueが少し触れた単一ファイルコンポーネントです。簡単に言ってしまえば、templateやjavascriptやstyleをコンポーネント毎に1つのファイルにまとめて書いてしまえ、という感じです。実際には次のように書き込んでおきます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data: () => {
return {
message: "Hello Vue!"
}
}
}
</script>
<style scoped>
p {
font-size: 10em;
text-align: center;
color: blue;
}
</style>
- 1〜5行目がこのコンポーネントのテンプレート
- 6〜14行目までは、この中で動くJavascriptのスクリプト
- 17〜22行目までがstyle
です。
3に関しては少々コメントしますと、<style scoped>
のようにscopedが書いてあると、この場合だとCSSのpがp[data-v-071215b2]というように属性が書き込まれ、上記1にあるtemplateの中の全てのタグに同じ属性が書き込まれ、<div data-v-071215b2="" id="MyAppRoot"><p data-v-071215b2="">Hello Vue!</p></div>
というようになります。
つまり、これによって、他のコンポーネントはもとより、別のアプリと関係ないスタイルへの影響を避ける、というものです(これは意外に重宝しそうですね)。
/app/javascript/packs/welcome.js
/app/javascript/packs/welcome.jsは、/app/views/welcome/index.html.erb内のコンポーネントと/app/javascript/packs/index.vueの単一ファイルコンポーネントをマッピングするものになります。内容は次のようです。
1
2
3
4
5
6
7
8
9
import Vue from 'vue'
import App from './index.vue';
document.addEventListener('DOMContentLoaded', () => {
const app = new Vue({ // eslint-disable-line no-new
el: '#MyAppRoot',
render: h => h(App)
})
console.log(app)
})
上記の
document.addEventListener('DOMContentLoaded', () => {
の部分は、jQueryでは
$(document).ready
とやっていたところです。
また、/app/javascript/packs/index.vueをimportして、それをAppという変数に受け取り、
const app = new Vue(App).$mount('#MyAppRoot')
とうように、Vueのコンストラクターの引数に渡して、そして、$mountで、views/welcome/index.html.erbの<div id="MyAppRoot"></div>
へのマッピング(紐付)をします。
config/routes.rb
次の2行を書き込み、ブラウザで確認できるようにします。
get "welcome/index"
root :to => 'welcome#index'
ブラウザによる確認
カレントディレクトリをshinraプロジェクトのルートにしたターミナルを2つオープンしておきます。
片方では
bin/webpack-dev-server
を実行します。Webpackがごちゃごちゃ言ってきますが、無視します。これはserverという名前が付いているように、ファイルを変更すると自動でWebpackを実行してくれる優れものです。
次に、railsを立ち上げます。
rails s
これでhttp://localhost:3000/にアクセスすると›
という感じの画面が見えるはずです。とりあえず、Hello World!は見えたということでw
ここのVue.jsのポイント
- /app/javascript/pack/application.scsはcssに変換されて、/app/views/layout/application.html.erbで読み込まれ、そこではcolor: red;と書いてあるのですが、それを上に出てきたindex.vueファイルの中のscopedのところに書いたCSSで上書きしています。反対にviews/welcome/index.html.erb内のHello Rails 5.1は赤いままです。
- Vue.jsには書き方が色々ありますが(参考:https://aloerina01.github.io/javascript/vue/2017/03/08/1.html)、今回の単一ファイルコンポーネント(Single File Components)はおすすめです(異論はあるかもしれませんが)。
-
index.vueの中のdataプロパティは関数を仕込むことを推奨しているようですが、その返しているオブジェクトのキーをtemplateの中から参照できます(<p>{{ message }}</p>のように)。
export default { data: () => { return { message: "Hello Vue!" } } }
-
index.vueの内容をVueとして登録するのがタグのIDを通じてで今回の場合、/app/views/welcome/index.html.erb内の
<div id="MyAppRoot"></div>
と/app/javascript/packs/welcome.jsのconst app = new Vue({ // eslint-disable-line no-new el: '#MyAppRoot', render: h => h(App) })
にあるelで指定した要素でマッピングします。
Database.ymlの変更
さて、今度はデータベースを作成します。まずは設定からです。今回はmysqlを指定したので、最初に作成したユーザやパスワードに変更します(productionのところとかは今回は変更しません)。
config/database.ymlというファイルをエディタで開くと次のようになっていると思います(コメントは取っています。また、上記で-dオプションでmysqlを指定していない場合はsqliteを使う設定なので、変更等は必要はありません)。
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password:
socket: /tmp/mysql.sock
development:
<<: *default
database: shinra_development
test:
<<: *default
database: shinra_test
production:
<<: *default
database: shinra_production
username: shinra
password: <%= ENV['SHINRA_DATABASE_PASSWORD'] %>
上記のうち変更するのは唯一mysqlのユーザの作成で作成したユーザ名とパスワードを書き込みます(上では「hoge:hogechan」でしたが、もちろん、任意です)。
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: hoge
password: hogechan
socket: /tmp/mysql.sock
これでrailsからmysqlにアクセスできるようになったはずです。
テーブルのmigrationファイル作成とmigrate
- ※
-
Railsでは、テーブル名は複数形にするのがルールです。
今回もそれに則っています。
さて、次の一覧のように8つのテーブルを作成します。id等はRailsが自動で作成してくれるので書いていません。
また、通常railsでは型を[string, text, integer, float, date, datetime, boolean, primary_key, time, timestamp, binary, decimal]あたりから選んで書くことになるのですが、このうち4種類しか利用していません(実際のDBには型やサイズ等がもっと色々ありますが、Railsではプログラミングでもなじみのあるもので代用できるようになっており、必要に応じてlimit等で長さを指定したりできます)。
- usersテーブル
- username:string
- email:string
- password:string
- rolesテーブル
- role_name:string
- user_rolesテーブル
- user_id:integer
- role_id:integer
- blogsテーブル
- user_id
- title:string
- content:text
- issue_date:datetime
- tagsテーブル
- tag_name:string
- categoriesテーブル
- category_name:string
- blog_tagsテーブル
- blog_id:integer
- tag_id
- blog_categoriesテーブル
- blog_id:integer
- category_id
これらのテーブルは
- usersとrolesがuser_roles(中間テーブル)を介して多対多
- usersとblogsが1:多
- blogsとtagsがblog_tags(中間テーブル)を介して多対多
- blogsとcategoriesがblog_categories(中間テーブル)を介して多対多
という関係なっています。ER図にすると、下のような感じになっています。
もう少しだけ説明すると、具体的なレコードで説明をすると(さらに下の表参照)、blogsにuser_idというフィールドがあって、これが書いたユーザが誰なのかを表すもので、ユーザ1人で複数のブログを書けるので、「1:多」ということになります(このuser_idは通常外部キーとか言います)。
次に、usersとrolesの関係、つまり、userがどんなロール(役割)を持てるかというのを表す際、もしユーザ1人にロール1つなら、1:1の関係で「1:多」の「多」が常に1になるような関係になれば良いのです。ただ今回は、一人のユーザが複数のロールを持てるようにするので(方法的にはいくつかありますが)、上のER図のように中間テーブルを利用して、つまり、中間テーブルでユーザとロールの関係を表現します。
すなわち(下の表のように)、IDが1のユーザ(chikkun)はadminとsuper_adminのロールを2つ持っているような場合、中間テーブルであるuser_rolesにuser_idが1でrole_idが1(これがadmin)のレコードとuser_idが1でrole_idが2(これがsuper_admin)の2レコードを作成することにより表現します。もちろん、user_idが2の人のようにロールが1つということも可能になります。これで中間テーブルを利用して、「多対多」を定義したということになります。
さて、具体的にmigrateしています。これもコマンドを叩いて作成することが可能ですが、今回は手作業で行います。
db/migrate/20170926153600_create_users_and_roles_and_user_roles.rb
というファイルに次のような内容で書き込みます。ここでファイル名の「20170925153600」は「年→月→日→時→分→秒」までの14桁(しっかりmigrateが順番になるようになっていれば大丈夫なんで、あまり正確じゃなくてもOKかも)、その後の名前はわかりやすいようにしただけです(ただし、createとかremoveとかある程度Railsは読み取っているらしいけれど、ここでは無視します)。
今回の名前は、usersとrolesとuser_rolesを作成する、というような意味にしました(もしかしたら、テーブル1つ1つ分けた方が良いという人もいるかもしれませんが、面倒なので・汗)。
ただ、後半のわかりやすいようにした名前は(数字は無視)、クラス名ではその名前に合わせたルールがあって、それは最初は大文字、そして_は削除して_の後ろは大文字するというものなので、「create_users_and_role_and_user_roles」は「CreateUsersAndRolesAndUserRoles」というようなクラス名になります。
- ※
-
migrationファイルは年〜秒までの14桁の数字+テーブル名やcreateとかaddとかわかりやすい言葉を_(半角アンスコ)でつなげて作成する。
- ※
-
migrationファイル内のクラス名は数字は無視して、最初は大文字、そして_は削除して_の後ろは大文字にして作成する。
- ※
-
rails db:createでデータベースができ、rails db:migrateで、テーブルの作成やフィールドの追加・変更・削除ができる。
- ※
-
rails db:dropでデータベースを削除して、再度行うことができるが、Rails5からはrails db:environment:set RAILS_ENV=developmentを実行しないとダメになった。
class CreateUsersAndRolesAndUserRoles < ActiveRecord::Migration[4.2]
def change
create_table :users do |t|
t.string :username, :null => true
t.string :email, :null => false
t.string :password
t.timestamps
end
create_table :roles do |t|
t.string :role_name, :null => false
t.timestamps
end
create_table :user_roles do |t|
t.integer :user_id, :null => false
t.integer :role_id, :null => false
t.timestamps
end
add_index :user_roles, :user_id
add_index :user_roles, :role_id
end
end
※usersのパスワードは今後インストールするdeviseの時に使わなくなるので、今回は「not null」をはずしています。
次に
db/migrate/20170926153800_create_blogs_and_tags_and_categories_and_chukan_tables.rb
というファイルを次のような内容で書き込みます。
class CreateBlogsAndTagsAndCategoriesAndChukanTables < ActiveRecord::Migration[4.2]
def change
create_table :blogs do |t|
t.integer :user_id, :null => false
t.string :title, :null => false
t.text :content, :null => false
t.datetime :issue_date
t.timestamps
end
create_table :tags do |t|
t.string :tag_name, :null => false
t.timestamps
end
create_table :categories do |t|
t.string :category_name, :null => false
t.timestamps
end
create_table :blog_tags do |t|
t.integer :blog_id, :null => false
t.integer :tag_id, :null => false
t.timestamps
end
create_table :blog_categories do |t|
t.integer :blog_id, :null => false
t.integer :category_id, :null => false
t.timestamps
end
add_index :blog_categories, :blog_id
add_index :blog_categories, :category_id
add_index :blog_tags, :blog_id
add_index :blog_tags, :tag_id
end
end
上記はchangeメソッドしか書いていませんが、フィールド名を変えたり、フィールドを削除したりするmigrationの場合はupメソッドとdownメソッドを書いて、migrateした場合はupメソッドが、roll back(元に戻)した場合はdownメソッドが実行されるように書いたりします。
これは、テーブルを作ったり、フィールドを増やしたりした場合は、ロールバックするにはそれを単純に削除するということで実現できるのでrailsはchangeだけ書けば問題なく処理してくれますが、フィールドを削除したり、フィールド名を変えたりした場合は、どんなフィールドを元に戻すのか、どんなフィールド名に戻すのかがchangeメソッドだけだとさすがのRailsでもわからないので、upメソッドとdownメソッドを書くということになります。詳しくはhttp://tanihiro.hatenablog.com/entry/2014/01/10/182122あたりをご参考に。
また、migrationファイルの書き方等はhttp://www.rubylife.jp/rails/model/index9.htmlや本家のhttps://railsguides.jp/active_record_migrations.htmlをご参考ください。
さて、このファイルを元に次のようなコマンドを叩きます。
rails db:create
rails db:environment:set RAILS_ENV=development #←これをやる必要がある!
rails db:migrate
これでエラーが起こらなかったら、DBやテーブルが作成されたはずです。
サンプルデータの作成
ついでに当面必要なusersテーブルのレコードを作成してきます。そのためにGemfileの適当なところに次の1行を書き込み、
gem 'forgery'
そして、ターミナル上で
bundle install
を実行します。
次にdb/seeds.rbに次のコードを書き込み
10.times do
User.create(email: Forgery('email').address, password: Forgery(:basic).password, username: Forgery('internet').user_name)
end
これで
rails db:seed
で、サンプルデータが投入されます(10人分)。
controller
- ※
-
controllerの名前は「テーブル名」+_+「controller.rb」というルールになっており、usersテーブルを扱うコントローラだったらusers_controller.rbとなる
というわけで、app/controllers/users_controller.rbを作成します(BlogsControllerは後回しとします)。
下以外のメソッドを使っていけないわけじゃありませんが、通常は次のようなメソッドを作成します(scaffoldで作成するとこれらができます)。
controllerの中にすでに定義されているメソッド
- public
-
index
これはレコードの一覧を表示させるメソッド。
-
show
1つのレコードの値を表示させるメソッド。
-
new
新しいレコードを登録するためのフォームを表示するメソッド(登録するのは次の次のメソッド)。
-
edit
レコードの編集するためのフォームを表示するメソッド(値はフィールドにセットされています)。
-
create
newで開いたフォームの値を元にレコードを作成するメソッド。
-
update
editで表示されたフォームに対して何らかの値を修正して、それらの変更を更新するメソッド。
-
destroy
レコードをIDを指定して削除するメソッド。
-
- private
-
set_user
編集の時(edit)やデータの表示(show)等にidからレコードを拾ってBlogモデルクラスのインスタンスにレコードをセットするメソッド。
-
user_params
Strong Parameterといって、リクエストの値の中にシステムが要求している以外のフィールドとかないかチェックしているメソッド(今回は詳細は避けます)。
-
さて、このusersテーブルには管理者の情報を登録することになるわけですが、だいたい、scaffoldを使ってcontrollerを作成すると次のようになります。
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
def index
@users = User.all
end
def show
end
def new
@user = User.new
end
def edit
end
def create
@user = User.new(user_params)
respond_to do |format|
if @user.save
format.html { redirect_to @user, notice: 'User was successfully created.' }
else
format.html { render :new }
end
end
end
def update
respond_to do |format|
if @user.update(user_params)
format.html { redirect_to @user, notice: 'User was successfully updated.' }
else
format.html { render :edit }
end
end
end
def destroy
@user.destroy
respond_to do |format|
format.html { redirect_to user_url, notice: 'User was successfully destroyed.' }
end
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:username, :email, :password)
end
end
がっ
今回は以下のようにindexメソッドのみ(しかも空の)を作成しておきます。
class UsersController < ApplicationController
def index
end
end
また、AjaxでRailsにアクセスし、JSONを返してもらうことになるのですが、これは上のusers_controller.rbとは区別しようと思っているので
app/controllers/api/users_controller.rb
1
2
3
4
5
6
class Api::UsersController < ApplicationController
def index
@users = User.all
render 'index', formats: 'json', handlers: 'jbuilder'
end
end
を作成します。そしてjbuilderでJSONを整形するために(その必要もないぐらい単純なデータ構造ですけれど)、
app/views/api/users/index.json.jbuilder
1
2
3
4
5
6
json.array! @users do |user|
json.id user.id
json.email user.email
json.password user.password
json.created_at user.created_at
end
を作成します。
model
usersテーブルに対応したModelを作成します(多のモデルは必要に応じて順次作成します)。ただ、バリデーションもしない単純なものを作成します。
app/models/user.rbに下の内容(たったの2行)を書き込みます。
class User < ActiveRecord::Base
end
view
viewは少々面倒です(scaffoldで作成される程度のものなら、手作業で数は若干多いという程度ですが、その若干数が多いという点に加え、ある程度はしっかりデザインしたいし、クライアントサイドのバリデーションをもしたいとなると面倒だ、という感じです)。ただ、最初は少しだけにして、段々複雑にします。
通常ですと、次のようなファイルを作成することになります。
-
_form.html.erb
edit.html.erbとnew.html.erbからインクルードされている、htmlのformが書かれている(後述)。
-
index.html.erb
レコード(Blog)の一覧表示するもの。
-
new.html.erb
ほとんど、_form.html.erbを読み込んでいるだけ。
-
edit.html.erb
ほとんど、_form.html.erbを読み込んでいるだけ。
-
show.html.erb
レコードの値の表示するもの。
ただし、とりあえず、今回はすべてをシングルページアプリケーションにするわけじゃありませんが、このユーザ部分はそうしようと考えているのでindex.html.erbのみを作成します。
今回はVue.jsのさっきとは違い、Single File Componentではない、もう少し単純な方法でユーザの一覧を表示させ、あとで変更します。
app/views/users/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div id="page-header" class="page-header">
<h4>管理者一覧</h4>
</div>
<table id="user_list" class='table-striped table-condensed table-bordered'
style="table-layout: fixed;" align="center">
<thead>
<tr>
<th v-for="(value, key) in columns">
{{ value }}
</th>
<th colspan="2"><button class="btn btn-xs btn-primary" id="new" v-on:click = "clear()">新規管理者</button></th>
</tr>
</thead>
<tbody>
<tr v-for="user in users">
<td v-for="(value, key) in columns">
{{ user[key] }}
</td>
<td align="center"><button class="btn btn-xs btn-info" id="edit" v-on:click = "insert(user)">編集</button></td>
<td align="center"><button class="btn btn-xs btn-warn" id="delete">削除</button></td>
</tr>
</tbody>
</table>
</div>
<%= javascript_pack_tag 'users_controller' %>
- 4行目のidであるuser_listは後で説明するvuejsの対象とするための印です。
- 8行目はvuejsのfor文になります。colums(キーがフィールド名の英語、値がフィールド名の日本語が入っている)というオブジェクトを回して、テーブルのthを作成しています。
- 9行目は2のループで拾ったvalueを変数展開しています。
- 15行目はjsonの配列であるusersを2同様、ループさせています。
- 16行目は、userがオブジェクトなので、keyを使って取り出しています(keyが後ろなので注意!)。
- 最後の行はusers_controller.jsを読み込むように、という指示になります(この後作成します)。
- table-stripedはBootstarpのクラス名ですが、この後すぐにBootstrapを使えるようにします。
- 上記の新規作成や編集ボタンなどはまだ実装しないので、クリックするとエラーになります・汗。
app/javascript/pack/users_controller.js
今回のjavascriptの中心はこれです。
app/javascript/pack/users_controller.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue/dist/vue.esm'
import axios from 'axios'
window.addEventListener("DOMContentLoaded", () => {
var ul = new Vue({
el: '#user_list',
data: {
users: [],
columns: {id: 'id', email: 'メールアドレス', password: 'パスワード', created_at: '作成日'}
},
created: function() {
axios.get('/api/users/')
.then((res) => {
console.log(res)
this.$data.users = res.data
})
}
})
})
- 1行目のインポート先がvueじゃなく(前回)、vue/dist/vue.esmとなっているのは、Single File Componentの場合はすぐに実行できる形にコンパイルされるのですが、そうでない場合はブラウザで実行されたときにJITコンパイラが走らないといけないらしく、そのためimport先を完全ビルドする方(30%ほど時間がかかるらしい)に換えています(結局Single File Componentの方が良いのでしょうね。後で修正しますw)。
- 上の方でインストールしておいた、Ajax通信するためのライブラリーです。一応Vue.jsの推奨らしいです。
- 5行目のelでマッピングするIDを指定しています。
- 7行目:dataプロパティのusersを用意し、サーバから取得するデータの格納先としています。
- 8行目:dataプロパティのcolumnsはDBのフィールド名(Railsから帰ってくるJSONのキーになっている)がキーで、値の方が表示のための日本語のフィールド名を格納しています。
- 10行目:createdプロパティは、コンポーネントが作成終了後に実行されるメソッドを登録しておきます。
アロー関数式
閑話休題
上にも若干出てきていますが、少しだけアロー関数にも触れておきます(その程度は知っているという場合には、抜かしてくださいw。巷には多くの解説があるので、僕のメモ書き的なので)。
typescriptではだいぶ前から使えていた気がしますが、もちろん、ES2016からはJavaScriptでも使えるようになっています(まあ、結局Babelで変換しているわけですが、現段階では)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var variable = "global variable";
function func() {
console.log(variable);
}
func();
func2 = function() {
console.log(variable);
};
func2();
func3 = () => {
console.log(variable);
};
func3();
- 3行目の関数宣言は今回の話には関係ありません。
- 8行目のような関数式の話です。
- 上の例のようにthisが出てこない場合は、単なる書き方の問題といえます。つまり、8〜10行目と14〜16行目は全く同じと言えます。
- ただし、若干アロー関数の書き方にはバリエーションがあります。
- var func = (x) => {/* 関数本体 */}; // ←普通
- var func = x => {/* 関数本体 */}; // ←引数が1つの場合()が省略できる
- var func = (x, y) => x + y; // ←var func = (x, y) => {return x + y;};を置き換えたもの
-
var func = (x, y) => ({result: x + y}); // ←オブジェクトを返す場合、外側に括弧が必要。
もちろん、var func2 = (x, y) => {return {result: x + y};}のように、普通に{}で囲んでもOKなので、無理に()にする理由はないかも。
- var func = x => x ** 2; // ←上記2と3の応用でこんな書き方もOK
さて、単なるシュガーシンタックスかというと違います。上にも書いたようにthisが出てくると違ってきます。
ちょいと長いスクリプトですが、中身は単純です。console.logでthis.variableを6回出力していますが、さて、何が出力されるでしょうか?
- ※
- ただし、これはnodeによる実行と、ブラウザの場合とでは出力が違ってきます。あくまでも今回はブラウザの場合という限定としてください。これはnodeにはグローバルスコープというのがないためです。Chromeだとコンソールを立ち上げてペースとして、リターンすれば実行できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var variable = 'global variable';
var outer = {
variable: "outer variable",
func: function() {
console.log(this.variable);
},
getFunc: {
variable: "inner variable",
func: function() {
var variable = "inner inner variable";
console.log(this.variable);
var func = function() {
console.log(this.variable);
};
return func;
}
}
};
outer.func();
var f = outer.getFunc.func();
f();
var outer2 = {
variable: "outer variable",
func: () => {
console.log(this.variable);
},
getFunc: {
variable: "inner variable",
func: () => {
var variable = "inner inner variable";
console.log(this.variable);
var func = () => {
console.log(this.variable);
};
return func;
}
}
};
outer2.func();
var g = outer2.getFunc.func();
g();
- JavaScriptはthisが何を指すかが、そのコンテキストによって変わってきます。
- 20行目で最初のメソッドの実行を行っていますが、それがouter.funcメソッドで、6行目のthis.variableが出力されます。この時のthisはouterオブジェクトのvariableつまりouter objectが出力されます。
- 21行目のouter.getFunc.func();によって、12行目のthis.variableが出力されますが、すぐ上のvariableじゃなく、9行目のinner variableが出力されます(もし、12行目にthisがないとinner inner variableが出力されます→わかりずらっ)。
- 21行目で返ってきた関数を22行目で実行したら、そのコンテキストではグローバルなもの、つまりglobal variableが出力されます。
- アロー関数の場合、全てがglobal variableと出力されます。
- 5の理由はアロー関数は、1〜5のように、その時その時でthisが変わっていくのに対して、そのアロー関数で定義する際にその関数が含まれるオブジェクトのthisを関数内のthisに束縛します(要するに、関数の外のオブジェクトのthisを定義する関数の中のthisにコピーする感じ)。なので、一番外側のthisがリレーのように束縛していった感じになっています。というわけで、ずっとglobal variableが出力されるわけです。
さて、なんでこんな話になったかというと、上にあった次のようなスクリプトのfunctionに注意が必要だからです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
window.addEventListener("DOMContentLoaded", () => {
var ul = new Vue({
el: '#user_list',
data: {
users: [],
columns: {id: 'id', email: 'メールアドレス', password: 'パスワード', created_at: '作成日'}
},
created: function() {
axios.get('/api/users/')
.then((res) => {
console.log(res)
this.$data.users = res.data
})
}
})
})
8行目のものをアロー関数にしてしまうと、1行目のアロー関数、8行目のアロー関数とリレーされるので、途中でnewしているVueのインスタンを指すものがなくなってしまいます。つまり、this.$data.usersはvueインスタンスのデータにアクセスしたいのに、でwindow.addEventListenerの外側のオブジェクトを指していることになるからです。
Bootstrapの導入
Bootstrapを使ってデザインしていこうと考えているので、以下のことを行います。
まずはターミナルで
$ yarn add bootstrap-sass
を実行します。
app/javascript/src/application.scssの中に、次の1行を書き込みます。
@import "~bootstrap-sass/assets/stylesheets/_bootstrap.scss";
上の方で書いたpタグの部分は削除しておきましょうw
application.html.erb
app/views/layouts/application.html.erbは、特に指定しない限り、通常すべてのページで読み込まれ、今後作成するnew.html.erbなどを<%= yield %>のところに読み込まれます。
そしてその<%= yield %>を<div class="container">
で囲みます(Bootstrapの作法にしたがってw)。
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Shinra</title>
<%= csrf_meta_tags %>
<%= javascript_pack_tag 'worldwide' %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
</head>
<body>
<div class="container">
<%= yield %>
</div>
</body>
</html>
config/routes.rb
config/routes.rb*
1
2
3
4
5
6
get "welcome/index"
root :to => 'welcome#index'
resources :users, only: %i(index)
namespace :api do
resources :users, only: %i(index)
end
のように書き込みます。
とりあえず、ここまでの確認
を実行し、http://localhost:3000/usersにブラウザでアクセスすると、次のように表示されればOKです。
もう少し応用を
さて、上までで管理者の一覧をぱらっと見せられるようになったわけですが、ここからは、そこから登録したり、更新したり、削除したりできるようにします。
概念的には次のような感じになります。
- 実際の外側は図にはありませんが、app/views/layouts/application.html.erbです。
- そこのyieldのところに、図のindex.html.erbが挿入されます。
-
index.html.erbの中に
<div id="users"></div>
があって、そこにUsers.vueがマッピングされます。また、ここでusers_controller.jsが読み込まれます。 - Users.vueからUserForm.vue(入力フォーム)とIndexUsers.vue(管理者一覧)がimportし、子コンポーネントとして登録します。
- 子供と親との会話等が課題になる予感w
まずは一覧表示をSingle File Component化
先ほどは、単一ファイルコンポーネントではなかったので、これを変更します。
app/views/users/index.html.erb
app/views/users/index.html.erbをシンプルに次のように書き換えます。
app/views/users/index.html.erb
1
2
3
4
5
6
<div id="page-header" class="page-header">
<h4>管理者管理</h4>
</div>
<div id="users_list"></div>
<%= javascript_pack_tag 'users_controller' %>
app/javascript/pack/index_users.vue
index.html.erbにあったものとusers_controller.jsにあったものをここに移してきます。
app/javascript/pack/index_users.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
<div>
<table id="user_list" class='table-striped table-condensed table-bordered' style="table-layout: fixed;" align="center">
<thead>
<tr>
<th v-for="(value, key) in columns">
</th>
<th colspan="2"><button class="btn btn-xs btn-primary" id="new" v-on:click = "clear()">新規管理者</button></th>
</tr>
</thead>
<tbody>
<tr v-for="user in users">
<td v-for="(value, key) in columns">
</td>
<td align="center"><button class="btn btn-xs btn-info" id="edit" v-on:click = "insert(user)">編集</button></td>
<td align="center"><button class="btn btn-xs btn-warn" id="delete">削除</button></td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import axios from 'axios'
export default {
data: () => {
return {
users: [],
columns: {id: 'id', email: 'メールアドレス', password: 'パスワード', created_at: '作成日'}
}
},
created: function () {
axios.get('/api/users/')
.then((res) => {
console.log(res)
this.$data.users = res.data
})
}
}
</script>
app/javascript/pack/users_controller.js
これを以下のように書き換えます。
app/javascript/pack/users_controller.js
1
2
3
4
5
6
7
8
import Vue from 'vue'
import UsersList from './index_users'
window.addEventListener("DOMContentLoaded", () => {
new Vue({
el: '#users_list',
render: h => h(UsersList)
})
})
- 今回はSingle File Componentなので、import Vue from ‘vue’に変更しています。
- この後作成するindex_users.vueをimportしています。
- elにusers_listを指定してrenderしています。
これで、先ほどと同様な画面が見えたらOKです。
JSONを読み込むWebpackのloaderのインストール
jsonをjavascript内でimportして、そのjsonの中に定数を定義しておくようなことをしたい(というか、私の場合いつもそうやっているので)ので、ローダをインストールします。
$ yarn add json-loader