Instantly share code, notes, and snippets.

Embed
What would you like to do?
express実践入門

express実践入門


自己紹介

小川充

  • mitsuruog
  • フロントエンドエンジニア
    • Javascript, HTML, CSS, Node.js, API設計とか認証とか
  • 2015/5月入社(約半年)

はじめに

あくまで「俺が考える最強のexpress実践入門」です。

初学者がexpressを攻略する上でのつまづくポイントと、中規模開発をターゲットにしたベストプラクティスを経験ベースでお話します。

おそらく、初〜中級者向けの内容です。


本コンテンツの使い方

  • express初心者
    • 初学者向けチュートリアル(dotinstallとか)のあとに
  • express経験者
    • ご自身のコードの見直しに
  • 他の言語の経験者
    • 他の言語の「あれ」は、node.jsでは「これ」のマッピングに

(※)中で紹介するコードは抜粋したものであり、そのままでは動作しない場合があります。ご注意ください。
(※)versionはnode v4.2.0, express v4.13.1です。


expressの(超)概要

expressとはなにか?expressの初め方について

(ググったらすぐ出てくる内容ですよ〜)


express

Fast, unopinionated, minimalist web framework for Node.js

  • Fast - 高速
  • unopinionated - オープン
  • minimalist - 軽量

Node.jsのための、高速で軽量でオープンなWebフレームワーク。


Why express??

  • ほぼデファクトの地位
  • 豊富な情報量、サンプル
  • 豊富な拡張機能(middleware)
  • Pure node.jsで作成した場合、アプリを仕上げていく過程で「そもそもexpressで良かったのでは?」となることが多かった(経験談)

(※)ただし、3系と4系の違いに注意。世の中のサンプルは3系で書かれているものが多く、動作しないことがある。

他のWebフレームワーク


Install

Node.jsをインストールして、、、
ほぼ一発。

mkdir myapp && myapp
npm init
npm install express

Hello world

サーバー側のコード(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-generator

通常はこちらの方をよく使います。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 

express-generatorプロジェクト構成

基本最小構成。後ほどオレ色に染め上げて行きます。

.
├── 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攻略

expressのここを理解すればOK!


express攻略の勘所

expressを理解する上での最小構成要素。

  • routing
  • middleware

以上2つ


expressの仕組み

  • routing
    • 外部からのHTTP(S)リクエストに対して、内部のロジックをマッピングすること。
  • middleware
    • routingの過程で何らかの処理を差し込む仕組み。
    • 共通処理(認証、エラーハンドリング、リクエストデータの加工、etc)を本来のロジックから分離して、コードベースを健全に保つ。

routing(1/2)

基本(Route paths, method, handler)

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(2/2)

基本(express.Router)

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が有効になる


routing(Request method)

よく利用するもの

router.get('/', (req, res) => {
	// 何かの処理
});
  • req.body
    • request bodyのkey-valueペア(body-parser middlewareが必要)
  • req.cookies
    • cookieのkey-valueペア(cookie-parser middlewareが必要)
  • req.params
    • /books/:id/books/1の場合req.params.id => 1
    • url pathパラメータのkey-valueペア
  • req.query
    • /books?order=ascの場合req.query.order => asc
    • リクエストパラメータのkey-valueペア
  • req.get
    • HTTPヘッダーの値を取得する
  • req.session
    • セッションのkey-valueペア(express-session middlewareが必要)

routing(Response method)

よく利用するもの

router.get('/', (req, res) => {
	// 何かの処理
});
  • res.cookie
    • cookieを付与
  • res.set
    • HTTPヘッダーを付与
  • res.redirect
    • 指定したPathへリダイレクト
  • res.render
    • テンプレートエンジンを利用して画面を生成して返却
  • res.sendStatus
    • ステータスコードを返却(401, 404, 500, etc...)
    • ex) res.sendStatus(401).json({...})
  • res.json
    • jsonを返却(200)

middleware(基本)

middlewareのhandler(実体)の基本I/Fの形 (ただし、エラーハンドラを除く)

function(req, res, next) {
  // middlewareの処理
  next();
}

middlewareは1つのroutingに対して複数連結して処理されるため、次のmiddlewareへ移動するためにnextを利用する。
middlewareの実行順序は宣言したもの順。エラーハンドラが最後にあるのは、それなりの理由がある。

middlewareは3種類ある

  • Application-level
  • Router-level
  • Error-handling

middleware(Application-level)

Application-level

var app = express();

// '/'に対するmiddleware
app.use(function (req, res, next) {
  next();
});

// 'GET books/:id'に対するmiddleware
app.get('books/:id', function (req, res, next) {
  next();
});

middleware(Router-level)

Router-level

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();
});

middleware(TPO)

Application-level middlewareとRouter-level middlewareの違いについて、利用者レベルでは正直良くわからない。

使い分け方針(TPO)

  • アプリ全体
    • Application-level middleware
    • app.use()
  • 特定のrouting
    • Router-level middleware
    • router.get('/:id', someMiddleware, businessLogic)

middleware(Error-handling)

Error-handling

(後述) エラーハンドリングの部分で紹介します。


express開発のベストプラクティス

中規模Webアプリケーションを構築するために必要な要素とは

  • アプリケーションのレイヤー化
    • プロジェクトストラクチャ
    • テンプレートエンジン
    • ORM
  • 共通処理
    • エラーハンドリング、認証、ロギング、セッション、設定情報
  • デリバリー
    • デプロイ(Heroku)

プロジェクトストラクチャ(1/4)

概要

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

プロジェクトストラクチャ(2/4)

