Express+mongoose+passportでユーザー登録認証&セッション全部やる
使用するミドルウェア
これらを使用するとロジックをコーディングする必要がなくなる
- mongoose
- express-session
- passport
- passport-local
- passport-local-mongoose
- connect-mongo
setup
$ cd ~/ $ express userlogin $ cd userlogin $ npm install $ npm install express-session passport passport-local connect-mongo mongoose passport-local-mongoose
app.js
以下の内容を追加 * sessionをmongoose経由でmongodbで保存 * /api へのアクセスは認証を通す
app.js
var passport = require('passport'); var LocalStrategy = require('passport-local').Strategy; var mongoose = require('mongoose');
app.js
mongoose.Promise = global.Promise; mongoose.connect("mongodb://localhost/dbname", {useMongoClient: true}); // 接続 // session store var MongoStore = require('connect-mongo')(session); app.use(session({ secret: config.secret.session, saveUninitialized: true, resave: true, store: new MongoStore({mongooseConnection: mongoose.connection}) })); app.use(passport.initialize()); app.use(passport.session()); var User = require("./models/user"); passport.use(new LocalStrategy(User.authenticate())); passport.serializeUser(User.serializeUser()); passport.deserializeUser(User.deserializeUser()); // view にセッション情報を渡す場合はこれ app.use(function(req,res,next){ res.locals = { user: req.user }; next(); }); // ユーザー登録&ログイン画面 var user = require('./routes/users'); app.use('/users',user); : :
データベース定義
models/user.js
var mongoose = require('mongoose'); var Schema = mongoose.Schema; var passportLocalMongoose = require('passport-local-mongoose'); var userSchema = new Schema ({ username : String, password : String, active : Boolean }); userSchema.plugin(passportLocalMongoose); module.exports = mongoose.model('User', userSchema);
ユーザーコントローラー
routes/users.js
// 登録フォーム router.get('/signup', function(req, res, next){ res.render('user/signup', { title : 'User Sign up', user : { username : '', password : '' } }); });
// ログインフォーム router.get('/signin', function(req, res, next){ res.render('user/signin', { title : 'User Sign in', user : { username : '', password : '' } }); });
var User = require('../models/user'); // 登録実行 router.post('/signup', function(req, res, next){ User.register( new User({ username: req.body.username, password: req.body.password }), req.body.password, function(err, user){ if (err) { return res.render('user/signup', {title: 'User Sign up Error', user: req.body, error: err}); } var authenticate = User.authenticate(); authenticate(req.body.username, req.body.password, function(err, result) { if(err) { return res.render('user/signup', {title: 'User Sign up Error', user: req.body, error: err}); } res.render('user/index',{title:'User Signed in', result: result}); }); } ); });
// ログイン実行 router.post('/signin', passport.authenticate('local'), function(req, res) { res.redirect('/'); });
動作確認
/signup
登録フォーム username, password をサブミット。成功するとusersコレクションに登録され、sessionにUser情報が入る/signin
ログインフォーム 成功するとsessionに該当のUser情報が入る/mypage
でユーザー認証, セッションの情報を取得できる。認証が不正な場合は status 401でレスポンスする。- req.user に情報が入る(今回は全viewにセッション情報を渡すためres.localsに設定)
create-react-appでreact-router
環境設定
router
フォルダでプロジェクトを作成
$ cd ~/
$ create-react-app router
react-routerインストール
今回はwebサイト
$ cd ~/router $ npm install --save react-router-dom
src/App.js 編集
- 前ページ共通で表示するHeader,Footer
- トップページは/
- /hoge, /moge でアクセスできるようにする
src/App.js
import React, { Component } from 'react'; import { BrowserRouter as Router, Route, Link } from 'react-router-dom'; const style = { common : { margin: 0, padding: "20px", textAlign: "center", width: "100%", height: "60px", backgroundColor: "#000", color: "#EEE" }, main : { fontSize: "1rem", padding: "40px" } } ; const Header = () => (<header style={style.common}>Common Header</header>); const Footer = () => (<footer style={style.common}>Common Footer</footer>); const TopPage = () => (<h1>Top Page</h1>); const HogePage = () => (<h1>Hoge Page</h1>); const MogePage = () => (<h1>Moge Page</h1>); const NoMatch = () => (<h1>Not found!! </h1>); class App extends Component { render() { return ( <Router> <div> <Header /> <nav> <ul> <li><Link to={process.env.PUBLIC_URL + "/"}>top</Link></li> <li><Link to={process.env.PUBLIC_URL + "/hoge"}>hoge</Link></li> <li><Link to={process.env.PUBLIC_URL + "/moge"}>moge</Link></li> </ul> </nav> <div style={style.main}> <Route exact path={process.env.PUBLIC_URL + "/" } component={TopPage} /> <Route path={process.env.PUBLIC_URL + "/hoge"} component={HogePage} /> <Route path={process.env.PUBLIC_URL + "/moge"} component={MogePage} /> <Route component={NoMatch}/> </div> <Footer /> </div> </Router> ); } } export default App;
ここで
<Route exact path={process.env.PUBLIC_URL + "/" } component={TopPage} />
exact
はその後の/hoge
や/moge
にマッチしないようにつける。process.env.PUBLIC_URL
は 本番配置するベースのURLをpackage.jsonのhomepage
設定を反映させるため
起動&確認
$ npm start
localhost:3000
localhost:3000/moge
Not Found の設定
上のApp.jsを以下のように変更&追加
src/Appjs
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom';
const NoMatch = () => (<h1>Not found!! </h1>);
<Switch style={style.main}> <Route exact path={process.env.PUBLIC_URL + "/" } component={TopPage} /> <Route path={process.env.PUBLIC_URL + "/hoge"} component={HogePage} /> <Route path={process.env.PUBLIC_URL + "/moge"} component={MogePage} /> <Route component={NoMatch}/> </Switch>
ブラウザで確認
node.js+React CMS開発-ソフトウェア構成
CMS開発の続きです
- node.js+ReactベースのCMS開発をはじめます
- node.js+React CMS を設計する(外部仕様))
- node.js+React CMS を設計する(データベース仕様))
- node.js+React CMS開発-サーバー雛形を作成
- node.js+React CMS開発-モデル定義
前提条件と目的および採用基準
今回のCMS開発の考え方として
- ビジネス向けのwebアプリケーション開発を想定
- メディアサイトのwebアプリケーションを想定
- 構造化が容易なアーキテクチャー
- ある程度枯れている&開発者が多い
- 手軽に作業ができる&コスト最小
- デザインが自由にできる
- IaaSに載せやすい *管理サイトおよびメディアサイトの両方を想定
- Javascriptだけで開発
Reactは決めの問題
- Vueもいい
- Angularもいい
- JQueryも慣れているしプラグインが豊富
- サービスのNativeアプリは作りたいかも
などなどありますが比較的目にする頻度が高く学習要素の数が少ないReactを採用することとして話をすすめます。 フロントエンドは create-react-app による開発環境が人気があります
管理画面などSPAのフロントエンドは create-react-appによる開発 create-react-app でbuildしたファイルをExpressのstaticファイルとして配置して配信、ExpressサーバーはそのままアプリのAPIサーバーも兼ねてSPA的な動作も実現する。
Node.js + Express
- Javascriptサーバーのデファクトスタンダード
- パッケージが充実してきている
- Expressのmiddlewareがweb開発で必要なものが揃っている
Expressに必要なデファクトスタンダードなmiddlewareを採用する開発 middlewareは別途まとめたい
Database
アプリケーションの内容により適宜選択,express側のmiddlewareやパッケージで吸収できる * MongoDB * MySQL * Postgres * Redis * その他
CMSの記事保存はMongoDB、計数管理などはMySQLに AWSなどで標準的に提供されている運用安定化したもの,携わる技術者の多いものがベスト
構成図
create-react-appのアプリをExpress内で配置、配信する
クライアントReactアプリとWebサーバー&APIサーバーを分けるのが面倒で一箇所(プロセス)でホスティングしたいときがあります
create-react-app で作成したプロジェクトをexpressの /_admin 以下で動作(配信)させてみます
Express環境
$ express together create : together create : together/package.json create : together/app.js create : together/public create : together/routes create : together/routes/index.js create : together/routes/users.js create : together/views create : together/views/index.jade create : together/views/layout.jade create : together/views/error.jade create : together/bin create : together/bin/www create : together/public/javascripts create : together/public/images create : together/public/stylesheets create : together/public/stylesheets/style.css install dependencies: $ cd together && npm install run the app: $ DEBUG=together:* npm start $ cd together/ $ npm install
create-react-appをexpressプロジェクト内で作成
$ cd together $ create-react-app client create-react-app client Creating a new React app in ~/together/client. Installing packages. This might take a couple of minutes. Installing react, react-dom, and react-scripts...
ディレクトリ構造は
together
├── app.js
├── bin
├── client <-- ここにcreate-react-app
├── node_modules
├── package-lock.json
├── package.json
├── public
├── routes
└── views
express の app.js
app.js
var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var index = require('./routes/index'); var users = require('./routes/users'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); // 静的ファイルの配信をURL毎に設定 //---------------------------------------------- app.use('/_admin',express.static(path.join(__dirname, 'client/build'))); // publicのstatic app.use(express.static(path.join(__dirname, 'public'))); app.use('/', index); app.use('/users', users); // 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.render('error'); }); module.exports = app;
clientのreactアプリを express上の /_admin
で動作させたいので
app.use('/_admin',express.static(path.join(__dirname, 'client/build')));
create-react-app の package.json
client/package.json
{ "name": "client", "version": "0.1.0", "private": true, "homepage": "/_admin", // <--- ここにBase URLを設定 "dependencies": { "react": "^16.1.1", "react-dom": "^16.1.1", "react-scripts": "1.0.17" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" } }
create-react-appをbuild
$ cd ~/together/client
$ npm run build
express サーバー起動
$ cd ~/together $ npm start
ブラウザで確認
localhost:3000
expressのデフォルトページ(route / )が表示されます
localhost:3000/users
expressのroute /users が表示されます
localhost:3000/_admin
create-react-app のおなじみのデフォルトページが表示されます。
以上です
create-react-app で Material-UIの画面を表示してみる
create-react-appでプロジェクトを作成し、Material-UIを利用した画面を表示します
環境設定
ローカルの開発環境はこちら * macOS High Sierra(v10.13)でJavaScript開発環境を整える
create-react-app でプロジェクトの雛形作成して起動してみる
$ cd ~/ $ create-react-app material $ cd material $ npm start Compiled successfully! You can now view material in the browser. Local: http://localhost:3000/ On Your Network: http://192.168.11.15:3000/ Note that the development build is not optimized. To create a production build, use npm run build.
ブラウザで確認
material-ui インストール
$ cd ~/material $ npm install --save material-ui + material-ui@0.19.4 added 18 packages in 9.907s
App.js
material-ui の機能を以下のように実装
- maretial-ui の使用するコンポーネントをrequireする
- Appbarのメニューアイコンクリックでメニューを表示
- Reactロゴ画像とwelcome文言をAvatarとテキストのListItemで表示
- To Get started... 文言を Chipで表示
import React, { Component } from 'react'; import logo from './logo.svg'; // import './App.css'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; import AppBar from 'material-ui/AppBar'; import Avatar from 'material-ui/Avatar'; import List from 'material-ui/List/List'; import ListItem from 'material-ui/List/ListItem'; import Menu from 'material-ui/Menu'; import MenuItem from 'material-ui/MenuItem'; import Popover, {PopoverAnimationVertical} from 'material-ui/Popover'; import Chip from 'material-ui/Chip'; class App extends Component { constructor(props) { super(props); this.state = { open: false }; } handleToggle = (event) => { this.setState({ open: !this.state.open, anchorEl: event.currentTarget, }); } handleRequestClose = () => { this.setState({ open: false, }); }; render() { return ( <div className="App"> <MuiThemeProvider> <div> <AppBar title="Title" iconClassNameRight="muidocs-icon-navigation-expand-more" showMenuIconButton={true} onLeftIconButtonTouchTap={this.handleToggle} /> <Popover open={this.state.open} anchorEl={this.state.anchorEl} anchorOrigin={{horizontal: 'left', vertical: 'bottom'}} targetOrigin={{horizontal: 'left', vertical: 'top'}} onRequestClose={this.handleRequestClose} animation={PopoverAnimationVertical} > <Menu> <MenuItem primaryText="Refresh" /> <MenuItem primaryText="Help & feedback" /> <MenuItem primaryText="Settings" /> <MenuItem primaryText="Sign out" /> </Menu> </Popover> </div> <header> <List> <ListItem leftAvatar={<Avatar src={logo} />}> Welcome to React </ListItem> </List> </header> <Chip> To Get started, edit <code>src/App.js</code> and save to reload. </Chip> </MuiThemeProvider> </div> ); } } export default App;
起動
$ cd ~/material $ npm start
ブラウザで確認
localhost:3000
メニューアイコンクリックで できました
express-react-views+Material-UIをSSRで試す
わざわざServer Side Renderingでやるべきことではないですが、やってみました。
やったこと
- express-generator で雛形を作る
- app.jsで view engineに express-react-views を設定
- view 内の layout.jsxとindex.jsx でmaterial-uiを利用
- ブラウザで確認
環境構築
$ express material $ cd material $ npm install $ npm install --save express-react-views react react-dom material-ui
app.js
var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var index = require('./routes/index'); var users = require('./routes/users'); var app = express(); // express-react-views engine setup // --------------------------------------------------------------- app.set('views', __dirname + '/views'); app.set('view engine', 'jsx'); app.engine('jsx', require('express-react-views').createEngine()); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', index); app.use('/users', users); // 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.render('error'); }); module.exports = app;
views/layout.jsx
var React = require('react'); const {getMuiTheme, MuiThemeProvider} = require('material-ui/styles'); class Layout extends React.Component { render() { console.log(this.props); return ( <html> <head><title>{this.props.title}</title></head> <body> <MuiThemeProvider muiTheme={getMuiTheme()}> {this.props.children} </MuiThemeProvider> </body> </html> ); } } module.exports = Layout;
views/index.jsx
var React = require('react'); var Layout = require('./layout'); var {AppBar, RaisedButton, Drawer, MenuItem} = require('material-ui'); class Index extends React.Component { constructor(props) { super(props); this.state = {open: false}; } handleToggle() { this.setState({open: !this.state.open}); } render() { return ( <Layout title={this.props.title}> <AppBar title={this.props.title} iconClassNameRight="muidocs-icon-navigation-expand-more"/> This index page <RaisedButton label="Toggle Drawer" onClick={this.handleToggle} /> <Drawer open={this.state.open}> <MenuItem>Menu Item</MenuItem> <MenuItem>Menu Item 2</MenuItem> </Drawer> </Layout> ); } } module.exports = Index;
サーバー起動
$ npm start
ブラウザで確認
localhost:3000
イベント処理が動作すべきところは反映していません。見た目だけ反映しています。 HTMLは非常に汚いです。
まとめ
- 見た目だけ反映したい場合は有効
- クライアントサイドでイベント処理が必要
- express-react-viewsでは厳しい
詳しくはこちら github.com
こんなのも github.com
Express+mongooseでsession管理
mongooseでセッション管理
パッケージインストール
$ npm install --save mongoose $ npm install --save express-session $ npm install --save connect-mongo
プロジェクト雛形作成
$ express session
app.js
var express = require('express'); var session = require('express-session'); var MongoStore = require('connect-mongo')(session); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var index = require('./routes/index'); var users = require('./routes/users'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); var mongoose = require('mongoose'); const option = { useMongoClient: true }; mongoose.connect("mongodb://localhost/session",option); app.use(session({ secret: 'somthing secret', saveUninitialized: true, resave: true, store: new MongoStore({mongooseConnection: mongoose.connection}) })); app.use('/', index); app.use('/users', users); // 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.render('error'); }); module.exports = app;
routers/users.js
/users へのアクセスでアクセス数をカウントする。
var express = require('express'); var router = express.Router(); /* GET users listing. */ router.get('/', function(req, res, next) { req.session.views++; console.log(req.session); res.send('respond with a resource'); }); module.exports = router;
ブラウザでアクセス
console
$ npm start > session@0.0.0 start ~/session > node ./bin/www
localhost:3000/users
sessionの viewsパラメータに書き込んだカウント数がアクセスごとに上がっていく
Session { cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true}, views: 20 } GET /users 304 12.501 ms - -
mongodbの中身確認
views に 数字が保存されているのを確認
$ mongo MongoDB shell version v3.4.10 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.4.9 > use session switched to db session > db.sessions.find() { "_id" : "1V04JMJx6rsIcgAtl1LJ0ovxaho_H1aB", "session" : "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"views\":20}", "expires" : ISODate("2017-12-04T04:12:29.189Z") } >
node.js+React CMS開発-モデル定義
CMS開発シリーズの続きです
- node.js+ReactベースのCMS開発をはじめます
- node.js+React CMS を設計する(外部仕様))
- node.js+React CMS を設計する(データベース仕様))
- node.js+React CMS開発-サーバー雛形を作成
今回は前回作成した雛形にmongooseスキーマ&モデルの定義を実装します。
目次
- 目次
- mongooseのインストール
- auto increment を行うmongoose-sequenceを導入
- ユーザーパスワードの暗号化のためmongoose-bycryptを導入
- モデル定義をするディレクトリ構造を決める
- 共通module
- モデル定義
- まとめ
mongooseのインストール
$ cd ~/nexpress $ npm install --save mongoose
auto increment を行うmongoose-sequenceを導入
MySQLなどのRDBで使われる自動採番(sequence) を利用したいのでmongoose-sequenceをインストール
$ npm install --save mongoose-sequence
ユーザーパスワードの暗号化のためmongoose-bycryptを導入
パスワードを暗号化して保存するためフィールドの暗号化と照合ができるプラグインmongoose-bycryptをインストール
$ npm install --save mongoose-bycrypt
モデル定義をするディレクトリ構造を決める
nexpress ├── README.md ├── app.js ├── bin ├── config.js <----- アプリケーション共通の設定情報 ├── db.js <----- mongodb接続や,mongoose,puluginに関する共通設定 ├── models/ <----- ここにモデルごとにファイルで配置する ├── node_modules/ ├── package-lock.json ├── package.json ├── public/ <----- 静的ファイル ├── routes/ <----- ルーティング └── views/ <----- ビュー
共通module
config.js
アプリケーション全体を通して使用するパラメータなどを定義します。
// config.js const config = { app: { port: 3000 }, db: { host: 'localhost', port: 27017, name: 'nexpress' }, secret: { key: '#%$SomethingRandomString&%$' } }; module.exports = config;
db.js
- データベースへの接続
- mongoose プラグインの追加
var config = require('./config'); var mongoose = require('mongoose'); // mongoose 利用 const AutoIncrement = require('mongoose-sequence')(mongoose); // 自動採番プラグイン const { db: { host, port, name } } = config; const option = { useMongoClient: true, poolSize: 10 }; mongoose.Promise = global.Promise; mongoose.connect(`mongodb:\/\/${host}:${port}/${name}`, option); // 接続 mongoose.AutoIncrement = AutoIncrement; module.exports = mongoose;
モデル定義
node.js+React CMS を設計する(データベース仕様))で定義したスキーマをnexpress/models
ディレクトリに定義する
user.js
ユーザー情報
var mongoose = require('../db'); // 接続とsequenceの初期化が行われている var Schema = mongoose.Schema; // 定義 var userSchema = new Schema ({ user_id : Number, login : String, password : String }); // user_idは自動採番 userSchema.plugin(mongoose.AutoIncrement, { inc_field: 'user_id'}); // passwordフィールドを自動暗号化 userSchema.plugin(require('mongoose-bcrypt'),{ fields: ['password'] }); module.exports = mongoose.model('User', userSchema);
site.js
サイト情報
var mongoose = require('../db'); var Schema = mongoose.Schema; var siteSchema = new Schema ({ site_id: Number, name : String, title : String }); siteSchema.plugin(mongoose.AutoIncrement, { inc_field: 'site_id' }); module.exports = mongoose.model('Site', siteSchema);
user-site.js
サイトの所有者、利用者、共同編集者ユーザーの登録情報。 ユーザーとサイトのリレーション
var mongoose = require('../db'); var Schema = mongoose.Schema var userSiteSchema = new Schema ({ user_id : { type: Number, refs: 'User' }, site_id : { type: Number, refs: 'Site' }, role : String }); module.exports = mongoose.model('UserSite', userSiteSchema);
category.js
カテゴリ情報
var mongoose = require('../db'); var Schema = mongoose.Schema; var categorySchema = new Schema ({ site_id: { type: Number, refs: Site }, category_id: Number name : String, title : String }); categorySchema.plugin(mongoose.AutoIncrement, { inc_field: 'category_id'}); module.exports = mongoose.model('Category', categorySchema);
category-category.js
カテゴリ同士の親子関係 カテゴリ間リレーション定義
var mongoose = require('../db'); var Schema = mongoose.Schema var contentCategorySchema = new Schema ({ category_id : { type: Number, refs: 'Category' }, content_id : { type: Number, refs: 'Content' } }); module.exports = mongoose.model('ContentCategory', contentCategorySchema);
content.js
コンテンツ、記事情報
var mongoose = require('../db'); var Schema = mongoose.Schema; var contentSchema = new Schema ({ site_id: { type: Number, refs: Site }, content_id: Number, user_id: { type: Number, refs: User}, title : String, summary : String, image: String, publish_date: { type: Date, default: Date.now }, expire_date: { type: Date, default: null }, article: [Schema.Types.Mixed] }); contentSchema.plugin(mongoose.AutoIncrement, { inc_field: 'content_id'}); module.exports = mongoose.model('Content', contentSchema);
content-category.js
コンテンツとカテゴリのリレーション
var mongoose = require('../db'); var Schema = mongoose.Schema var contentCategorySchema = new Schema ({ category_id : { type: Number, refs: 'Category' }, content_id : { type: Number, refs: 'Content' } }); module.exports = mongoose.model('ContentCategory', contentCategorySchema);
tag.js
タグ情報
var mongoose = require('../db'); var Schema = mongoose.Schema; var tagSchema = new Schema ({ site_id : { type: Number, refs: Site }, tag_id : Number, name : String }); tagSchema.plugin(mongoose.AutoIncrement, { inc_field: 'tag_id'}); module.exports = mongoose.model('Tag', tagSchema);
content-tag.js
var mongoose = require('../db'); var Schema = mongoose.Schema var contentTagSchema = new Schema ({ tag_id : { type: Number, refs: 'Tag' }, content_id : { type: Number, refs: 'Content' } }); module.exports = mongoose.model('ContenTag', contentTagSchema);
file.js
添付画像やアップロードされたファイルの情報
var mongoose = require('../db'); var Schema = mongoose.Schema; var fileSchema = new Schema ({ site_id: { type: Number, refs: Site }, file_id: Number, user_id: { type: Number, refs: User}, path : String }); fileSchema.plugin(mongoose.AutoIncrement, { inc_field: 'file_id' }); module.exports = mongoose.model('file', fileSchema);
まとめ
- アプリケーション全体で共有する設定情報を config.js に定義
- データベース接続およびmongoose共通環境を db.js に定義
- モデル
models/*.js
にモデルごとに定義 - 各モデルはコントローラで使用するモデルの必要な分だけrequireする
- データベース接続はpoolして使いまわす
- アプリケーションでは自動採番フィールド(
*_id
)をキーにしてリレーションする - user.passwordは暗号化する
- Index,Validationは別途行う
これで基本的なモデルの定義ができました。
: ├── models │ ├── category-category.js │ ├── category.js │ ├── content-category.js │ ├── content-tag.js │ ├── content.js │ ├── file.js │ ├── site.js │ ├── tag.js │ ├── user-site.js │ └── user.js :
つづく
mongooseでpasswordを暗号化して保存してチェックする方法
Express+mongooseでパスワードを暗号化して保存する方法
mongoose-bcrypt をインストール
npm install mongoose-bcrypt --save
app.js
ルーティングの/ でデータ登録、/checkでパスワードをチェックしてみます
app.js
var mongoose = require('mongoose'); const option = { useMongoClient: true, }; mongoose.Promise = global.Promise; mongoose.connect(`mongodb://localhost/nexpress`, option); // スキーマ定義 var Schema = mongoose.Schema; var userSchema = new Schema ({ user_id : Number, login : String, password : String }); userSchema.plugin(require('mongoose-bcrypt'),{ fields: ['password'] }); var User = mongoose.model('User', userSchema); router.get('/', function(req, res, next) { var user = new User(); user.name = '山田太郎'; user.login = 'ichi-bit'; user.password = 'PassPassPass'; user.save(); res.send('created user'); }); router.get('/check', function(req, res, next) { User.findOne({login: 'ichi-bit'}).then((user)=>{ user.verifyPassword('PassPassPass').then(function(value){ console.log('Encrypted password is ' + value); res.send('Encrypted password is ' + value); }); }); });
動作確認
$ npm start
- localhost:3000 にアクセス
created user
- localhost:3000/check
Encrypted password is true
user.verifyPassword('PassPassPass')
の部分のパスワードを変更すると Encrypted password is false
となって動作していることがわかります。
備考
model.verifyXXXXX()
のXXXXはUppercase のフィールド名で対象のフィールドをチェックできます
node.js+React CMS開発-サーバー雛形を作成
CMS開発シリーズの続きです
今回は Expressのプロジェクト作成とviewエンジンにReactを使用するテンプレートを作成します
目次
開発環境
- macOS High Sierra(v10.13)でJavaScript開発環境を整える
- 各ソフトウェアのバージョンはこの時点で最新にすると以下の通り
node.js v9.2.0
$ nodebrew ls $ nodebrew ls v8.6.0 v8.7.0 v8.8.1 v9.0.0 v9.1.0 v9.2.0 current: v9.2.0 $ node -v v9.2.0
mongodb 3.4.9
$ mongo MongoDB shell version v3.4.10 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.4.9 > db.version() 3.4.9 >
express プロジェクト作成
$ express --version 4.15.5 $ express nexpress $ cd ~/ $ express nexpress warning: the default view engine will not be jade in future releases warning: use `--view=jade' or `--help' for additional options create : nexpress create : nexpress/package.json create : nexpress/app.js create : nexpress/public create : nexpress/routes create : nexpress/routes/index.js create : nexpress/routes/users.js create : nexpress/views create : nexpress/views/index.jade create : nexpress/views/layout.jade create : nexpress/views/error.jade create : nexpress/bin create : nexpress/bin/www create : nexpress/public/javascripts create : nexpress/public/images create : nexpress/public/stylesheets create : nexpress/public/stylesheets/style.css install dependencies: $ cd nexpress && npm install run the app: $ DEBUG=nexpress:* npm start
初期パッケージインストール
基本環境となる依存パッケージをインストールします。 現時点で考えられるものだけをインストール。機能開発時に各々で必要なパッケージを導入していきます。
express-react-views
viewエンジンはReactにする
$ npm install express-react-views react react-dom --save npm notice created a lockfile as package-lock.json. You should commit this file. + react@16.1.1 + react-dom@16.1.1 + express-react-views@0.10.4 added 133 packages in 7.6s
mongoose
mongodbをODM化するミドルウェア
$ npm install mongoose --save + mongoose@4.13.2 added 28 packages in 3.995s
passport
ユーザー認証はpassportを使う(strategyは該当機能開発時に都度導入)
$ npm install passport --save + passport@0.4.0 added 3 packages in 1.871s
pagkage.json
dev用のサーバー起動スクリプトもscripts
に追加
{ "name": "nexpress", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www", "dev": "DEBUG=nexpress:* node-dev ./bin/www" }, "dependencies": { "body-parser": "~1.18.2", "cookie-parser": "~1.4.3", "debug": "~2.6.9", "express": "~4.15.5", "express-react-views": "^0.10.4", "jade": "~1.11.0", "mongoose": "^4.13.2", "morgan": "~1.9.0", "passport": "^0.4.0", "react": "^16.1.1", "react-dom": "^16.1.1", "serve-favicon": "~2.4.5" } }
アプリケーション雛形を作成
app.js に view engineをReact仕様にする設定を追加、express-generatorで作成されたjade環境は削除します。 またviews/以下の .jade を .jsxにリネームして中身も変更します。
jade 削除
$ npm uninstall jade removed 44 packages in 1.096s
.jade を .jsxにrename
views ├── error.jsx ├── index.jsx └── layout.jsx
app.js
viewにReactを使用する
var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var index = require('./routes/index'); var users = require('./routes/users'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jsx'); app.engine('jsx', require('express-react-views').createEngine()); // uncomment after placing your favicon in /public //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', index); app.use('/users', users); // 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.render('error',{title:"Error Page"}); }); module.exports = app;
views/layout.jsx
ページレイアウトのデフォルト。 デザインおよび要素の追加はは後ほど整えるとして以下のような初期設定(タイトル表示と現存するRouteコントローラへのリンク)をする
var React = require('react'); class Layout extends React.Component { render() { return ( <html> <head><title>{this.props.title}</title></head> <body> <header> <h1>nexpress</h1> <nav> <ul> <li><a href="/">index</a></li> <li><a href="/users">users</a></li> </ul> </nav> </header> {this.props.children} </body> </html> ); } } module.exports = Layout;
views/index.jsx
トップページ
タイトルを表示するだけのシンプルなテンプレート
var React = require('react'); var Layout = require('./layout'); class Index extends React.Component { render() { return ( <Layout title={this.props.title}> <div> <h2>{this.props.title}</h2> </div> </Layout> ); } } module.exports = Index;
views/error.jsx
express-generatorでデフォルト作成された error.jadeに習って同じような表示を継承
var React = require('react'); var Layout = require('./layout'); class Error extends React.Component { render() { return ( <Layout title={this.props.title}> <div> <h1>{this.props.message}</h1> <h2>{this.props.error.status}</h2> <pre> {this.props.error.stack} </pre> </div> </Layout> ); } } module.exports = Error;
起動&確認
起動
npm start
もしくは上で追加したdev用スクリプトnpm run dev
$ cd ~/nexpress $ $ npm start nexpress@0.0.0 start /Users/haranaga/nexpress node ./bin/www
ブラウザで確認
URLにアクセスして以下画面が表示されればOK
http://localhost:3000/
これでnode.js+React 環境のアプリケーションの雛形ができました。 ソースはこちら(リポジトリのタグ template-react-view)
つづく
mongooseでauto increment のフィールド定義と動作確認
Express+mongooseで RDBのようなAUTO INCREMENTフィールドを実現する
mongoose-sequence パッケージをインストール
$ npm install --save mongoose-sequence
app.js
var mongoose = require('mongoose'); const option = { useMongoClient: true, }; mongoose.Promise = global.Promise; mongoose.connect(`mongodb://localhost/nexpress`, option); // スキーマ定義 var Schema = mongoose.Schema; const AutoIncrement = require('mongoose-sequence')(mongoose); var siteSchema = new Schema ({ site_id: Number, name : String, title : String }); // site_id フィールドをAutoIncrementする siteSchema.plugin(AutoIncrement, { inc_field: 'site_id'}); var Site = mongoose.model('Site', siteSchema); app.get('/', function(req, res ) { site = new Site(); site.name = 'hoge'; site.title = 'moge'; site.save(); res.send('respond with a resource'); });
ブラウザで / にアクセス 10回ほど
db確認
$ mongo > use nexpress switched to db nexpress > db.sites.find(); { "_id" : ObjectId("5a0c271cb830a7e523d7bedd"), "site_id" : 1, "title" : "moge", "name" : "hoge", "__v" : 0 } { "_id" : ObjectId("5a0c272db830a7e523d7bede"), "site_id" : 2, "title" : "moge", "name" : "hoge", "__v" : 0 } { "_id" : ObjectId("5a0c2766001be7e530483144"), "site_id" : 3, "title" : "moge", "name" : "hoge", "__v" : 0 } { "_id" : ObjectId("5a0c2769001be7e530483145"), "site_id" : 4, "title" : "moge", "name" : "hoge", "__v" : 0 } { "_id" : ObjectId("5a0c29584a2a12e5796ffc10"), "site_id" : 5, "title" : "moge", "name" : "hoge", "__v" : 0 } { "_id" : ObjectId("5a0c295a4a2a12e5796ffc11"), "site_id" : 6, "title" : "moge", "name" : "hoge", "__v" : 0 } { "_id" : ObjectId("5a0c295d4a2a12e5796ffc12"), "site_id" : 7, "title" : "moge", "name" : "hoge", "__v" : 0 } { "_id" : ObjectId("5a0c295f4a2a12e5796ffc13"), "site_id" : 8, "title" : "moge", "name" : "hoge", "__v" : 0 } { "_id" : ObjectId("5a0c29614a2a12e5796ffc14"), "site_id" : 9, "title" : "moge", "name" : "hoge", "__v" : 0 } { "_id" : ObjectId("5a0c29634a2a12e5796ffc15"), "site_id" : 10, "title" : "moge", "name" : "hoge", "__v" : 0 } >
備考
siteSchema.plugin(AutoIncrement, { inc_field: 'site_id'});
ここで inc_fieldを指定しない場合は _id が AIフィールドになるらしい
node.js+React CMS を設計する(データベース仕様)
nexpressスキーマ構成を定義する。
必要最低限の要件を元にシンプルな構造を定義する。
1. Schemaに対する要件
- ユーザー登録がエントリーポイント
- ユーザー
- ユーザーはサイトを複数作成できる
- サイトを新規作成したとき、自動的にそのサイトの最高権限ユーザーとなる
- ユーザーは自他含め複数のサイトに属する事ができる
- サイト内での権限(role)を付与される
- カテゴリ
- サイト単位でカテゴリを定義できる
- カテゴリは階層構造を取る
- カテゴリは他のカテゴリの子となることができる
- タグ
- サイト単位でタグを定義できる
- タグは短いキーワードとして登録する
- コンテンツ
- コンテンツはサイトおよび作成したユーザーに属す
- コンテンツには公開期間を設定できる
- コンテンツは複数のカテゴリに属することができ、かつカテゴリをキーとして検索できる
- コンテンツにはタグを付与でき、かつタグをキーとして検索できる
2. Schema定義
user
ユーザー情報
フィールド名 | 型 | 概要 |
---|---|---|
user_id | String | ユーザーID、登録時に自動生成 |
login | String | ログインアカウント |
password | String | パスワード、暗号化して格納 |
name | String | 表示名 |
site
サイト情報
フィールド名 | 型 | 概要 |
---|---|---|
site_id | String | サイトID、登録時に自動生成 |
name | String | 管理用名称 |
title | String | 表示用サイトタイトル |
user_site
ユーザーとサイトのリレーション
フィールド名 | 型 | 概要 |
---|---|---|
site_id | String | サイトID外部キー |
user_id | String | ユーザーID外部キー |
role | String | 権限 |
content
コンテンツ情報
フィールド名 | 型 | 概要 |
---|---|---|
content_id | String | 記事ID、作成時に自動生成 |
site_id | String | サイトID外部キー |
user_id | String | ユーザーID外部キー |
name | String | 表示用のカテゴリ名称 |
title | String | 記事タイトル |
summary | String | 記事サマリー、リード文 |
image | String | アイキャッチ画像のアップロードパス |
publish_date | Date | 掲載日時 |
expire_date | Date | 掲載終了 |
article | String | 記事本文(JSON形式で本文構造を保存する予定もしくはembed) |
category
カテゴリ情報
フィールド名 | 型 | 概要 |
---|---|---|
category_id | String | カテゴリID、作成時に自動生成 |
site_id | String | サイトID外部キー |
name | String | 表示用のカテゴリ名称 |
slug | String | カテゴリのURLスラッグ |
category_category
カテゴリ間のリレーション
フィールド名 | 型 | 概要 |
---|---|---|
parent_category_id | String | カテゴリID、外部キー、親のカテゴリID |
category_id | String | カテゴリID、外部キー |
content_category
記事とカテゴリ間のリレーション
フィールド名 | 型 | 概要 |
---|---|---|
content_id | String | 記事ID、外部キー |
category_id | String | カテゴリID、外部キー |
tag
タグ情報
フィールド名 | 型 | 概要 |
---|---|---|
site_id | String | サイトID外部キー |
tag_id | String | タグID、作成時に自動生成 |
name | String | タグワード |
content_tag
記事とタグ間のリレーション
フィールド名 | 型 | 概要 |
---|---|---|
content_id | String | 記事ID、外部キー |
tag_id | String | タグID、外部キー |
つづく
node.js+React CMS を設計する(外部仕様)
nexpress(CMS名称)画面構成
1. サイト画面構成
SSR(Server Side Rendering)にする
- トップページ
- 記事リストページ
- カテゴリ(サブカテゴリ)
- 検索結果
- タグ検索
- ランキング
- 新着
- その他条件リスト
- 記事本文ページ
- (ユーザープロフィールや固定ページ)
2. 管理画面構成
SPA(Single Page Application)にする
- Adminトップページ
- ユーザー登録
- ログイン
- ダッシュボード
- サイト管理
- デザイン管理
- ユーザー管理
- カテゴリ管理
- タグ管理
- 記事管理
- 編集
- プレビュー
3. 外部仕様
全画面共通
- PC,tablet向け、SP向け(HTML,AMP)に表示できるようにする
- PC,SPを分けるかレスポンシブにするか選択可能にする
サイト
- 全ページ共通
- ヘッダー
- フッター
- 広告のタグ類
- その他全ページ共通表示要素
- トップページ
- 新着記事など様々な条件で抽出したリストを複数表示。指定した順にに表示される
- 記事リストには記事のアイキャッチとタイトル、本文の一部、カテゴリ名が表示される
- その他誘導するためのリンクを表示
- 記事本文
- 本文には以下の要素を含を含む
- 見出し
- 小見出し
- 画像、画像キャプション、Youtube、Markdown、画像スライド、記事引用、リンク
- 本文をページ分け
- 記事のPVをカウントする
- その他記事への誘導
- SNSへの連携
- 本文には以下の要素を含を含む
- 新着、ランキング、カテゴリ、検索、タグ検索記事リスト用途のページ
- 各条件で抽出した記事リストを表示する
つづく
node.js+ReactベースのCMS開発をはじめます
なぜCMSか
現在CMSのデファクトスタンダードはwordpressです。PHPが動作するサーバーであれば簡単に設置でき、豊富なプラグイン、デザインテーマがあって使いやすくなっています。 これに匹敵できるようなCMSをnode.jsベースで開発してみます。CMSはWebアプリケーションの機能を幅広く実装するため、開発事例を網羅するためにCMSを題材に選びました。
なぜnode.jsか
Webアプリケーションの開発においてフロントエンドの複雑化に対応することが必須となっています。 フロントエンドで動的な機能を実現するためにJavascriptの使用が標準となるため、バックエンドの実装言語にかかわらずJavascriptは必ず通る道です。 それならばバックエンドもJavascriptで構築すべきと考えました。node.jsは一般的に言われているメリットもあるためnode.jsをベースとします。 使用言語をjavascriptに統一することによりエンジニアの人員採用のチャンスも大きくなります。
なぜReactか
よく比較されるAngular,Vueなど選択肢があるなか、どれをとっても有用な点があり、一概にどれが良いと言うことができません。 個人的にはとっつきやすいVueが好みですが、スマホアプリの開発を視野に入れた場合にReact Nativeの活用が考えられるため、Reactを使います。 またcreate-react-appのリリースにより開発環境が簡単に作れることもReactを使う理由です。 今回開発するCMSは画面のカスタマイズを柔軟かつ安定的にするためweb部品をコンポーネント化することも念頭にあり、効果的に利用できそうです。
開発コンセプト
- node.js ベース
- React を利用
- 独自コードをなるべく少なくするためスタンダードなライブラリを利用する
- 大規模組織、大規模トラフィックを対象とする
- すべてjavascriptで構築
- 運用インフラはAWSを想定
- 開発環境はMacOSのlocal環境
- OSSとして公開
開発の進め方
- 実際の設計手順で行います。
- 企画、機能要件は最初に決めずに進行中でも変更可能にします。
プロジェクトの名前
nexpressとします
つづく
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