Node.jsまとめ

# Node.js まとめ

## 参考

## リスト

- [Node.js本家](https://nodejs.org)
- [Node.js日本ユーザグループ](https://nodejs.jp/)
- [npm](https://www.npmjs.com/)
    - Nodeのパッケージマネージャ

    ```bash
    # Nodeで作成する前に実行してpacakge.jsonを作成しておく
    npm init
    # npm でパッケージを追加
    npm install [package name] --save
    npm install [package name] --save-dev

    # どこかから持ってきた環境でpacakge.jsonがあれば以下で必要なパッケージがインストールされる
    npm install
    ```

- [NodeSchool](https://nodeschool.io/ja/)
    - NodeでNodeを学習

- [Express](http://expressjs.com/)
    - NodeのデファクトのMVCフレームワーク
    - RouteとMiddleware
    - [MSの情報](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs)

- [Socket.io](https://socket.io/)
    - ソケット接続ライブラリ

## メモ

### Node

- イベントとストリームは理解しておく。

#### [イベント](https://nodejs.org/dist/latest-v8.x/docs/api/events.html)

- EventEmitter クラスのインスタンス
- eventEmitter.on() でリスナー（イベント？コールバック？）を登録
- eventEmitter.emit() でイベントを実行
- リスナーはすべて同期的に実行される
- eventEmitter.once() はリスナーが1回しか実行されない
- 複数リスナーを登録するとリスト化されて登録される
- デフォルトでは10個までリスナーを登録できる
- 'error'というリスナーは特別。登録されていない場合に、eventEmitter.emit('error') で例外が出る。  

```js
const EventEmitter = require('events');

// EventEmitter クラスを継承
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();

// 'event'というイベントを登録
myEmitter.on('event', () => {
  console.log('an event occurred!');
});

// 'event'イベントを実行。'an event occurred!'が表示される
myEmitter.emit('event');

myEmitter.on('args', (a, b) => {
  console.log(a, b, 'an event occurred!');
});
myEmitter.emit('args', 'abc', '123');

// 1回しか実行しないイベント
myEmitter.once('once', () => {
  console.log('once!');
});
myEmitter.emit('once');
myEmitter.emit('once'); // 無視される
```


#### [ストリーム](https://nodejs.org/dist/latest-v8.x/docs/api/stream.html)

- ストリームを扱うための抽象インタフェース
- stream モジュールを使用して作成する
- 種類としては、readble, writable, その両方があり、EventEmitter のインスタンスである
    - Readable: 読み込みができるストリーム (例 fs.createReadStream()).
    - Writable: 書き込みができるストリーム (例 fs.createWriteStream()).
    - Duplex: 読み書きができるストリーム (例 net.Socket).
    - Transform: 読み書きするときにデータをを変換するストリーム (例 zlib.createDeflate()).
- 文字列(string)またはバッファ(Buffer) を処理する
- ストリームが生成されると、objectMode オプションを使用してオブジェクトモードにスイッチする
- Readable と Writable ストリームは内部バッファにデータを保持する
    - readbale.push(chunk) をコールすると Readable ストリームにバッファされる。使用する側は readbale.read() をコールしてはダメ）
        - 作成時に指定する highWaterMark （= バッファ量）を超えるとバッファリングを停止して、バッファリングされたデータが消費されるまで停止する
    - writable.write(chunk) をコールすると Writable ストリームにバッファされる。writable.write() は バッファ量が highWaterMark 以内であれば true を返す。超えていれば false を返す
- Duplex and Transform は、 Readable かつ Writable であるのでそれぞれ用に別のバッファを持つ。（Readable用とWritable用をもつということ）

- Writable ストリームは、write() や end() のようなメソッドを提供する
- Readable ストリームは、EventEmitter を使って読み込み中や終了時にイベントを実行する


#### Writable ストリーム

```js
// Writable（write() で書き込んで end() 終了する）
const myStream = getWritableStreamSomehow();
myStream.write('some data');
myStream.write('some more data');
myStream.end('done writing data');

// イベント
myStream.on("close", () => {});
myStream.on("drain", () => {}); // Writable.write(chunk) が false を返すときにコールされる
myStream.on("error", () => {});
myStream.on("finish", () => {}); // Writable.end() がコールされたときにコールされる
myStream.on("pipe", (src) => {}); // Readable ストリームでReadable.pipe() 
                                  // がコールされたときにコールされる
myStream.on("unpipe", (src) => {});
```

#### Readable Streams

- ２つのモード
    - flowing: 下流からデータを自動的に読み込む。EventEmitter を使ってできる限り早くアプリケーションに提供する。
    - paused: ストリームからデータを読み込むには、stream.read() をコールしないとダメ。
- 通常は、pausedから始まり以下の場合に、flowingモードに移行する
    - 'data' イベントハンドラを追加したとき
    - stream.resume() メソッドをコールしたとき
    - Writable にデータを送るために、stream.pipe() をコールしたとき
- 以下の場合に、pausedモードに移行する
    - pipe の宛先がないときに、stream.pause() をコールしたとき
    - pipe の宛先があるが、すべてのpipの宛先を削除したとき。（stream.unpipe() をコールして削除したときなど）

```js
// Readable
const readable = new stream.Readable();
readable.on('data', (chunk) => {
  console.log(`Received ${chunk.length} bytes of data.`);
  readable.pause(); // dataイベントを停止する
  console.log('Restart.');
  readable.resume(); // dataイベントを再開する
});

// pipe ※適用した writable が返されるのでチェーン化できる
const r = fs.createReadStream('file.txt');
const z = zlib.createGzip();
const w = fs.createWriteStream('file.txt.gz');
r.pipe(z).pipe(w);

// イベント
readable.on("close", () => {}); // ストリームや下位のリソースが切断されたときにコールされる
readable.on("data", (chunk) => {}); // 使う側にデータの塊を受け渡すときにコールされる
readable.on("end", () => {}); // 読み込むデータが無くなったときにコールされる
readable.on("error", () => {});
readable.on("readable", () => {}); // ストリームからデータ読み込みが可能になったらコールされる
```


#### ストリームの実装

- stream を読み込む
- 継承したい機能から４つのストリーム(Readable, Writable, Duplex, Transform)をextendする

```js
const { Writable } = require('stream');
class MyWritable extends Writable {
    construcotr(options) {
        super(options);
    }

    _write(chunk, encoding, callback) {
    }
}
```

- ストリームにより以下のメソッドを実装する
    - Readable: _read()
    - Writable: _write, _writev, _final
    - Duplex: _read, _write, _writev, _final
    - Transform: _transform, _flush, _final

##### Writable

```js
// extends して実装するパターン
const { Writable } = require('stream');

class MyWritable extends Writable {
  constructor(options) {
    super(options);
    // 初期化処理
  }

  _write(chunk, encoding, callback) {
  }
}

// 簡単アプローチ(ストリームのインスタンス化)
const { Writable } = require('stream');

// Writableのコンストラクタに渡せるオプションはコンストラクタ参照
// メソッドとしては、write, writeev, destory, finalが指定できる。
const myWritable = new Writable({
  write: function(chunk, encoding, callback) {
    // ...
  },

  writev: function(chunks, callback) {
    // ...
  }
});

// サンプル
const { Writable } = require('stream');
class LogStream extends Writable {
  constructor(options) {
    super(options)
  }

  _write(chunk, encoding, callback) {
    if (chunk.toString().indexOf('4') >= 0) {
        callback(new Error('chunk has unfortune number "4"'));
    } else {
        console.log(chunk);
        callback();
    }
  }
}
```


##### Readable

```js
// extendsして実装するパターン
const { Readable } = require('stream');

class MyReadable extends Readable {
  constructor(options) {
    super(options);
  }

  _read(size) {
  }
}

// 簡単アプローチパターン
const { Readable } = require('stream');

// Readableのコンストラクタに渡せるオプションはコンストラクタ参照
// メソッドとしては、read, destoryが指定できる。
const myReadable = new Readable({
  read: function(size) {
  }
});

// サンプル
const { Readable } = require('stream');
class CountupStream extends Readable {
  constructor(max) {
    super(option)
    this._max = (max > 0)? max: 5;
    this._index = 0;
  }

  _read(size) {
    this._index++;
    if (this._index > max) {
      this.push(null);
    } else {
      this.push('' + this._index);
    }
  }
}
```




### [デバッグ](https://nodejs.org/en/docs/guides/debugging-getting-started)  

```js

// アプリケーションを --inspect 付きで実行すると以下のような表示がされる。
node --inspect app.js
Debugger listening on ws://127.0.0.1:9229/68a0f7dd-3ec3-42aa-a30e-5d1c9e81a45d
For help see https://nodejs.org/en/docs/inspector
Debugger attached.

// 以下のURLにブラウザでmeta情報にアクセスする
// "devtoolsFrontendUrl" を参照しchromeでアクセスするとブレイクポイント等のデバッグができる。
// またはchromeで"chrome://inspect" にアクセスし、configureボタンから "127.0.0.1:9229" を追加する
http://127.0.0.1:9229/json/list

[ {
  "description": "node.js instance",
  "devtoolsFrontendUrl": "chrome-devtools://devtools/bundled/inspector.html  
     ?experiments=true&v8only=true&ws=127.0.0.1:9229/39b0d0bc-3d8f-41b8-8844-d3185157987a",
  "faviconUrl": "https://nodejs.org/static/favicon.ico",
  "id": "39b0d0bc-3d8f-41b8-8844-d3185157987a",
  "title": "./bin/www",
  "type": "node",
  "url": "file://D:_data_study_nodejs_http_expresssample_myapp_bin_www",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/39b0d0bc-3d8f-41b8-8844-d3185157987a"
} ]
```

- [VisualStudioCode](https://qiita.com/kuniken/items/a6341297a97925f5081a)  

    - 実行は三角ボタンで（自動的に--inspect-brkを追加して実行される）
    - ソースを参照してブレイクポイントを設定できる。
    - ステップ実行も可能
    - 最下部の入力欄に変数を入力することで中身の確認が可能

```js

// デバッグで、設定ボタンからlaunch.jsonを作成する。以下を設定。
{
    "version": "0.2.0",
    "configurations": [{
        "type": "node",
        "request": "launch",
        "name": "プログラムの起動",
        "program": "${workspaceFolder}\\bin\\www" // これはpackage.jsonを参照して追加される
    }]
}
```

### express

- RouteとMiddleware
    - [Middleware List](http://expressjs.com/en/resources/middleware.html)
- テンプレートエンジン
    - [Pug](https://pugjs.org)
    - [ejs](http://ejs.co/)
- バリデータ：[express-validator](https://express-validator.github.io/docs/)
- データベース：[MongoDB](https://www.mongodb.com/), Webサービス:[mlab](https://mlab.com)
    - ORM: [Mongoose](https://mongoosejs.com/)

- [3rd Party Middleware](a0a89c7ee0303c0bbaa0)

#### 認証：[Passport](http://www.passportjs.org/docs/)

- [日本語](http://knimon-software.github.io/www.passportjs.org/guide/)
- [Strategy](http://www.passportjs.org/packages/) を変えることで色々な認証方法に対応できる
- OpenID や OAuth にも対応
- 使い方  

    ```js
    // 認証処理はpassport.authenticate()メソッドを使う。引数は使用するStrategy
    // authenticate() は express の middleware　になっている。
    var passport = require('passport');
    var app = require('express');

    // passportの初期化処理
    app.use(passport.initialize());
    // 認証情報を永続化する場合は必要。
    // これを有効にした場合はセッション処理はpassport.session()を使うこと
    app.use(passport.session()); 

    // デフォルトでは、認証処理で失敗すると、「401 Unauthorized」を返す。
    // 成功した場合は、req.user に情報を入れる。
    app.post('/login',
        passport.authenticate('local'),
        function(req, res, next) {
            res.redirect('', req.user);
        }
    );

    // 成功/失敗に応じてリダイレクトする場合
    app.post('/login', passport.authenticate('local', {
        successRidirect: '/top',
        failureRidirect: '/error',
        // failureFlash: true, // 各認証処理側でflashメッセージを設定する場合
        // successFlash: 'Success!!', // 固定でflashメッセージを設定する場合
        // failureFlash: 'Username or Password is wrong' // 固定でflashメッセージを設定する場合
        // failureFlash: true // Verify Callbackで「message」プロパティに指定する場合
        // req.flash()を使うほうがいいみたい？
    }));

    // 通常は認証処理正常終了後、セッションは持続されるが、API等で持続したくない場合はsessionオプションにfalse
    app.post('/login', passport.authenticate('local-login', {
        session: false, // API等の場合は毎回認証をかけたいのでセッションを切る
    });

    // 認可処理の設定
    var authorizeMiddleware = function(req, res, next) {
        if (req.isAuthenticated()) {
            return next();
        }
        res.redirect('/login');
    }
    // 認証が必要なURLに認可処理を追加する
    app.get('/profile',
        authorizeMiddleware, // 認可処理を通過してから次の処理をする
        function (req, res, next) {
            ...
        }
    );

    // ロールを指定したりする場合
    var authorizeMiddleware = function(role){
        function(req, res, next) {
            // ユーザ情報のroleと指定したroleが一致したらOK
            if (req.isAuthenticated() && req.user.role == role) {
                return next();
            }
            res.redirect('/login');
        }
    }
    // 認証が必要なURLに認可処理を追加する
    app.get('/profile',
        authorizeMiddleware('admin'), // 認可処理を通過してから次の処理をする
        function (req, res, next) {
            ...
        }
    );
    ```
  
- Strategy の設定  
    - [passport-local Strategy](https://github.com/jaredhanson/passport-local):ID/PASSを使った認証  

    ```js
    // Strategy には Verify Callbackが必要。
    // Verify Callback は「ユーザ検索」と、「認証情報の保持」を処理する。
    // Verify Callback では、done()メソッドを実行する。
    //  成功時： done(null, user, {message: "ようこそ！"}); // userは保持する情報
    //  失敗時： done(null, false, {message: "認証に失敗しました"});
    //  エラー： done(err);
    var passport = require('passport');
    var LocalStrategy = require('passport-local').Strategy;

    // シンプルなパターン
    passport.use(new LocalStrategy(
        // reqがない
        function(username, password, done) {
            User.findOne({ username: username }, function (err, user) {
              if (err) return done(err);
              if (!user || !user.validPassword(password)) return done(null, false);
              return done(null, user);
            });
        }
    ));

    passport.use(new LocalStrategy({
            usernameField: "user", // POSTするときのNAME属性名（デフォルト: username）
            passwordField: "pass", // POSTするときのNAME属性名（デフォルト: password）
            session: true, // セッションを使用するかどうか
            passReqToCallback: true, // チェック用のコールバックにリクエストオブジェクトを渡すかどうか。
        },
        // passport.authenticate()でfailureFlash: trueの場合はmessageを使ってflashできる
        function(req, username, password, done) {
            User.findOne({ username: username }, function (err, user) {
              if (err) { return done(err); }
              if (!user) {
                return done(null, false, { message: 'Incorrect username.' });
              }
              if (!user.validPassword(password)) {
                return done(null, false, { message: 'Incorrect password.' });
              }
              return done(null, user);
            });
        }
    ));

    // form
    <form action="/login" method="post">
    <div>
        <label>Username:</label>
        <input type="text" name="user"/>
    </div>
    <div>
        <label>Password:</label>
        <input type="password" name="pass"/>
    </div>
    <div>
        <input type="submit" value="Log In"/>
    </div>
    </form>
    ```

#### セッション

    ```js
    // セッションを保持するためにCookieを使用する。Cookieにデータを出し入れする処理を記述する。
    passport.serializeUser(function(user, done){
        // IDだけを保存する
        done(null, user.id);
    });

    passport.deserializeUser(function(user, done){
        User.findById(id, function(err, user) {
            (err)? done(err): done(null, user);
        });
    });
    ```