Node.js の MVCフレームワーク Sails.js が良さげなので少し試してみた

Sails.js はNode.jsのフルスタックMVCフレームワークです。

Railsを意識しているフレームワークのようで、Node.js環境において非常に簡単にWebアプリを構成できそうです。

記事作成時の環境です。

  • Node.js v0.10.1
  • npm npm v1.2.15

インストール

では早速導入

globalインストールでSails.jsをインストールします。

$ npm install -g sails@0.8.9
(2013.5.7 修正) npm install -g sails → npm install -g sails@0.8.9

バージョンを確認

$ sails -v
info: v0.8.9 
$ sails (-h)

debug: Welcome to Sails! (v0.8.9)  

info: Usage: sails <command>

sails lift                       Run this Sails app (in the current dir)
sails console                    Run this Sails app (in the current dir & in interactive mode.)
sails new <appName>              Create a new Sails project in the current dir
sails generate model <foo>       Generate api/models/Foo.js
sails generate controller <foo>  Generate api/controllers/FooController.js
sails version                    Get the current globally installed Sails version 

ふむ、公式が船なだけあってsailsですね(?)

sails lift
サーバを起動します
sails console
lift と同じくサーバ起動ですが、対話式(?)です
sails new
アプリ構成フィルダを生成します
sails generate model hoge
Model hoge を生成します
sails generate controller fuga
Controller fuga を生成します
sails version
globalインストールしたSailsのバージョン

では、sails new を使ってアプリのひな形を生成します。

$ sails new sails_oar

info: Generating Sails project (sails_oar)...  
debug: Generating app directory...  
debug: Generating directory public...  
debug: Generating directory public/images...  
debug: Generating directory assets...  
debug: Generating directory assets/js...  
debug: Generating directory assets/templates...  
debug: Generating directory assets/mixins...  
debug: Generating directory assets/styles...  
debug: Generating directory views...  
debug: Generating directory views/home...  
debug: Generating directory api...  
debug: Generating directory api/models...  
debug: Generating directory api/adapters...  
debug: Generating directory api/controllers...  
debug: Generating directory api/services...  
debug: Generating directory api/policies...  
debug: Generating directory config...  
debug: Generating directory config/locales...  
debug: Generating package.json...  
debug: Generating README.md...

モジュールをダウンロードします。

$ cd sails_oar && npm install

npm http GET https://registry.npmjs.org/sails/0.8.9
...(省略)

ダウンロードが完了したら、おもむろにサーバーを立ち上げます

$ sails lift

debug: Starting server in /path/to/sails_oar...  
   info  - socket.io started
