はじめに
Ruby on Railsを通してフロントエンドも何となくわかった気になっていたJavaScript初心者がイマドキのフレームワークを使い始めるまでに辿ったステップをまとめました。
前提となる今のスキルレベル
Ruby on Rilsは1年間やってきて基本的なことはわかるレベルだが、JavaScriptは全くの初心者。
フロントエンドはBootstrapでできる範囲で済ませ、Rails内で使うJavaScriptといえば<form>
タグにremote='true'
を設定してあれこれするだけでAjaxの処理が余裕でできる〜!で終わっていました。
この状態でJavaScriptを自分のスキルに入れていました。
もちろんコードはイチからは書けず、検索したものをコピペで済ます日々。
JSON,DOM...なんとなくはわかったつもりでいるけど具体的に何、そしてどう書くの?
最新のフレームワークに挑戦したいけど、どうやってRailsの環境で使うの?
山が高すぎて、、どこから登ろうかという状態でした。
そこから一歩ずつ進んでいき、約1か月強でRiot.jsを使い始めるまでの記録です。
さてどこから勉強しようか?
大まかに下記のようなステップで進めていきました
Step1. まずは素のJavaScriptを書いてみる
Step2. jQueryを使ってみる
Step3. 処理の用途別にファイルを分割する & デザインパターンを使ってみる
Step4. Riot.jsを使ってみる
3あたりがお腹いっぱいになります(*´3`)-з
Step1. まずは素のJavaScriptを書いてみる
常識なのだろうけど全くわからなかったのが JavaScriptって非同期処理なんだよという話
Railsの場合、一般的にソースコードの上から下へ順に処理を実行するののだが、JavaScriptの場合ユーザーからのアクション(イベント)に応じて処理を実行するため、どうもそうではないらしい。
その場合、1つの処理ならば問題ないが、複数の処理を実行させる場合、前の処理が終わった後に実行という(いわゆるコールバック)という記述の仕方になるので、工夫しないと入れ子が半端ないコールバック地獄というものに陥るらしい。はにゃー。
ということで幾つかのコールバック処理を書いてみて、非同期処理の流れをつかむことに。
Step1-1. コールバックの基本的な関数定義、呼び出しのサンプル
Railsの環境でviewを作って、、というはまだ仰々しかったので、使いこなしている人にアドバイスをもらいNode.jsの環境をnodebrewを使って構築し、
$ node app/assets/javascripts/sample.js(<= ファイルを指定)
コマンドで実行
/**
* Sample1 (参照 http://qiita.com/nekoneko-wanwan/items/f6979f687246ba089a35)
*/
// 1. 関数の定義
var sayYellow = function(){
console.log('黄色');
};
var sayName = function(myName){
console.log(myName + 'の好きな果物です');
}
// 2. 呼び出しの定義
var sayBanana = function(aCallback){
console.log('バナナは');
aCallback();
};
// 3. 実行
sayBanana(sayYellow); // => バナナは黄色
// 引数に関数を渡して実行
sayBanana(function(){
sayName('Hana'); // => バナナはHanaの好きな果物です
});
Step1-2. setTimeoutメソッドを使ったコールバック処理のサンプル
// Sample_1: setTimeout基本の使い方
var example = function(){
console.log('5秒後に実行!');
};
var executeConsolelog = function(_callback, _time){
setTimeout(_callback, _time);
};
// 実行
executeConsolelog(example, 5000); // => 5秒後にexampleが表示される
/** Sample_2: setTimeoutの第1引数に無名関数のコールバックを渡す
* 参照:http://easyramble.com/javascript-settimeout-callback-args.html
*/
var greet = 'Hello!',
name = 'JavaScript';
// 実行
setTimeout( function() {
showLog(greet, name)
}, 3000);
function showLog(a, b) { // 関数宣言なので後に定義してても巻き上げ実行される
console.log(a + b);
};
/** Sample_3: setTimeoutとclearTimeoutの使い方
* 参照 https://techacademy.jp/magazine/5541
*/
var cnt = 0;
var countUp = function(){
console.log(cnt++);
};
// 実行
setTimeout(countUp, 1000); // => 0, 1秒後に1が表示
var i = 0;
var countUpTill5 = function(){
console.log(i++);
var id = setTimeout(countUpTill5, 1000);
if(i > 5) {
clearTimeout(id);
}
};
// 実行
countUpTill5(); // => 0, 1秒ごとに1-5まで表示
文末のセミコロンが抜けてるとか発生するので、、
ここでJavaScriptの基本的な文法チェックをしてくれるのエディタのお便利機能を導入。
AtomユーザーなのでESLintを入れることに。
もっと細かい指摘もカスタマイズできるらしい。
とにかくデバッグしてみる
Google Chromeのデベロッパーツール(検証)でのデバッグを利用し始めました。
ソースファイルにブレイクポイント打ってステップ実行したり、console.logで変数を表示してみたり無限大です!
Step2. jQueryを使ってみる
jQueryを導入して、下記のステップでサンプルを作ってみた。
ポイントは一気に書きあげようとしないこと。少し書いては、実行して、また少し書いての繰り返しを行った方がはまりづらいのでベターということをハマった末実感しました。
Step2-1. 静的なJSONデータを使って、Ajaxを実行
GitHubのgistで静的なJSONファイルを用意し、読み込んだ内容をHTML側に表示するというサンプルを作成。
下記ステップで少しずつバージョンアップしていった。
- ページが読み込まれたらJSONのデータを取得してconsole.logで内容を表示してみる
- ページ読み込み時でなく、ユーザーからのイベント操作で1の処理を実行してみる
- 1の表示処理をconsole.logからidセレクターを設定した要素内に表示してみる
- 処理が終わった後に別の処置を呼び出してみる
[{
"id": 1,
"firstname": "Taro",
"lastname": "Suzuki",
"fruitsId": 2
}, {
"id": 2,
"firstname": "Hanako",
"lastname": "Tanaka",
"fruitsId": 1
}]
$(function(){
var jsonFilePath = 'https://gist.githubusercontent.com/xxx/userProfile.json'; // gistのJSONファイルへのpath
$("#showConsoleLog").on("click", function () {
$.ajax({
url: jsonFilePath, // 通信するurl(必須)
type: "GET", // POST or GET(デフォルトGET)
dataType: "json", // 受信時に期待するデータのタイプ
success: function (items) { // 成功時に呼ばれるコールバック関数を指定(itemsはサーバから受信したJSONデータ)
var i, max;
for (i = 0, max = items.length; i < max; i++){
console.log("こんにちは、" + items[i].firstname + " " + items[i].lastname + "さん");
}
}
});
});
Step2-2. DBから取得したJSONデータを使って、Ajaxを実行
- Railsでmodelからデータを取得し、JSON形式でview表示する(APIを自作する)
- 1のJSONデータをJavaScriptで表示
具体的なサンプルとして
「都道府県一覧のjsonデータを返すAPIを自作する」をやってみます
やったこと
scaffoldでapi/
以下にアプリ名Prefecture
でcontroller
, model
, migration
を作成し、修正。
ルーティングも追加。
ポイント
-
model
に静的なデータを保存する場合、SQLのcreate文を走らせなくても、rubyの機能を使い、model
ファイルへのallメソッド作成で対応した。active_hash
なども使えそうだけど極力Gemだけに頼らない方法で実装したかったので。 -
viewをJSON形式で表示したい場合、jbuilderを使うと便利だった。
備考
一覧を表示するだけであればindexアクションのみで良いが、次のステップで詳細表示が必要な場合を加味してshowも記載してみました。model
のfind
メソッド、controller
のshow
アクション、ルーティング定義、show
用のjbuilder
ファイルなど。
class CreatePrefectures < ActiveRecord::Migration
def change
create_table :prefectures do |t|
t.string :name
t.string :furigana
t.timestamps null: false
end
end
end
class Prefecture < ActiveRecord::Base
class << self
def all
[
{id: 1, name: '北海道', furigana: 'ほっかいどう'},
{id: 2, name: '青森県', furigana: 'あおもりけん'},
{id: 3, name: '岩手県', furigana: 'いわてけん' },
{id: 4, name: '宮城県', furigana: 'みやぎけん' },
{id: 5, name: '秋田県', furigana: 'あきたけん' }
] //とりあえず5件のデータを格納
end
def find(id)
self.all.find{ |item| item[:id] == id.to_i }
end
end
end
class Api::PrefecturesController < Api::ApplicationController
def index
@prefectures = Prefecture.all
end
def show
@prefecture = Prefecture.find_by_id(params[:id])
end
end
Rails.application.routes.draw do
(省略)
namespace :api, { format: 'json' } do
resources :prefectures, only: [:index, :show]
end
end
json.prefectures do
json.array! @prefectures do |prefecture|
json.id prefecture[:id]
json.name prefecture[:name]
json.furigana prefecture[:furigana]
json.towns prefecture[:towns] do |town|
json.id town[:id]
json.name town[:name]
end
end
end
json.prefecture do
json.id @prefecture[:id]
json.name @prefecture[:name]
json.furigana @prefecture[:furigana]
end
Step3. 処理の用途別にファイルを分割する & デザインパターンを使ってみる
ここで急にグイッとレベルが上がります。ちょっと詰め込みすぎ感ありますが、進みます(・ェ・)/
jQueryを使ってだいぶスッキリしてきたが、処理が増えることを考えて、デザインパターンを使った書き方に書き換える。
これを使うことで、Aの処理が終わったらBを実行するというコールバックの実行が複数件あってもスマートにかける。処理と処理の関係が密結合でなく疎結合なため、処理の内容に変更が生じた場合の修正範囲の最小化や、再利用性が高いらしい。
今回は、Observerパターンを基にしたPublishSubscribeパターンを導入
Rails & JavaScriptで実装する場合のイメージはこちらが図解されていてわかりやすい!
作成したサンプルファイルと用途はこちらです。
ファイル名 | 用途 |
---|---|
prefectureView.js | ユーザーのボタンクリックイベントでModelを呼び出す |
prefectureModel.js | DBと通信し(Ajax)、JSONデータを取得してくる |
prefectureList.js | JSONデータを受け取った後にDOMへ描画する |
app.js | prefectureView, PrefectureListをページロード時に呼び出し |
その他のファイルたち
ファイル名 | 用途 |
---|---|
jquery-pubsub.js | jQueryを使ってPublishSubscribeパターンを実装できるファイル |
application.js | ファイルの読み込み順をケアする必要がある場合指定する |
jquery-pubsub.jsはjquery-tiny-pubsub からダウンロードしてきて利用
$(document).ready(function() {
var prefectureView = new PrefectureView();
var prefectureList = new PrefectureList();
});
var PrefectureView = (function(){
function PrefectureView(){
var indexBtn = $("#displayPrefectureBtn"),
_this = this;
_this.model = new PrefectureModel();
indexBtn.on('click', function(){
_this.index();
});
}
PrefectureView.prototype.index = function(){
this.model.index();
};
return PrefectureView;
}());
var PrefectureModel = (function() {
function PrefectureModel(){
var hostname = window.location.hostname,
protocol = window.location.protocol,
port = window.location.port;
this.baseURL = [protocol, '//', hostname, ':', port, '/api'].join('');
}
PrefectureModel.prototype.index = function(){
var params = {
url: this.baseURL + '/prefectures.json',
type: "GET",
action: "index"
};
return this._request(params);
};
PrefectureModel.prototype._request = function(params){
var deferred = $.ajax(params);
deferred.done(function(prefectureData){
prefectureData = deferred.responseJSON.prefectures;
$.publish('prefecture.loaded', [ prefectureData ]); // 通知
});
};
return PrefectureModel;
}());
var PrefectureList = (function(){
function PrefectureList(){
var _this = this;
$.subscribe('prefecture.loaded', function(e, prefectureData){ // あらかじめ購読
_this.draw(prefectureData);
});
}
PrefectureList.prototype.draw = function(prefectureData){
var i,
len = prefectureData.length,
prefectureElements = [],
resultArea = $("#resultArea");
for(i = 0; i < len; i++) {
prefectureElements.push(
'<li>' + prefectureData[i].id + ',' + prefectureData[i].name + '</li>'
);
}
resultArea.html('<ul id="prefectureList">' + prefectureElements.join('') + '</ul>');
};
return PrefectureList;
}());
/* jQuery Tiny Pub/Sub - v0.7 - 10/27/2011
* http://benalman.com/
* Copyright (c) 2011 "Cowboy" Ben Alman; Licensed MIT, GPL */
(function($) {
var o = $({});
$.subscribe = function() {
o.on.apply(o, arguments);
};
$.unsubscribe = function() {
o.off.apply(o, arguments);
};
$.publish = function() {
o.trigger.apply(o, arguments);
};
}(jQuery));
ファイルの読み込み順を調整
//= require jquery
//= require jquery_ujs
//= require twitter/bootstrap
//= require admin/jquery-pubsub
//= require admin/prefecture/prefectureView
//= require admin/prefecture/prefectureModel
//= require admin/prefecture/app
//= require_tree .
viewファイルには下記のみ記載しておく
<button id="displayPrefectureBtn">Display PrefectureSelect</button>
<div id="resultArea"></div>
Step4. Riot.jsを使ってみる
流行りのフレームワークはいろいろあるのだけど、初心者にも分かりやすいということで、Riot.jsを使うことにする。
公式ガイドにサンプルがあるので、まずこれを写経しながら少しずつ理解していっている状態です。
jQueryを使って作成した今までの処理をRiotを使って書き換えるイメージです。通信部分はjQueryのコードを利用する予定です。
まとめ
まだRiot.jsを使い始めたばかりですが、楽しいです。それも変に飛び級せずに基礎的なとこから一歩ずつ学んでいるからかなと思っています。
もやっとしていた部分がステップを進んで手を動かしていくうちに少しずつ腹落ちしてきている感があります。
ここまで来る際に何度もググったワード
-
関数の種類
関数宣言、無名関数、即時関数、、などの意味、書き方、その違い -
変数の種類
ローカル、グローバル変数、インスタンス(メンバ)変数。。
やっと最近インスタンス変数の使いどころがわかってきたところ -
クラス
-
コンストラクタ
-
thisの指すもの、どこで別の変数に入れる必要があるか
今もconsoleで検証しながら逐一確認している状態です。
皆さんもHappyなJavaScript Lifeを!