Node.js 再入門メモ(その 6)

今日のテーマ

複数のブラウザ間で情報をリアルタイムに共有したり、通知を送受信したりするための仕掛けを試して、「リアルタイム「いいね」ボタン」をつくる。

壮大な完成イメージはこんなの。 とってもスタイリッシュ。

f:id:tercel_s:20170827212554p:plain

http://localhost:3001/ にアクセスしているすべてのブラウザで、表示が連動している。 つまり、片方のボタンを押しただけで他方の情報まで同時に更新される(時間的な遅延なしで)。

昨日までのモジュールに加え、SocketIO を使う。

まだ「初めてやってみた」「書いてみたら動いた」レベルなので、コード中のアンチパターンが多そう。 取り敢えず動く状態にするまでに時間を掛けすぎてしまい、日記としてまとめる気力が失せてしまった。 そんなわけで、ここから先日本語はほとんど登場しない。 まぁいつもの事だが。

初期設定

npm 初期化

$ npm init -y

ライブラリの導入

$ npm i --save express
$ npm i --save superagent
$ npm i --save socketio socket.io-client
$ npm i --save react react-dom
$ npm i --save-dev webpack
$ npm i --save-dev babel-loader babel-core
$ npm i --save-dev babel-preset-es2015 babel-preset-react
package.json (抜粋)
{
  (略)
  "scripts": {
    "build": "webpack",
    "start": "node server.js"
  },
  (略)
  "dependencies": {
    "express": "^4.15.4",
    "react": "^15.6.1",
    "react-dom": "^15.6.1",
    "socket.io-client": "^2.0.3",
    "socketio": "^1.0.0",
    "superagent": "^3.6.0"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "webpack": "^3.5.5"
  }
}

webpack の設定

ディレクトリとファイルの作成

$ mkdir public src
$ touch webpack.config.js
webpack.config.js
const path = require('path')
module.exports = {
  entry: path.join(__dirname, 'src/index.js'),
  output: {
    path: path.join(__dirname, 'public'),
    filename: 'app.js'
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /.js$/,
        loader: 'babel-loader',
        options: {
          presets: ['es2015', 'react']
        }
      }
    ]
  }
}

アプリケーションの実装

サーバサイドのコーディング

server.js
// ---------------------------------
// HTTP サーバ
const express = require('express')
const app = express()

const server = require('http').createServer(app)

app.use(express.static('./public'))

// 初期表示用に最新の値を取得
let counter = 0
app.get('/api/getCountValue', 
  (req, res) => {
    res.json({
    counter: counter
  })
})
server.listen(3001)

// ---------------------------------
// WebSocket サーバ
const socketio = require('socket.io')
const io = socketio.listen(server)

// 接続
io.on('connection', (socket) => {
  console.log('ClientID: ', socket.client.id)

  // countUp を受信したら、
  // カウントアップして全クライアントにブロードキャスト
  socket.on('countUp', () => {
    io.emit('newCountValue', ++counter)
  })
})

クライアントサイドのコーディング

build/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta http-equiv="content-type"
    content="text/html; charset=UTF-8">
    <title>Hello!</title>
  </head>
  <body>
    <div id="Content"></div>
    <script src="./app.js"></script>
  </body>
</html>
src/index.js
import React, {Component} from 'react'
import ReactDOM from 'react-dom'
import request from 'superagent'
import socketio from 'socket.io-client'
const socket = socketio.connect('http://localhost:3001')

class CountUpButton extends Component {
  constructor(props) {
    super(props)
    this.state = {
      text: !props || !props.text ? '' : props.text
    }
  }

  // WebSocket からの配信結果を親コンポーネントに通知
  componentWillMount() {
     socket.on('newCountValue', (newValue) => {
      this.props.updateCounter(newValue)
    })
  }

  // カウンタの更新処理
  // WebSocket サーバにイベントを送信
  countUp() {
    socket.emit('countUp', {})
  }

  render(props) {
    return (
      <button type='button'
              onClick={e => this.countUp(e)}>
        {this.state.text}
      </button>
    )
  }
}

class App extends Component {
  // コンストラクタ
  constructor(props) {
    super(props)
    this.state = { counter: 0 }
  }

  // 初期データ取得(ここだけ Ajax)
  componentWillMount() {
    request
      .get('/api/getCountValue')
      .end((err, data) => {
        if(err) { return }

        const resultJSON = data.body
        const initValue = resultJSON.counter
        this.setState({
          counter: initValue
        })
      })
  }

  // 子コンポーネント(ボタン)から呼ばれる
  // カウンタ更新処理
  handleChange(newValue) {
    this.setState({ counter: newValue })
  }

  // コンポーネント描画
  render() {
    return(
      <div>
        <h3>{this.state.counter} いいね</h3>
        <CountUpButton 
          text='いいね!' 
          updateCounter={e => this.handleChange(e)}/>
      </div>
    )
  }
}

ReactDOM.render(<App />, 
  document.getElementById('Content'))

実行

$ npm run build
$ npm start