JavaScriptだけでWebサイト開発

node.js + Express + mongodb系 + React でサービスを作るメモ

React(create-react-app)とExpress(REST API)で画像アップ掲示板を作ってみる(2)

掲示板の続きです。

これまではテキストのみを投稿する掲示板でしたが、画像もアップできるようにします。 前回とExpress(REST_API)で画像アップ掲示板を作ってみる(1))はサーバーサイドのAPIをファイルアップロードに対応しました。

今回はフロントエンドのアップロードインターフェースをReactで実装します。

作るもの

  • これまで作ってきた掲示板を拡張
  • フロントエンドはcreate-react-appで作成
  • バックエンドはExpress
  • 書き込みと同時に画像をアップロードできるようにする
  • 書き込み一覧に画像を表示する

0. これまで作ったファイル構成

kakikomi
  ├── kakiapi                     サーバー API
  │   ├── app.js
  │   ├── bin
  │   ├── node_modules
  │   ├── package-lock.json
  │   ├── package.json
  │   ├── public
  │   ├── routes
  │   └── views
  └── kakifront                   フロントエンド
      ├── README.md
      ├── build 
      ├── node_modules
      ├── package-lock.json
      ├── package.json
      ├── public
      └── src

作業は kakikomi/ 以下で行います

1. サーバーサイドAPIのapp.js全部

kakiapi/app.js

var express = require('express');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var mongoose = require('mongoose');
var restify = require('express-restify-mongoose');

var app = express();
var router = express.Router();

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static('public'));

mongoose.connect('mongodb://localhost/app1',{useMongoClient:true});
restify.serve(router, mongoose.model('Post', new mongoose.Schema({
  name: { type: String },
  kakikomi: { type: String },
  filename: {type: String}
})));
app.use(router);

/////////////////////////
///  追加
/////////////////////////
var path = require('path');  // ファイルの拡張子を取得するのに使う
var multer  = require('multer');
// 格納場所と新しくつけるファイル名の定義
var storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'public/uploads');
  },
  filename: function (req, file, cb) {
    cb(null, file.fieldname + '_' + Date.now() + path.extname(file.originalname));
  }
});
var upload = multer({ storage: storage });
// ルーティング
app.post('/api/upload', upload.single('image'), function (req, res, next) {
  delete req.file.buffer; // responseには入れない
  res.json(req.file); // 取得した情報を返す
});
/////

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.send({error:'error'});
});

module.exports = app;

説明

  • データベースはmongodb
  • ODMはmongoose
  • 掲示板用のコレクションはpostsでフィールドはkakikomi,name,filenama
  • filename にアップされた画像のパスを保存する
  • ファイルアップロード用のミドルウェアはmulter
  • /api/upload にファイルをPOSTされると画像を保存して、ファイル情報を返却する

2. フロントエンド(create-react-app)の実装

以前作成したテキスト版掲示板のフロントエンドを拡張します

kakifront/src/Api.js

import React, { Component } from 'react';
import './App.css';


/**
* 画像アップ用のコントロール
*/
class ImageUpload extends Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange = (e) => {
    e.preventDefault();
    var form = new FormData();
    form.append('image',e.target.files[0]);

    fetch('/api/upload', {
      method: "POST",
      body: form
    }).then(
      response => response.json()
    ).then(
      data => {
      this.props.onChange(data.filename);
    });
  }

  render() {
    return (
      <div>
        <input type="file" name="image" onChange={this.handleChange} />
      </div>
    );
  }
}

/**
* 書き込み1件
*/
class Post extends Component {
  render() {
    var image = '';
    if (this.props.post.filename) {
      image = (<img src={`http://localhost:3000/uploads/${this.props.post.filename}`} alt={this.props.post.filename} />);
    }
    return (
      <div className="kakikomi">
        <div>{this.props.post.kakikomi}</div>
        {image}
        <p className="name">{this.props.post.name}</p>
      </div>
    );
  }
}

