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
)の外に配置してファイル指定のイベントで先にアップロードしてサーバーファイルパスを返す
- 書き込み用のForm(
- 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