アプリケーション本体

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/

プロジェクトストラクチャ(3/4)

アプリケーション設定

設定系は小分けにする方が取り回しが良くていい。テストも楽(経験談)

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/

プロジェクトストラクチャ(4/4)

静的リソース

あくまで最小構成。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');

テンプレートエンジン(QA)

Q:どうしても使い慣れたHandlebarsが使いたいです
A: Handlebarsはexpress内部のテンプレートエンジン処理のI/Fと異なるため、consolidate.jsでHandlebarsをラップする必要がある。
(他のJavascriptテンプレートエンジンを利用する場合も同様)

自信がない人は近づかない方がいい(経験談)


ORM

ORM(Object-relational mapping)この2つでOK


ORM(Mongoose)

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);

ORM(Mongoose)

利用するにはModelをロードして利用する。

var User = require('./models/user.model');

User.find({}}
  .exec()
  .then((users) => {
    // 正常系の処理
  }, (err) => {
    // 異常系の処理
  })

よく使うもの

  • find
  • findOne
  • findById
  • findByIdAndUpdate
  • findByOneAndUpdate
  • findByIdAndRemove
  • findByOneAndRemove
  • create
  • save

findByIdAndUpdateではなく、findByIdしてからsaveしている方が多い気がします(経験談)


ORM(Mongoose)

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);

ORM(Sequelize)

さーせん!時間切れ。。。orz


エラーハンドリング(1/3)

特殊なmiddleware。

あまり細かくtry-catchせずグローバルレベルでエラーハンドリングすることが多い。

function(err, req, res, next) {
  // エラー処理    
}

最初の引数errにエラーの情報が連携される。基本的には次の3処理を行う。

  • エラーログ出力
  • REST API用のレスポンス返却
  • 画面用のレスポンス返却(エラーページ)

エラーハンドリング(2/3)

エラーハンドリングの順番

// エラーログ出力 
app.use(logErrors);

// REST API用のエラーハンドラ(ここでは、/apiがAPIの想定)
app.use('/api', clientErrorHandler);

// エラーベージ表示用エラーハンドラ
app.use(errorHandler);

エラーハンドリング(1/3)

エラーハンドリングの記述例

// エラーログ出力
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)

Passport

  • ほぼ、node.jsの認証モジュールでデファクト
  • 様々な認証に対応可能(Strategy)
  • 下手に独自で認証を実装するくらいなら、Passportの使い方を習得したほうが後々潰しが効く(経験談)

対応例)


Passportのフォルダ構成

  • passport全体の設定 => passport.js
  • 個別のStrategyの設定 => passport/
  • 認証フィルタはmiddleware化する
config
  app.js              - passportの初期化
  passport.js         - passportの全体的な設定 
  passport/           - Strategyごとの認証ロジックの設定
    local.js          - Username and password認証用
    twitter.js        - twitter認証用
  middlewares
    authorization.js  - routingで利用する認証フィルタ

Passportの使い方(1/3)

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の使い方(2/3)

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'));
  ...

}

Passportの使い方(3/3)

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(認証フィルタ)

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) => {
  // 何かの処理
});

設定情報

  • 動作環境ごとで異なる情報(DB接続設定、APIKey、Secret)
  • production環境の設定はリポジトリにCommitせず、動作環境の環境変数から取得する方がいい(経験談)

node.jsでの環境変数の取得方法

prosess.env.SOME_KEY

dotenv(1/2)

motdotla/dotenv

  • .envファイルを環境変数にマッピングする
  • ロードするファイルなどはカスタム可能

利用方法

var app = express();

// nodeの動作環境(development/production)ごとにロードする設定情報を変更する
require('dotenv').config({
  path: 'config/environment/.env.' + app.get('env')
});

dotenv(2/2)

よく使うディレクトリ構造

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の方が良さそう。今度使ってみる。日々精進。


セッション

expressjs/session

var app = express();
var session = require('express-session');

app.use(session({
  secret: 'secret-key',
  resave: false,
  saveUninitialized: true
}));
  • Session IDがcookieに保存される
  • デフォルトのSession ID名はconnect.sid
  • デフォルトはオンメモリ上で保持される。通常はSession Storesで永続化する

Session Stores)


セッション(Redis Session Store)

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
  })
}));

セッション(MongoDB Session Store)

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
  })
}));

ロギング

expressjs/morgan

  • アクセスログを出力する
  • 出力先は標準出力(ファイル出力&ローテション可能)
  • expressに求める機能が薄いためなのか、今のところこれで十分(経験談)

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

デプロイ(Heroku)

git push heroku master
  • Herokuの話
    • アプリケーション直下にpackage.jsonがあるとNode.jsと判定される。
    • 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",
	...
  }
}

デプロイ(Heroku Q&A)

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", ← これ
	...
  }
}

デプロイ(Heroku Q&A)

Q: augularとか使っているので、gulpでビルドをしたいです
A: おじさんがいい方法知ってるから、後で裏に来なさい


小ネタ

レスポンスのx-powered-byヘッダーを消す。

app.set('x-powered-by', false);

まとめ

最初はこれくらい知ってれば、まぁ大丈夫じゃないかな。


enjoy express :)

It’s your turn.


Analytics

@mitsuruog

This comment has been minimized.

Owner

mitsuruog commented Oct 29, 2015

title

@mitsuruog

This comment has been minimized.

Owner

mitsuruog commented Oct 29, 2015

img_20151029_095825_20151029100001532

@lecaoquochung

This comment has been minimized.

lecaoquochung commented Aug 31, 2016

cool! thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment