🚶‍♀️

MEAN Stackアプリケーション 最初の一歩

5 min read

2014-05-26 に Qiita に投稿した記事のアーカイブです。本文中のリンクは動作しないことがあります。

これまで MAMP (Mac, Apache, MySQL, PHP)を書いていましたが、結局ガワを HTML+JS で書く必要があり、そうこうしている内に「じゃあ全部 JS で出来ないの?」という興味が湧いたので、最近流行っているらしい MEAN (MongoDB, Express, AngularJS, Node.js)を試してみました。

よくこの手のチュートリアルでは「CRUD がどうのこうの…」といきなり 4 種やらせようとしますが、個人的には「モチベーションのために最低限動けばええんじゃ、あとは調べるわい」という気分なので、以下は本当に最低限です。

前提

OSX 10.9 で、node, yo, mongodb はインストール済みとします。説明は少なめなので、何らかの Web フレームワークの開発経験はあるが MEAN スタックは触ったことがないというレベルの方を想定して書いています。

generator-angular-fullstack をインストール

Yeoman generator であるgenerator-angular-fullstack(GitHub)をインストールします。

$ npm install -g generator-angular-fullstack

ボイラープレートを作成

作成したいディレクトリ内に移動してから。

$ yo angular-fullstack hello
[?] Would you like to use Sass (with Compass)? Yes
[?] Would you like to include Twitter Bootstrap? Yes
[?] Would you like to use the Sass version of Twitter Bootstrap? Yes
[?] Which modules would you like to include?
    angular-resource.js, angular-cookies.js, angular-sanitize.js, angular-route.js
[?] Would you like to include MongoDB with Mongoose? Yes
[?] Would you like to include a Passport authentication boilerplate? No

Passport のみ不要に設定。ここから結構待たされます。

MongoDB で連番 ID を扱うために

MongoDB では ObjectId というユニークな識別子を持っていますが、連番 ID を扱いたいこともあるでしょう。そのために mongoose のプラグインとしてpackage.json"dependencies""mongoose-auto-increment"(GitHub)を追加します。

package.json
"mongoose-auto-increment": "~3.0.2"
$ npm install

Todos へのルーティングを追加する

簡易な Todo アプリケーションのために AngularJS のテンプレートを追加します。ディレクトリはずっとそのまま。

$ yo angular-fullstack:route todos

View を書く

前節のangular-fullstack:routeで新たなファイルがいくつか増えます。js のローディングに関する記述も追記されます。出てくるテンプレートはシンプルとはいえ融通が効かないようなので、慣れたら自分で作ったほうが速そう。

app/views/partials/todo.htmlも今増えたファイルのひとつです。

app/views/partials/todo.html
<div ng-include="'partials/navbar'"></div>

<div>
  <h1>Todo</h1>
  <form ng-submit="add()">
    <div class="form-group">
      <label for="task">Task</label>
      <input
        type="text"
        class="form-control"
        id="task"
        ng-model="newTodo"
        placeholder="What needs to be done?"
      />
    </div>
    <input class="btn" value="Add" type="submit" />
  </form>
</div>
<hr />
<ul>
  <li ng-repeat="todo in todos">{{todo.todoId}}: {{todo.content}}</li>
</ul>

クライアント側 Controller を書く

app/scripts/controllers/todos.js
"use strict";

angular
  .module("helloApp")
  .controller("TodosCtrl", function ($scope, $resource) {
    var init = function () {
      $scope.newTodo = "";
      $resource("/api/todos/").query(function (todos) {
        $scope.todos = todos;
      });
    };
    init();

    $scope.saveTodo = function (todo, callback) {
      return $resource("/api/todos/").save(
        todo,
        function (todo) {
          console.log("success");
        },
        function (err) {
          console.log("error");
        }
      ).$promise;
    };

    $scope.add = function () {
      var todo = { content: $scope.newTodo };
      $scope.saveTodo(todo).then(function (res) {
        init();
      });
    };
  });

saveTodo()まわりは試行錯誤中、あまり美しくないです。$resource().save()の第 2,3 引数で成功時と失敗時のハンドリング。$scope.addthen()は保存後の処理。

サーバー側を実装

server.js
var express = require("express"),
  path = require("path"),
  fs = require("fs"),
  mongoose = require("mongoose"),
  autoIncrement = require("mongoose-auto-increment"); // 連番を扱う場合のみ

// 略

var db = mongoose.connect(config.mongo.uri, config.mongo.options);
autoIncrement.initialize(db); // 連番を扱う場合のみ

// 略

連番 ID を扱わない場合は変更なし。

API を定義

lib/routes.js
var api = require("./controllers/api"),
  todos = require("./controllers/todos"), // 追加
  index = require("./controllers");

// 略
module.exports = function (app) {
  app.route("/api/awesomeThings").get(api.awesomeThings);

  /* 追加 */
  app.route("/api/todos").get(todos.show).post(todos.create);
  /* ここまで */

  // 略
};

URL と各種 HTTP メソッドをここで定義。実装の詳細はtodos = require('./controllers/todos')ここに書いた部分(次の節)。

サーバー側 Controller を書く

lib/controllers/todos.js
"use strict";

var mongoose = require("mongoose"),
  Todo = mongoose.model("Todo");

exports.create = function (req, res, next) {
  var newTodo = new Todo(req.body);
  newTodo.save(function (err) {
    if (err) return res.json(400, err);
  });
  return res.send(200);
};

exports.show = function (req, res, next) {
  Todo.find({}, function (err, todos) {
    res.send(todos);
  });
};

POST メソッドでレスポンスをreturn res.send()もしくはres.json()しないとブラウザの非同期処理が正しく動かない。最初気付かずにsave()のみにしてハマったので注意。

Model を書く

lib/models/todo.js
"use strict";

var mongoose = require("mongoose"),
  Schema = mongoose.Schema,
  autoIncrement = require("mongoose-auto-increment"); // 連番IDを使う場合のみ

var TodoSchema = new Schema({
  content: String,
});

TodoSchema.plugin(autoIncrement.plugin, { model: "Todo", field: "todoId" }); // 連番IDを使う場合のみ
module.exports = mongoose.model("Todo", TodoSchema);

サーバー側 Controller, Model を勝手に作ってくれる機能は無いようなので、手動で書きました。

Grunt を起動

$ grunt serve

http://localhost:9000/todosで View が表示されるはず。入力して Add を押すと下に追加されます。Todo と言いながらチェックも削除もできないが、まぁ勝手は分かった。

とりあえず、ここまで。