/**
* 書き込みリスト
*/
class List extends Component {
  // 書き込みリスト
  render() {
    const posts = this.props.posts;
    var list = [];
    for (var i in posts) {
      list.push( <li key={i}><Post post={posts[i]} /></li> );
    }
    return (<ul>{list}</ul>);
  }
}

/**
* 投稿フォーム
*/
class UpForm extends Component {

  constructor(props) {
    super(props);
    this.state = {
      kakikomi:'',
      name:'',
      message:'書き込んでください',
      filename: ''
    };
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.handleFile = this.handleFile.bind(this);
  }

  handleSubmit = (e) => {
    e.preventDefault();
    fetch('/api/v1/Post', {
      method : "POST",
      headers: {
        "Content-Type": "application/json",
        "Accept": "application/json"
      },
      body: JSON.stringify({
        kakikomi: this.state.kakikomi,
        name : this.state.name,
        filename: this.state.filename
      })
    }).then(data => {
      this.setState({
        message:'アップしました',
        kakikomi: '',
        name: '',
        filename: ''
      });
    }).then(() => {this.props.onSubmit()});
  }

  handleChange = (e) => {
    this.setState({message:''});
    this.setState({ [e.target.name] :e.target.value});
  }

  handleFile = (filename) => {
    console.log(filename);
    this.setState({filename : filename});
  }

  render() {
    return  (
      <div className="form">
        <p>{this.state.message}</p>
        <form onSubmit={this.handleSubmit}>
          <label>内容</label>
          <textarea name="kakikomi" value={this.state.kakikomi} onChange={this.handleChange}></textarea>
          <label>名前</label>
          <input type="text" name="name" value={this.state.name}  onChange={this.handleChange} />
          <button type="submit">投稿</button>
        </form>
        <label>画像</label>
        <ImageUpload onChange={this.handleFile}/>
      </div>
    );
  }
}

/**
* ヘッダー
*/
class Header extends Component {
  render() {
    return (
      <header>
      <h1>掲示板</h1>
      </header>
    );
  }
}

/**
* 全画面
*/
class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      posts: []
    };
    this.updatePosts();

    this.updatePosts = this.updatePosts.bind(this);
  }

  updatePosts(e) {
    getPost((data) => {
      this.setState({posts: data});
    });
  }

  render() {
    // this.updatePosts();
    return (
      <div className="App">
      <Header />
      <UpForm onSubmit={this.updatePosts}/>
      <List posts={this.state.posts}/>
      </div>
    );
  }
}

/**
* サーバーから書き込み一覧を取得する
* @method getPost
* @param  {Function} callback データ取得後のコールバック
* @return {[type]}
*/
function getPost(callback) {
  fetch('/api/v1/Post?sort={"_id":-1}')
  .then(response => response.json())
  .then((data) => {callback(data)});
}

export default App;

やったこと

  • 画像アップ用のフォームコントロールImageUploadを追加
    • 書き込み用のForm(UpForm)の外に配置してファイル指定のイベントで先にアップロードしてサーバーファイルパスを返す
  • UpFormではstateにfilenameをもち、アップロード先のファイルパスを保持するようにする
  • 書き込み投稿時に内容、名前とファイルパスをサーバーAPIにポストする
  • 表示時にfilenameがあればimageタグで画像を表示する
    • この際画像をホスティングしているのはサーバーAPIなので localhost:3000をURLに利用
    • サーバーサイドAPIではstaticファイルのホスティングをpublicディレクトリに指定してある

これで画像アップロード掲示板ができました。

3. まとめ

  • サーバーAPIとフロントエンドを分けて作りました
  • サーバーAPIは Express+MongoDB
    • mongoose(モデル)
    • express-restify-mongoose(モデル定義をCRUD REST API化)
    • multer(アップロード)
  • フロントエンド
    • create-react-app
  • サーバーをlocalhost:3000で起動
  • フロントエンドはlocalhost:3001テスト

ソース

ソースファイルはこちらです(tag:v2) github.com

©ichi-bit