debug: 
debug:   
debug:   
debug:   
debug:                    <|  
debug:                     |  
debug:                 \___ ___//  
debug: --___---___--___---___--___---___  
debug: --___---___--___---___--___---___  
debug:   
debug: Sails (v0.8.9)  
debug: Sails lifted on port 1337 in development mode.  
debug: 
debug: ( to see your app, visit: http://localhost:1337 )  

いよいよ船出ですね(?)

http://localhost:1337 にアクセスするとデフォルトの画面が表示されます。

Sails.js

サーバが起動していることが確認できたら、Ctrl+C でサーバを停止します。

ちなみに node app.js でも起動することができます。

ですので、supervisor などを使う場合は

supervisor app.js

とします。
(sails v0.8.895 では .app.js でした)

フォルダ構成

生成されたフォルダ構成を見ていきます。
sails_oar
├── api
├── assets
├── config
├── node_modules
├── public
├── views
├── .gitignore
├── app.js
├── package.json
└── README.md

MVCに対応するそれぞれのフォルダが配置してありますね。

Model
/api/modeles
View
/views
Controller
/api/controllers

Viewテンプレートは ejs のようです。いいですね。

では、公式のチュートリアルにしたがって色々試してみましょう。

  • https://github.com/balderdashy/sails

URLルーティング

api/controllersフォルダに存在する xxxxController.js ファイルとURLのひも付けが デフォルトで自動的に設定されています。

例えば、
http://localhost:1337/hello(/index) にアクセスする場合、api/controllers/HelloController.js のindexプロパティの処理を実行します。

http://localhost:1337/users/home にアクセスする場合、api/controllers/UsersController.js の homeプロパティの処理を実行します。

また、URLルーティング設定は routes.js で自由に定義することもできます。

$ vim config/routes.js
module.exports.routes = {

    // To route the home page to the "index" action of the "home" controller:
    '/' : {
        controller  : 'home'
}

ルートディレクトリへのアクセスで controller:home を割り当てていますね。

これで http://localhost:1337/ へのアクセスは http://localhost:1337/home/index の内容を表示することになります。

で、そのhomeコントローラーですが、そんなファイルは無いですね。。。

どうやら、コントローラ「 api/controllersにHomeController.js 」 がなくても、それに紐づくビュー 「 views/home/index.ejs 」 を読み込んでくれるんですね。

Controller

せっかくなので、Controllerを作ってサーバ側で処理を実行してみましょう。

Controllerを作成します。

$ sails generate controller home index
debug: Generating controller.js for Home...  

api/controllers/HomeController.js が生成されました。

アクション(上記では アクションは index)を指定せず

$ sails generate controller home

とすると、index アクション(プロパティ)が省略(HomeController.jsの定義のみ出力)されます。

さて、Controllerの中身を見てみましょう。

$ vim api/controllers/HomeController.js
/*---------------------
	:: Home 
	-> controller
---------------------*/
var HomeController = {

	// To trigger this action locally, visit: `http://localhost:port/home/index`
	index: function (req,res) {

		// This will render the view: /path/to/sails_oar/views/home/index.ejs
		res.view();

	}

};
module.exports = HomeController;

indexプロパティに hello という名前で文字列をセットしてViewへ渡します。

    index: function(req, res) {

		//res.view();	← 値を渡さず index.ejs の表示をするだけの場合
        res.view({ hello: 'Hello Sails!!!' });

    }
vim views/home/index.ejs
<div id="content">
    <h2>It works!</h2>

    <p><%= hello %></p> //←追加

    <p>You've successfully installed the Sails Framework.</p>

...(省略)

これで <%= hello %> の部分に HostController.js で割り当てた文字列が表示されるはずです。

では、確認してみましょう。サーバを起動していない場合は sails lift で起動してから http://localhost:1337/ へアクセスします。

Sails.js

表示されれば文字列はうまくViewへ引き渡されたことになります。

ちなみに、res.send( 'Hello Sails!!! );
とすると、Viewテンプレートを使わず Hello Sails!!! の文字列だけが表示されます。

View

Viewテンプレートの設定は、config/views.js にあります。

Jade等、他のテンプレートでも使えると思います(未検証)

$ vim config/views.js
module.exports = {
	viewEngine: 'ejs'
};

sails コマンドで出力された ejs ファイルは他に

404.ejs
404 Page Not Found
500.ejs
500 Internal Server Error
layout.ejs
Viewファイルの共通部分定義

の3つがあります。

404.ejsを見て見ます。

$ vim views/404.ejs
<h1>Page not found</h1>

<h2>The view you were trying to reach does not exist.</h2>


<style type="text/css">
...(省略)
</style>

..../home/index.ejs と ..../404.ejs には head の記述がありませんが、layout.ejs が 共通個所のテンプレートとなるファイルです。

$ vim views/layout.ejs
<!DOCTYPE html>
<html>
    <head>
        <title><%- title %></title>

        <!-- Viewport mobile tag for sensible mobile support -->
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

        <!-- JavaScript and stylesheets from your assets folder are included here -->
        <%- assets.css() %>
        <%- assets.js() %>
    </head>

    <body>
        <%- body %>

        <!-- Templates from your view path are included here -->
        <%- assets.templateLibrary() %>
    </body>
</html>
<%- title %>
タイトル。そのままですね。
<%- assets.css() %>
asetts/styles/**.css が読み込まれます。
<%- assets.js() %>
asetts/js/**.js が読み込まれます。
<%- body %>
各Viewの内容が入ります。
<%- assets.templateLibrary() %>
asetts/templates/**.js が読み込まれます。

Controller 側で titleプロパティをセットし、Viewへ渡すとタイトルなります。

404.ejsと500.ejsは <%- body %> の箇所に挿入されることになります。

Model

ModelはDBのスキーマを模したJSONファイルでDBの検索結果や、更新時にModelのオブジェクトを使うことでSQLを記述すること無くDBを操作することができます。

では、UserModelを生成します。

$ sails generate model User
warn: In order to serve the blueprint API for this model, you must now also generate an empty controller.  
warn: If you want this behavior, run 'sails generate controller User' to create a blank controller.  
debug: Generating model.js for User...  

Sails v0.8.895 では表示されていなかった警告が。。。
空のControllerを作らないといけないようなので作っておきます。

$ sails generate controller user
debug: Generating controller.js for User... 
$ vim api/models/User.js
/*---------------------
	:: User
	-> model
---------------------*/
module.exports = {

	attributes	: {

		// Simple attribute:
		// name: 'STRING',

		// Or for more flexibility:
		// phoneNumber: {
		//	type: 'STRING',
		//	defaultValue: '555-555-5555'
		// }
		
	}

};

Modelを操作してサーバサイド側でデータの出し入れを行うこともできますが、Sails.jsではModelを作成するとAPIとして機能させることができるので試してみたいと思います。

ファイルが生成されていることを確認したら、
http://localhost:1337/user/create へアクセスするか、
http://localhost:1337/user へPOSTアクセスします。

{
    "id": 1,
    "createdAt": "2013-04-29T05:02:13.441Z",
    "updatedAt": "2013-04-29T05:02:13.441Z"
}

もう一度、http://localhost:1337/user/create へアクセスします。

{
    "id": 2,
    "createdAt": "2013-04-29T05:14:59.805Z",
    "updatedAt": "2013-04-29T05:14:59.805Z"
}

idが連番で1つずつ増えていき、データが生成されているようです。

内容を確認しましょう。

http://localhost:1337/user/ へ(GETで)アクセスします。

[{
    "id": 1,
    "createdAt": "2013-04-29T05:02:13.441Z",
    "updatedAt": "2013-04-29T05:02:13.441Z"
},
{
    "id": 2,
    "createdAt": "2013-04-29T05:14:59.805Z",
    "updatedAt": "2013-04-29T05:14:59.805Z"
}]

id : 2 のユーザーを表示します。
http://localhost:1337/user/2
もしくは、http://localhost:1337/user/show/2 へアクセスします。

{
    "id": 2,
    "createdAt": "2013-04-29T05:14:59.805Z",
    "updatedAt": "2013-04-29T05:14:59.805Z"
}

削除をする場合は
http://localhost:1337/user/2 へDELETEアクセス
もしくは、http://localhost:1337/user/destroy/2 へGETアクセスします。

このようにデータベース管理の Create(生成)、Read(読み取り)、Update(更新)、Delete(削除)が行える機能があります。

これをCRUDと言いますが、下記の様なURLとメソッドでデータの出し入れが可能です。

全ユーザー一覧
http://localhost:1337/user
ユーザー表示
http://localhost:1337/user/1
http://localhost:1337/user/show/1
ユーザー作成
http://localhost:1337/user/create?name=Fisslewick
http://localhost:1337/user (POSTメソッド)
ユーザー更新
http://localhost:1337/user/update/1?name=Gordo
http://localhost:1337/user/1 (PUTメソッド)
ユーザー削除
http://localhost:1337/user/destroy/1
http://localhost:1337/user/1 (DELETEメソッド)

データベース接続設定

格納したJSONデータはどこに保存されているんでしょうか?

その設定は、adapter config に格納されています。

$ vim config/adapters.js
// Configure installed adapters
// If you define an attribute in your model definition, 
// it will override anything from this global config.
module.exports.adapters = {

	// If you leave the adapter config unspecified 
	// in a model definition, 'default' will be used.
	'default': 'disk',
	
	// In-memory adapter for DEVELOPMENT ONLY
	// (data is NOT preserved when the server shuts down)
	memory: {
		module: 'sails-dirty',
		inMemory: true
	},

	// Persistent adapter for DEVELOPMENT ONLY
	// (data IS preserved when the server shuts down)
	// PLEASE NOTE: disk adapter not compatible with node v0.10.0 currently 
	//				because of limitations in node-dirty
	//				See https://github.com/felixge/node-dirty/issues/34
	disk: {
		module: 'sails-dirty',
		filePath: './.tmp/dirty.db',
		inMemory: false
	},

	// MySQL is the world's most popular relational database.
	// Learn more: http://en.wikipedia.org/wiki/MySQL
	mysql: {
		module		: 'sails-mysql',
		host		: 'YOUR_MYSQL_SERVER_HOSTNAME_OR_IP_ADDRESS',
		user		: 'YOUR_MYSQL_USER',
		password	: 'YOUR_MYSQL_PASSWORD',
		database	: 'YOUR_MYSQL_DB'
	}
};

デフォルトで disk('default': 'disk') となっています。

diskの項目を見ると、filePath: './.tmp/dirty.db' となっています。

確認してみましょう。

$ vim ./.tmp/dirty.db

スキーマとデータそのものが書き出されていることが確認できます。

{"key":"waterline_schema_user","val":{"autoIncrement":1,"attributes":{"id":{"type":"INTEGER","autoIncrement":true,"default":"AUTO_INCREMENT","constraints":{"unique":true,"primaryKey":true}},"createdAt":{"type":"DATE","default":"NOW"},"updatedAt":{"type":"DATE","default":"NOW"}},"identity":"user","globalId":"User","module":"sails-dirty","filePath":"./.tmp/dirty.db","inMemory":false,"adapter":"sails-dirty","migrate":"alter","globalize":true,"autoPK":true,"autoUpdatedAt":true,"autoCreatedAt":true,"schema":{"id":{"type":"INTEGER","autoIncrement":true,"default":"AUTO_INCREMENT","constraints":{"unique":true,"primaryKey":true}},"createdAt":{"type":"DATE","default":"NOW"},"updatedAt":{"type":"DATE","default":"NOW"}}}}
{"key":"waterline_schema_user","val":{"autoIncrement":1,"attributes":{"id":{"type":"INTEGER","autoIncrement":true,"default":"AUTO_INCREMENT","constraints":{"unique":true,"primaryKey":true}},"createdAt":{"type":"DATE","default":"NOW"},"updatedAt":{"type":"DATE","default":"NOW"}},"identity":"user","globalId":"User","module":"sails-dirty","filePath":"./.tmp/dirty.db","inMemory":false,"adapter":"sails-dirty","migrate":"alter","globalize":true,"autoPK":true,"autoUpdatedAt":true,"autoCreatedAt":true,"schema":{"id":{"type":"INTEGER","autoIncrement":true,"default":"AUTO_INCREMENT","constraints":{"unique":true,"primaryKey":true}},"createdAt":{"type":"DATE","default":"NOW"},"updatedAt":{"type":"DATE","default":"NOW"}}}}
{"key":"waterline_data_user","val":[{"id":1,"createdAt":"2013-04-29T05:02:13.441Z","updatedAt":"2013-04-29T05:02:13.441Z"}]}
{"key":"waterline_data_user","val":[{"id":1,"createdAt":"2013-04-29T05:02:13.441Z","updatedAt":"2013-04-29T05:02:13.441Z"},{"id":2,"createdAt":"2013-04-29T05:14:59.805Z","updatedAt":"2013-04-29T05:14:59.805Z"}]}
@                                                                                                        @                                                                                                        @                                                                                                        @                                                                                                        @                                                                                                                 
"./.tmp/dirty.db" 5L, 2497C

設定を 'default': 'memory' とするとメモリ上にデータを格納することができます。
(sails v0.8.985 では、memory がデフォルトだった)

memory を使った場合、サーバを落とすとデータが消えてしまいますので注意が必要です。

余談ですが、supervisor などファイル変更のたびにサーバが再起動する仕組みだとmemoryを設定すると、ファイル変更のたびにデータが初期化されてしまい、とても使い勝手が悪くなるので 普段は disk で設定しておく方がよさそうです。

mysqlへの接続

他にも、mysql の設定があることが確認できます。 少し不安定な気もしますが、試してみましょう。

設定を 'default': 'mysql' とすると、mysql に接続してデータを格納することができます。

...(省略)

	'default': 'mysql',

...(省略)

	mysql: {
		module		: 'sails-mysql',
		host		: 'localhost',
		user		: 'user',
		password	: 'password',
		database	: 'sails_oar',
        port 		: '8889' 	//追加(各環境に合わせて設定)	
	}
};

mySQLに接続する前にモジュールのインストール行います。

$ npm install -s sails-mysql
sails-mysql@0.7.8 node_modules/sails-mysql
├── async@0.1.22
├── underscore@1.4.3
├── underscore.string@2.3.1
└── mysql@2.0.0-alpha7 (require-all@0.0.3, bignumber.js@1.0.1)

次に、mySQL側の設定を行います。

データベース名「sails_oar」で作成

mysql>CREATE DATABASE sails_oar;

ユーザ登録。権限とパスワードの設定を行います。

mysql> GRANT CREATE,SELECT,INSERT,UPDATE,DELETE
-> ON sails_oar.*
-> TO user@localhost
-> IDENTIFIED BY 'password';

mysql> FLUSH PRIVILEGES;

user テーブルとスキーマ設定

mysql> CREATE TABLE user ( 
    -> id INT(11) NOT NULL AUTO_INCREMENT,
    ->name VARCHAR(255),
    ->phoneNumber VARCHAR(15),
    ->createdAt TIMESTAMP NOT NULL,
    ->updatedAt TIMESTAMP NOT NULL,
    ->PRIMARY KEY (id)
    );

Userモデルの項目を追加します。
sails generator で生成された UserModel のコメントアウトを外します。

	attributes	: {

		// Simple attribute:
		name: 'STRING',

		// Or for more flexibility:
		phoneNumber: {
			type: 'STRING',
			defaultValue: '555-555-5555'
		}
		
	}

name プロパティと phoneNumber プロパティが定義されており、
両方 文字列(STRING)で、phoneNumber のデフォルト値は '555-555-5555'となります。
(※ 今回の mySQL を使った私の環境では DBレコード追加でデフォルト値が設定されませんでした。。。)

では、サーバを起動して mySQL にデータを追加します。

mySQLへの設定の誤りや接続が失敗していると起動時にエラーとして表示されます。

$ sails lift
debug: Starting server in /path/to/sails_oar...  
Error spawning mySQL connection:
{ [Error: connect ECONNREFUSED]
  code: 'ECONNREFUSED',
  errno: 'ECONNREFUSED',
  syscall: 'connect',
  fatal: true }
   info  - socket.io started

...

ブラウザを開いてアドレスバーに各項目の値をセットしてアクセスします。

http://localhost:1337/user/create?name=username&phoneNumber=000-000-0000

※ ここで、初回のみhttp://localhost:1337/user/createとするとデータベース側の name、phoneNumberカラムがなくなってしまいます。

{"name":"username","phoneNumber":"000-000-0000","id":1,"createdAt":"2013-04-29T06:30:01.256Z","updatedAt":"2013-04-29T06:30:01.256Z"}

今度は http://localhost:1337/user/create で追加してみます。

{"id":2,"createdAt":"2013-04-29T06:30:18.947Z","updatedAt":"2013-04-29T06:30:18.947Z"}

(※ phoneNumber のデフォルト値が設定されず・・・)

その他のDBへの接続

MongoDBなどのKVSへのデータ操作も可能なようですし、Redis への対応も現在行なっているようです。

ユーザ認証

先程作ったUserModelを使ったAPIなどは、誰でもアクセス出来ると運用上困るので、認証を掛ける設定が用意されています。
$ vim config/policies.js
/**
* Policy defines middleware that is run before each controller/controller.
* Any policy dropped into the /middleware directory is made globally available through sails.middleware
* Below, use the string name of the middleware
*/
module.exports.policies = {

	// Default policy (allow public access)
	'*': true

	/** Example mapping: 
	someController: {

		// Apply the &quot;authenticated&quot; policy to all actions
		'*': 'authenticated',

		// For someAction, apply 'somePolicy' instead
		someAction: 'somePolicy'
	}
	*/
};

someController個所のコメントアウトを外し、任意のコントローラに変更します。
(例) UserControllerなど

ログイン処理などが完了した後、session.authenticated = true; に設定することで UserController へのアクセスはログイン処理を行わないとアクセス出来ないようになります。

(2013.5.7 修正)session.authenticated = true; → req.session.authenticated = true;

□□□

まだまだ、機能が沢山あるので、またまとめて記事を書きたいと思います。

(2013.5.7 追記) ZeBeVogue別館 さんの記事node.jsのMVCフレームワークSails.jsを使ってみた にて本記事の一部の誤りについて書かれていたので訂正しております。