2012年7月のJTPAギークサロン

JTPAギークサロン「青木淳氏とNode.js/Herokuを体験する」で使用したサンプルコードや環境設定手順などを掲載しておきます。

事前準備

node、npm、git、ssh、heroku、mysqlがローカル環境にインストールされていること(以下のコマンドでバージョンを確認)。

$ node --version
v0.8.2

$ npm --version
1.1.21

$ git --version
git version 1.7.5.4

$ ssh -V
OpenSSH_5.6p1, OpenSSL 0.9.8r 8 Feb 2011

$ heruku --version
heroku-gem/2.28.14 (x86_64-darwin11.2.0) ruby/1.9.2

$ mysql --version
mysql  Ver 14.14 Distrib 5.5.24, for osx10.7 (i386) using  EditLine wrapper

herokuにサインアップ済みであり、My Appsページにアクセスできること。また、My AccountページでBilling Infoが登録済みであること。

expressを使ってnodeを動かしてみる

npmを使ってexpressをインストールする。 最新の3.0系はherokuでサポートされていないため2.5系を明示的に指定する。

$ npm -g install express@2.5
$ express --version
2.5.11

expressでアプリの枠組みを作る。

$ express -s -t ejs jtpa-hackathon
$ cd jtpa-hackathon
$ npm install

ディレクトリ構成は以下のようになっている。

