express実践入門
あくまで「俺が考える最強のexpress実践入門」です。
初学者がexpressを攻略する上でのつまづくポイントと、中規模開発をターゲットにしたベストプラクティスを経験ベースでお話します。
おそらく、初〜中級者向けの内容です。
(※)中で紹介するコードは抜粋したものであり、そのままでは動作しない場合があります。ご注意ください。
(※)versionはnode v4.2.0, express v4.13.1です。
expressとはなにか?expressの初め方について
(ググったらすぐ出てくる内容ですよ〜)
Fast, unopinionated, minimalist web framework for Node.js
Node.jsのための、高速で軽量でオープンなWebフレームワーク。
そもそもexpressで良かったのでは?
」となることが多かった(経験談)(※)ただし、3系と4系の違いに注意。世の中のサンプルは3系で書かれているものが多く、動作しないことがある。
他のWebフレームワーク
Node.jsをインストールして、、、
ほぼ一発。
mkdir myapp && myapp
npm init
npm install express
サーバー側のコード(app.js)
var express = require('express');
var app = express();
// HTTPリクエストを受け取る部分
app.get('/', function (req, res) {
res.send('Hello World!');
});
// サーバーを起動する部分
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
サーバーを起動して、http://localhost:3000
にアクセス
node app.js
curl http://localhost:3000 -> Hello World!
通常はこちらの方をよく使います。Express application generator
(sudo)npm install express-generator -g
// expressコマンドでアプリのひな形を生成します
express myapp
create : myapp
create : myapp/package.json
create : myapp/app.js
...
create : myapp/bin
create : myapp/bin/www
cd myapp
// 依存モジュールをインストールします
npm install
// サーバーを起動します
node bin/www // or npm run start
基本最小構成。後ほどオレ色に染め上げて行きます。
.
├── app.js // expressサーバーの設定
├── bin
│ └── www // サーバーの起動
├── package.json
├── public // 静的ファイル置き場
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes // サーバー側のコントローラ
│ ├── index.js
│ └── users.js
└── views // サーバー側で画面を作成する際のテンプレート
├── error.jade
├── index.jade
└── layout.jade
expressのここを理解すればOK!
expressを理解する上での最小構成要素。
以上2つ
HTTPメソッド、Path、マッピングする内部ロジックを指定する方式。
var app = express();
// GET http://localhost:3000/
app.get('/', (req, res) => {});
// POST http://localhost:3000/books
app.post('/books', (req, res) => {});
// PUT http://localhost:3000/books/1
app.put('/books/:id', (req, res) => {});
// DELETE http://localhost:3000/books/1
app.delete('/books/:id', (req, res) => {});
routing用のmiddlewareを作る仕組み。 routing部分をモジュール化(別ファイル化)することが多いため、こちらの方をよく利用します。
routingをモジュール化(router.js)
var app = express();
var router = express.Router();
router.get('/:id', (req, res) => {
// 何かの処理
});
module.exports = router;
モジュールを利用する。(app.js)
var router = require('./router');
...
app.use('/books', router);
「http://localhost:3000/books/1」のroutingが有効になる
よく利用するもの
router.get('/', (req, res) => {
// 何かの処理
});
/books/:id
で/books/1
の場合req.params.id
=> 1/books?order=asc
の場合req.query.order
=> ascよく利用するもの
router.get('/', (req, res) => {
// 何かの処理
});
res.sendStatus(401).json({...})
middlewareのhandler(実体)の基本I/Fの形 (ただし、エラーハンドラを除く)
function(req, res, next) {
// middlewareの処理
next();
}
middlewareは1つのroutingに対して複数連結して処理されるため、次のmiddlewareへ移動するためにnext
を利用する。
middlewareの実行順序は宣言したもの順。エラーハンドラが最後にあるのは、それなりの理由がある。
middlewareは3種類ある
var app = express();
// '/'に対するmiddleware
app.use(function (req, res, next) {
next();
});
// 'GET books/:id'に対するmiddleware
app.get('books/:id', function (req, res, next) {
next();
});
var router = express.Router();
// '/'に対するmiddleware
router.use(function (req, res, next) {
next();
});
// 'GET books/:id'に対するmiddleware
router.get('books/:id', function (req, res, next) {
next();
});
Application-level middlewareとRouter-level middlewareの違いについて、利用者レベルでは正直良くわからない。
使い分け方針(TPO)
app.use()
router.get('/:id', someMiddleware, businessLogic)
(後述) エラーハンドリングの部分で紹介します。
中規模Webアプリケーションを構築するために必要な要素とは
railsに似せた構成が好み。
正直好みの問題です、悩みたくないならこれを使ってください(経験談)
app/ // アプリケーション本体
api/ // REST APIのコントローラ
controllers/ // 画面のコントローラ
models/ // Entiry
repositories/ // DAO(CRUD部品)
views/ // 画面のテンプレート
bin/ // サーバー起動
config/ // アプリケーションの設定系
environment/ // 環境定義
middlewares/ // middlewareの置き場
passport/ // 認証系のstrategyの置き場
express.js // express本体の設定
router.js // トップレベルのルーティング設定
...js // 何かの共通設定系
public/ // 静的リソースの置き場(js, css)
util/ // ユーティリティ
package.json
bower.json
REST APIと画面コントローラーの置き場所は毎回悩む(経験談)
app/ // アプリケーション本体
api/ // REST APIのコントローラ
users/
index.js // /users配下のルーティング設定
user.controller.js //
controllers/ // 画面のコントローラ
users/
index.js // user画面のactionのルーティング
user.controller.js // user画面のコントローラ
models/ // Entiry
user.model.js
repositories/ // DAO(CRUD部品)
user.repository.js
views/ // 画面のテンプレート
layout.jade // 共通テンプレート
user.jade
bin/
config/
public/
設定系は小分けにする方が取り回しが良くていい。テストも楽(経験談)
app/
bin/
www // サーバー起動
config/
environment/ // 環境設定
.env.development
middlewares/ // middleware置き場
authorization.js
errorHandler.js
passport/ // passportのstrategy置き場
local.js
twitter.js
db.js // DB関連の設定(接続先など)
passport.js // 認証(passport)の設定
express.js // expressサーバー本体の設定
router.js // トップレベルのルーティング
public/
あくまで最小構成。ViewをSPA(Single page application)にする場合は、最初からMEAN Stackを利用した方がいい(経験談)
app/
bin/
config/
public/
bower_modules/
javascripts/
stylesheets/
package.json
MEAN Stack)
特にこだわりなければ、Jade
にしておくのが最も平和(経験談)
app.js
// テンプレートが格納されているフォルダを指定する
app.set('views', path.join(__dirname, '../app/views'));
// expressで利用するテンプレートエンジンを指定する
app.set('view engine', 'jade');
Q:どうしても使い慣れたHandlebarsが使いたいです
A:
Handlebarsはexpress内部のテンプレートエンジン処理のI/Fと異なるため、consolidate.jsでHandlebarsをラップする必要がある。
(他のJavascriptテンプレートエンジンを利用する場合も同様)
自信がない人は近づかない方がいい(経験談)
ORM(Object-relational mapping)この2つでOK
MongooseのModelクラスを作成します。
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
// スキーマ定義
var UserSchema = new Schema({
email: {
type: String,
lowercase: true
},
password: String,
admin: {
type: Boolean,
default: false
}
});
// 外部に公開します
module.exports = mongoose.model('User', UserSchema);
利用するにはModelをロードして利用する。
var User = require('./models/user.model');
User.find({}}
.exec()
.then((users) => {
// 正常系の処理
}, (err) => {
// 異常系の処理
})
よく使うもの
findByIdAndUpdateではなく、findByIdしてからsaveしている方が多い気がします(経験談)
Modelクラスにロジックを寄せるのが綺麗です(経験談)
Mongoose Modelクラスの中身の例)
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var UserSchema = new Schema({
...
});
//////////// Virtuals
UserSchema
.virtual('password')
.set((password) => {
this.cachedValue = password;
// 何かのSetter処理
})
.get(() => { return this.cachedValue});
//////////// Validations
UserSchema
.path('email')
.validate((value) => {
// 何かのバリデーション処理
}, 'メッセージ');
//////////// Hooks
UserSchema.pre('save', (next) => {
// 何かの処理
return next();
});
//////////// Instance Methods
UserSchema.methods = {
authenticate(password) {
// 何かの処理
}
};
//////////// Static Methods
UserSchema.statics = {
findOne(id) {
// 何かの処理
}
}
module.exports = mongoose.model('User', UserSchema);
さーせん!時間切れ。。。orz
特殊なmiddleware。
あまり細かくtry-catchせずグローバルレベルでエラーハンドリングすることが多い。
function(err, req, res, next) {
// エラー処理
}
最初の引数err
にエラーの情報が連携される。基本的には次の3処理を行う。
エラーハンドリングの順番
// エラーログ出力
app.use(logErrors);
// REST API用のエラーハンドラ(ここでは、/apiがAPIの想定)
app.use('/api', clientErrorHandler);
// エラーベージ表示用エラーハンドラ
app.use(errorHandler);
エラーハンドリングの記述例
// エラーログ出力
function logErrors(err, req, res, next) {
console.error(err.stack);
next(err);
}
// REST API用のレスポンス返却
function clientErrorHandler(err, req, res, next) {
res.status(500).json({
message: err.message,
error: err
});
}
// 画面用のレスポンス返却
function errorHandler(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
}
対応例)
passport.js
passport/
config
app.js - passportの初期化
passport.js - passportの全体的な設定
passport/ - Strategyごとの認証ロジックの設定
local.js - Username and password認証用
twitter.js - twitter認証用
middlewares
authorization.js - routingで利用する認証フィルタ
passportモジュールの読み込み。Session用のmiddlewareの設定。
app.js
var passport = require('passport');
// passportモジュールをLoad
require('./passport')(app);
// session用のmiddlewaresを有効化
app.use(passport.initialize());
app.use(passport.session());
Passport全体の設定。sessionのserializer/deserializer&利用するStrategyの設定
config/passport.js
module.exports = () => {
// sessionにユーザー(のキー)情報を格納する処理
passport.serializeUser((user, done) => {
done(null, user.id);
});
// sessionからユーザー情報を復元する処理
passport.deserializeUser((id, done) => {
// DBのUserテーブルからユーザーを取得する処理
User.findById(id).exec((err, user) => {
done(err, user)
});
});
// 利用するstrategyを設定
passport.use(require('./passport/local'));
...
}
Strategyの個別設定。
(※)詳細はそれぞれのStrategyの公式ページを参照してください。
config/passport/local.js
// Strategyをロードする
var LocalStrategy = require('passport-local').Strategy;
// Strategyことの認証ロジックを追加する
module.exports = new LocalStrategy({
// 認証ロジック
});
(ログインなど)特定のrouting時に認証を行うようにする。
app.post('/login',
passport.authenticate('local', { failureRedirect: '/login' }),
function(req, res) {
// 認証成功時
res.redirect('/');
});
passportの作者が作ったものがある。
connect-ensure-login
予めmiddleware化しておく。
config/middlewares/authorization.js
// 認証フィルタに引っかかると`/badLoginRedirectPath`にリダイレクト
exports.authorize = require('connect-ensure-login').ensureLoggedIn('/badLoginRedirectPath');
routingでmiddlewareを設定する
var auth = require('middlewares/authorization');
app.use('/some', auth.authorize, (req, res) => {
// 何かの処理
});
node.jsでの環境変数の取得方法
prosess.env.SOME_KEY
.env
ファイルを環境変数にマッピングする利用方法
var app = express();
// nodeの動作環境(development/production)ごとにロードする設定情報を変更する
require('dotenv').config({
path: 'config/environment/.env.' + app.get('env')
});
よく使うディレクトリ構造
config
environment
.env.development // 開発用
.env.production // 本番用(通常は空)
.env.test // テスト用
.env.development
MONGOLAB_URI=http://localhost:27017
express側で値を取得する
console.log(process.env.MONGOLAB_URI) // => http://localhost:27017
とはいえ。。。
lorenwest/node-configの方が良さそう。今度使ってみる。日々精進。
var app = express();
var session = require('express-session');
app.use(session({
secret: 'secret-key',
resave: false,
saveUninitialized: true
}));
connect.sid
Session Stores)
var app = express();
var session = require('express-session');
// Session Storeをロードする
var RedisStore = require('connect-redis')(session);
app.use(session({
secret: 'secret-key',
resave: true,
saveUninitialized: true,
// Session Storeの設定
store: new RedisStore({
url: <REDIS_URL> // redis://localhost:6379
})
}));
DBの接続情報はmongoose
を利用する。(mondoDB URLでももちろん可)
var app = express();
var session = require('express-session');
// Session Storeをロードする
var MongoStore = require('connect-mongo')(session);
var mongoose = require('mongoose');
app.use(session({
secret: 'secret-key',
resave: true,
saveUninitialized: true,
// Session Storeの設定
store: new mongoStore({
mongooseConnection: mongoose.connection
})
}));
app.js
var logger = require('morgan');
// ログフォーマット
// combined, common, dev, short, tiny
app.use(logger('dev'));
出力内容
PUT /api/articles/5621dc5633d2c52b7c166873 500 12.569 ms - 47
GET /new-post 200 771.074 ms - 1885
GET /stylesheets/style.css 200 13.891 ms - 102
GET /bower_components/bootstrap/dist/css/bootstrap-theme.min.css 200 18.804 ms - 23357
git push heroku master
package.json
があるとNode.jsと判定される。npm run start
が自動実行される。(Procfile必要なし)package.json
{
"name": "node-sample",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www" ← これ
},
"dependencies": {
"body-parser": "~1.13.2",
"cookie-parser": "~1.3.5",
...
}
}
Q: bowerモジュールがインストールされません
A: package.jsonのdependenciesにbowerを追加してpostinstall
する
ECMAScript 6で開発したアプリをHerokuにデプロイ - フレクトのHeroku Lab
package.json
{
"name": "node-sample",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
"postinstall": "bower install" ← これ
},
"dependencies": {
"body-parser": "~1.13.2",
"cookie-parser": "~1.3.5",
"bower": "^1.6.3", ← これ
...
}
}
Q: augularとか使っているので、gulpでビルドをしたいです
A: おじさんがいい方法知ってるから、後で裏に来なさい
レスポンスのx-powered-by
ヘッダーを消す。
app.set('x-powered-by', false);
最初はこれくらい知ってれば、まぁ大丈夫じゃないかな。