jtpa-hackathon/
	|-- app.js
	|-- node_module/
	|-- package.json
	|-- public/
	|   |-- images/
	|   |-- javascripts/
	|   `-- stylesheets/
	|       `-- style.css
	|── routes/
	|   `-- index.js
	`-- views/
		`-- index.ejs

nodeを起動する。

$ node app.js

ブラウザからhttp://localhost:3000/にアクセスすると、Welcome to Expressというページが表示される。

herokuにアプリをデプロイする

ターミナルからherokuにログインする。

$ heroku login
Enter your Heroku credentials.
Email: account@yourdomain.com
Password (typing will be hidden):
Authentication successful.

heroku上に新しいアプリケーションを作成する。

$ heroku create
Creating falling-galaxy-1006... done, stack is cedar
http://falling-galaxy-1006.herokuapp.com/ | git@heroku.com:falling-galaxy-1006.git
Git remote heroku added

herokuのMy Appsページに新しいアプリケーションが追加されていることを確認する。 また、ブラウザからhttp://<herokuアプリ名>.herokuapp.comにアクセスして、Heroku | Welcome to your new app!のページが表示されることを確認する。

heroku上でブートストラップとなるProcfileを作る。

$ echo 'web: node app.js' > Procfile

nodeの待受ポート番号の指定で$PORT環境変数を優先させるようにapp.jsを修正する。 また、package.jsonでnodeのバージョンを明記する。 (差分はこちらを参照)

ローカルにgitリポジトリを作成する。

$ pwd
~/jtpa-hackathon  # カレントディレクトリを確認
$ git init

.gitignorenode_modules/を追記する。

$ echo 'node_modules/' >> .gitignore

ローカルのgitリポジトリに既存のファイルをコミットする。

$ git add .
$ git commit -m 'Initial commit'

ローカルのgitリポジトリにコミットしたファイルを、heroku上のgitリポジトリにプッシュする。

$ git push heroku master
(略)
-----> Launching... done, v3
       http://falling-galaxy-1006.herokuapp.com deployed to Heroku

To git@heroku.com:falling-galaxy-1006.git
* [new branch]      master -> master

ブラウザからhttp://<herokuアプリ名>.herokuapp.comにアクセスして、Welcome to Expressのページが表示されることを確認する。

ejsでビュースクリプトを書く

views/index.ejsviews/layout.ejsにHTMLを書き加えて、Welcome to Expressのページがどう変化するかを見てみる。

views/index.ejs

<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
<p>Hello, JTPA!</p>

views/layout.ejs内の<%- body %>の部分にviews/index.ejsの内容が組み込まれるようになっている。 ヘッダーやフッターなど各ページに共通する部分はレイアウトスクリプト(views/layout.ejs)に、リクエストによって動的に変わる部分はビュースクリプト(views/index.ejs)にコーディングする。 ビュースクリプト内では<%= variable %>と記述すると変数の内容が出力される。

次に、自前でビュースクリプトを作ってみる。 まずコントローラをroutes/index.jsに追加する。

routes/index.js

exports.mytemplate = function(req, res) {
	res.render('mytemplate', {
		title: 'My Template'
	  , param1: req.params.p // URLから取得
	  , param2: 'hard coded'
	});
};

res.render()の第1引数でビュースクリプトを指定し、第2引数に渡しているオブジェクトリテラルが、ビュースクリプト内で変数として参照可能となる。

続いて、/mytemplateへのリクエストを追加したコントローラにルーティングするコードをapp.jsに追加する。

app.js

app.get('/', routes.index);
app.get('/mytemplate/:p', routes.mytemplate); // 追加

app.get()の第1引数に含まれる:pは、URLの該当箇所にあるパラメータを保持するプレースホルダであり、routes.mytemplate内でreq.params.pとして参照されている。

最後にビュースクリプトviews/mytemplate.ejsを作成する。

views/mytemplate.ejs

<h1>Param1: <%= param1 %></h1>
<h2>Param2: <%= param2 %></h2>

nodeを起動して、ブラウザからhttp://localhost:3000/mytemplate/1にアクセスしてみる。 (ここまでの差分はこちらを参照)

JSONを返すAPIを作る

ビュースクリプトを使わず、データをJSONで返すようなAPIを作ってみる。

まずコントローラをroutes/index.jsに追加する。

routes/index.js

exports.jsonapi = function(req, res) {
	var json = {
		name: 'obama'
	  , job: 'president'
	};
	res.send(json);
};

app.jsでルーティングする。

app.js

app.get('/', routes.index);
app.get('/mytemplate/:p', routes.mytemplate);
app.get('/jsonapi', routes.jsonapi); // 追加

nodeを起動して、ターミナルからcurlコマンドでhttp://localhost:3000/jsonapiにアクセスしてみる。

$ curl -v http://localhost:3000/jsonapi
> GET /jsonapi HTTP/1.1
> User-Agent: curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:3000
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 34
< Connection: keep-alive
<
{"name":"obama","job":"president"}

res.send()にオブジェクトリテラルを渡すと、nodeはContent-Typeヘッダをapplication/jsonにセットして、JSON形式でレスポンスボディを返すようになっている。

次に、クエリーストリングで指定されたパラメータをレスポンスに含めるようにAPIを改造する。

routes/index.js

exports.jsonapi = function(req, res) {
	var json = {
		name: 'obama'
	  , job: 'president'
	  , salary: req.query.salary || 'unknown'
	};
	res.send(json);
};

curlコマンドでhttp://localhost:3000/jsonapi?salary=100にアクセスすると、{"name":"obama","job":"president","salary":"100"}というJSONが返ってくる。 http://localhost:3000/jsonapiにアクセスすると、{"name":"obama","job":"president","salary":"unknown"}というJSONが返ってくる。 (ここまでの差分はこちらを参照)

セッションを扱う

セッションはユーザーのログイン状態を追跡するためなどに使われる。 ここでは簡単なセッションへの保存、読取を行なってみる。

まずコントローラに2つのメソッドを追加する。

routes/index.js

exports.set_session = function(req, res){
	req.session.value = req.params.value;
	res.send('value = '+req.params.value);
};

exports.get_session = function(req, res){
	res.send('session.value = '+req.session.value);
}

app.jsでルーティングする。

app.js

app.get('/', routes.index);
app.get('/mytemplate/:p', routes.mytemplate);
app.get('/jsonapi', routes.jsonapi);
app.get('/set/:value', routes.set_session); // 追加
app.get('/get', routes.get_session); // 追加

ブラウザでhttp://localhost:3000/getにアクセスするとsession.value = undefinedと表示される。 続いてhttp://localhost:3000/set/111にアクセスするとvalue = 111と表示され、セッションに111という値が保存される。 再びhttp://localhost:3000/getにアクセスするとsession.value = 111と表示される。 (ここまでの差分はこちらを参照)

herokuのDaaSを使う

heroku上のアプリケーションにデータベースのアドオンClearDB MySQL Databaseを追加して、DaaS(Database as a Service)としてローカルのnodeから利用する。

$ heroku addon:add cleardb
Adding cleardb to falling-galaxy-1006... done, v4 (free)
Use `heroku addons:docs cleardb` to view documentation

$ heroku addons
=== falling-galaxy-1006 Configured Add-ons
cleardb:ignite

$ heroku config

heroku configを実行した際にCLEARDB_DATABSE_URLと表示されたものが、ClearDBへのDSN(データソース名)となっており、以下の構成をしている。

mysql://{username}:{password}@{hostname}/{dbname}?reconnect=true

mysqlshowコマンドでローカルからClearDBに接続できることを確認する。

$ mysqlshow -u {username} -p{password} -h {hostname} {dbname}
Database: heroku_xxxxxxxxxxxxxxx
+--------+
| Tables |
+--------+
+--------+

まずテーブル定義と初期データを挿入するSQLスクリプトtest.sqlを作る。

test.sql

CREATE TABLE IF NOT EXISTS table1
(
	id INTEGER NOT NULL UNIQUE,
	name VARCHAR(50),
	age INTEGER
);

INSERT INTO table1 VALUES(1, 'Iron Man', 20);
INSERT INTO table1 VALUES(2, 'Black Widow', 21);
INSERT INTO table1 VALUES(3, 'Captain America', 22);

ClearDBに対してSQLスクリプトを実行する。

$ mysql -u {username} -p{password} -h {hostname} {dbname} < test.sql

table1テーブルが作られたことを確認する。

$ mysqlshow -u {username} -p{password} -h {hostname} {dbname}
Database: heroku_xxxxxxxxxxxxxxx
+--------+
| Tables |
+--------+
| table1 |
+--------+

$ mysql -u {username} -p{password} -h {hostname} {dbname} -e 'SELECT * FROM table1'
+----+-----------------+------+
| id | name            | age  |
+----+-----------------+------+
|  1 | Iron Man        |   20 |
|  2 | Black Widow     |   21 |
|  3 | Captain America |   22 |
+----+-----------------+------+

nodeからClearDBを操作する

mysqlモジュールを使って、nodeからheroku上のClearDBにアクセスしてみる。

まずpackage.jsonを編集して、mysqlモジュールをdependenciesに追加する。 node-mysqlの最新バージョンである2.0系は使わず、0.9系を明示的に指定する。

package.json

{
	"name": "jtpa-hackathon"
  , "version": "0.0.1"
  , "private": true
  , "engines": {
	  "node": "0.8.x"
	, "npm": "1.1.x"
  }
  , "dependencies": {
	  "express": "2.5.11"
	, "ejs": ">= 0.0.1"
	, "mysql": ">= 0.9.5" // 追加
  }
}

編集が終わったら、npm installを実行してmysqlモジュールをインストールする。

続いて、データベースを操作するコントローラをroutes/index.jsに実装していく。 はじめにmysqlモジュールを使ったクライントをローカル変数として作成してDSNの設定を行う。

routes/index.js

var mysql = require('mysql').createClient();
mysql.host = 'us-cdbr-east.cleardb.com';
mysql.user = 'xxxxxxxxxxxxxx';
mysql.password = 'xxxxxxxx';
mysql.database = 'heroku_xxxxxxxxxxxxxxx'

データベースからデータを取得するコントローラを追加して、app.jsでルーティングする。

routes/index.js

exports.db_select = function(req, res){
  mysql.query('SELECT * FROM table1 WHERE id = ?', [req.params.id],
	function(err, result, fields) {
	  if(err) {
		res.send(500);
		throw err;
	  }

	  if(result.length) {
		var content = []
		  , record = result.shift();

		content.push('id: '+record.id);
		content.push('name: '+record.name);
		content.push('age: '+record.age);
		res.send(content.join('<br />'));
	  }
	  else {
		res.send(404);
	  }
	}
  );
};

app.js

app.get('/set/:value', routes.set_session);
app.get('/get', routes.get_session);
app.get('/db_select/:id', routes.db_select); // 追加

ブラウザ、またはcurlコマンドで、http://localhost:3000/db_select/1にアクセスしてみる。 (ここまでの差分はこちらを参照)

次にデータベースにデータを挿入するコントローラを追加して、app.jsでルーティングする。

routes/index.js

exports.db_insert = function(req, res){
  var id = req.query.id
	, name = req.query.name
	, age = req.query.age;

  mysql.query('INSERT INTO table1 VALUES(?, ?, ?)', [id, name, age],
	function(err, result, fields) {
	  if(err) {
		res.send(500);
		throw err;
	  }

	  var content = []
	  content.push('id: '+id);
	  content.push('name: '+name);
	  content.push('age: '+age);
	  res.send('<h2>New record</h2>'+content.join('<br />'));
	}
  );
};

app.js

app.get('/set/:value', routes.set_session);
app.get('/get', routes.get_session);
app.get('/db_select/:id', routes.db_select);
app.get('/db_insert', routes.db_insert); // 追加

ブラウザ、またはcurlコマンドで、http://localhost:3000/db_insert?id=4&name=Hulk&age=30にアクセスしてみる。 (ここまでの差分はこちらを参照)

おつかれさまでした