React(create-react-app)とExpress(REST API)で画像アップ掲示板を作ってみる(1)
前回の続きです。
前回まではテキストのみを投稿する掲示板でしたが、画像もアップできるようにします。
作るもの
- これまで作ってきた掲示板を拡張
- フロントエンドは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にファイルのアップロード機能を追加する
Expressで標準的なmulterを使用します。作業はkakiapi/
で行います。
$ cd kakiapi $ npm install --save multer + multer@1.3.0 added 15 packages in 2.988s
アップロードされたファイルを格納する場所を作ります
$ mkdir public/uploads
app.js
に以下を追加
///////////////////////// /// 追加 ///////////////////////// 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; console.log(req.file); res.send(req.file); // 取得した情報を返す }); //////////////////////////
やったこと
- multerのoptionにファイル保存場所と新しく付与するファイル名の定義をstorageに設定
- filename は フィールド名+時刻+拡張子
- レスポンスはアップされたファイルの情報をそのまま返す
- mongooseのスキーマに filename フィールドを Stringで追加しておく
動作確認
- postmanで http://localhost:3000/api/upload に対してファイルをアップしてみる。コンソールでアップされた情報が確認できます
{ fieldname: 'image', originalname: 'ほげのふぁいる.jpg', encoding: '7bit', mimetype: 'image/jpeg', destination: 'public/uploads', filename: 'image-1510280080562.jpgs', path: 'public/uploads/image-1510280080562.jpg', size: 20617 } POST /api/upload 200 14.944 ms - 238
- ファイル確認
$ ls -l public/uploads total 48 -rw-r--r-- 1 ichibit staff 20617 11 10 11:14 image-1510280080562.jpg
次回はフロントエンドを作ります
create-react-app で React フロントエンドの掲示板を作ってみる(投稿)
前回の続きです。
フォームを表示して、投稿をできるようにします。
作るもの
- これまで作ってきた掲示板と同じもの
- フロントエンドが今回作るもの。前回作った掲示板APIと連携して書き込みデータをやりとりする。
サーバーAPIを立ち上げておく
前前回作った「掲示板REST API」を localhost:3000で立ち上げておきます。このサーバーと 今回作るアプリが連携します。
開発環境
- MacOS High Sierra
- 詳細はこちら
1. App.jsにコンポーネントを追加
App.js
import React, { Component } from 'react'; import './App.css'; /** * 書き込み1件 */ class Post extends Component { render() { return ( <div className="kakikomi"> <div>{this.props.post.kakikomi}</div> <p className="name">{this.props.post.name}</p> </div> ); } } /** * 書き込みリスト */ class List extends Component { // 書き込みリスト render() { const posts = this.props.posts; var list = []; // map 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:'書き込んでください' }; this.handleSubmit = this.handleSubmit.bind(this); this.handleChange = this.handleChange.bind(this); } handleSubmit = (e) => { e.preventDefault(); // サーバーAPIにJsonでPOSTする 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 }) }).then(data => { this.setState({ message:'アップしました', kakikomi: '', name: '' }); }).then(() => {this.props.onSubmit()}); } // フォームの値とstateをbindする役割 handleChange = (e) => { this.setState({message:''}); this.setState({ [e.target.name] :e.target.value}); } 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> </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() { 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;
やったこと
- コンポーネント
- Header : ヘッダー
- UpForm : 投稿フォーム
- List : 書き込みリスト
- フォームの入力と同時ににstateに値を更新
- フォームのイベント処理でAPIに書き込みをPOSTする
ブラウザでの動作
- 初期表示でフォームと書き込み一覧をを表示
- フォームに入力して「投稿」押すとリストのトップに追加される
2. まとめ
- サーバーサイドのAPIとcreate-react-appで作成したブラウザクライアントが連携した掲示板
- サーバーサイドAPIは前前回作った掲示板API
- クライアントをスマホアプリにする場合はReact Nativeにする
github
ソースはこちらにあります(tag:v1)
create-react-app で React フロントエンドの掲示板を作ってみる(リスト表示)
create-react-appはFacebookが開発したReact web アプリケーションの環境構築パッケージです。
React はそのコードを書いて動かす以前に前提となる開発環境を整備するのに苦労することから適用を断念しがちでした。このツールによって導入がしやすくなりました。
- 作るもの
- 開発環境
- 1. create-react-app のインストール
- 2. プロジェクト作成
- 3. package.jsonにサーバー情報を追加
- 4. App.js編集
- 5. buildして配置
- 6. まとめ
作るもの
- これまで作ってきた掲示板と同じもの
- フロントエンドが今回作るもの。前回作った掲示板APIと連携して書き込みデータをやりとりする。
サーバーAPIを立ち上げておく
前回作った「掲示板REST API」を localhost:3000で立ち上げておきます。このサーバーと 今回作るアプリが連携します。
開発環境
- MacOS High Sierra
- 詳細はこちら
1. create-react-app のインストール
$ npm install -g create-react-app
2. プロジェクト作成
React アプリケーションのプロジェクトを生成します。
$ create-react-app kakifront
このようなファイル構成で作られます。
. ├── README.md ├── node_modules/ ├── package-lock.json ├── package.json ├── public/ │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src/ ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── registerServiceWorker.js
でコンパイル&httpサーバーが起動します
$ npm start Compiled successfully! You can now view kakifront 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.
localhost:3000にアクセスするとデフォルトのReact表示画面が表示されます。 (今回はサーバーAPI側が3000で立ち上がっていますので、3001でアプリが立ち上がります。)
3. package.jsonにサーバー情報を追加
package.json
{ "name": "kakifront", "version": "0.1.0", "private": true, "homepage": "http://localhost:8080/test/react", "dependencies": { "react": "^16.0.0", "react-dom": "^16.0.0", "react-scripts": "1.0.17" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "proxy" : "http://localhost:3000" }
"proxy" : "http://localhost:3000"
を記述することによりコードの中で fetch(/api/v1/***)
と書くことができます
4. App.js編集
App.js
import React, { Component } from 'react'; import './App.css'; class Post extends Component { render() { return ( <div className="kakikomi"> <div>{this.props.post.kakikomi}</div> <p className="name">{this.props.post.name}</p> </div> ); } } class List extends Component { // 書き込み一覧 constructor(props) { super(props); this.state = {posts:[]}; } // 書き込み一覧をサーバーから取得 componentDidMount() { fetch('/api/v1/Post?sort={"_id":-1}') .then(response => response.json()) .then(data => this.setState({ posts: data })); } render() { const {posts} = this.state; var list = []; for (var i in posts) { list.push( <li key={i}><Post post={posts[i]} /></li> ); } return (<ul>{list}</ul>); } } class Header extends Component { render() { return ( <header> <h1>掲示板</h1> <nav> <ul> <li><a href="/">トップ</a></li> <li><a href="#">投稿</a></li> </ul> </nav> </header> ); } } class App extends Component { render() { return ( <div className="App"> <Header /> <List /> </div> ); } } export default App;
やったこと
- タイトルとメニュー表示のHeaderコンポーネント
- 書き込みリスト List コンポーネント
- 1書き込みの Postコンポーネント
- List 表示時にサーバーから書き込み一覧を取得し state.postsに入れる
スタイルはApp.cssにまとめる
App.css
body { margin: 0; padding: 50px; font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; } header { margin-bottom:40px; } ul { list-style: none; } nav ul { display:flex; } nav ul li { margin-right: 20px; } .kakikomi { border: 1px solid #888; padding:20px; margin-bottom: 40px; } .kakikomi div { } .kakikomi .name { font-size:0.8em; color: #555; }
ブラウザでの動作
- http://localhost:3001 にアクセスすると書き込みリストが表示されます。
5. buildして配置
$ npm run build
で build/
以下にproductionファイルが生成されます。このフォルダを静的HTTPサーバー(今回は 別に立ち上げている apacheのlocalhost:8080/test/react/) に配置して動作確認
package.json
で
"homepage": "http://localhost:8080/test/react",
と記述するとbuild時に生成されるHTMLに適した相対パスを埋め込んでくれます。
6. まとめ
- create-react-app でプロジェクトをつくる
- package.json の homepage, proxy を設定して、配置位置とサーバーサイドのbaseURLを設定
- App.jsで必要なコンポーネントを定義
- List表示時にサーバーAPIの書き込みリストを取得して表示
トップページで書き込み一覧を表示することができました
ExpressでMongoDBへのCRUD機能を実現するRESTful Web APIを作ってみる
作るもの
- Express内で定義したMongoDBのスキーマに対してデータをCreate,Read,Update,DeleteするWebAPIインターフェース
- これまで作ってきた掲示板アプリのWebAPIを作ってみる
開発環境
- MacOS High Sierra
- 詳細はこちら
1. Express プロジェクト作成
express-generator で雛形を作成します。テンプレートエンジンはデフォルトのままで作成。 これまで作ってきた掲示板機能のAPIを想定しています。
$ express kakiapi warning: the default view engine will not be jade in future releases warning: use '--view=jade' or '--help' for additional options create : kakiapi create : kakiapi/package.json create : kakiapi/app.js create : kakiapi/public create : kakiapi/views create : kakiapi/views/index.jade create : kakiapi/views/layout.jade create : kakiapi/views/error.jade create : kakiapi/routes create : kakiapi/routes/index.js create : kakiapi/routes/users.js create : kakiapi/bin create : kakiapi/bin/www create : kakiapi/public/javascripts create : kakiapi/public/images create : kakiapi/public/stylesheets create : kakiapi/public/stylesheets/style.css install dependencies: $ cd kakiapi && npm install run the app: $ DEBUG=kakiapi:* npm start $ cd kakiapi/ $ npm install npm WARN deprecated jade@1.11.0: Jade has been renamed to pug, please install the latest version of pug instead of jade npm WARN deprecated transformers@2.1.0: Deprecated, use jstransformer npm notice created a lockfile as package-lock.json. You should commit this file. added 103 packages in 3.134s
以下フォルダが作成されました
. ├── app.js ├── bin/ │ └── www ├── node_modules/ ├── package-lock.json ├── package.json ├── public/ │ ├── images │ ├── javascripts │ └── stylesheets ├── routes/ │ ├── index.js │ └── users.js └── views/ ├── error.jade ├── index.jade └── layout.jade
2. express-restify-mongoose をインストール
express-restify-mongoose mongooseで定義したオブジェクトモデルに対してcrud機能を実現するmiddleware mongooseと合わせてインストールします
$ cd kakiapi $ npm install mongoose express-restify-mongoose --save
3. app.jsを編集
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()); /// 追加 mongoose.connect('mongodb://localhost/app1',{useMongoClient:true}); restify.serve(router, mongoose.model('Post', new mongoose.Schema({ name: { type: String }, kakikomi: { type: String } }))); app.use(router); /// // 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;
やったこと
- express-generator で作成されたコードから以下を削除
- favicon関連
- view engine に関する記述
- public/以下をstaticファイル配信する設定
- index,usersのコントローラ
- mongoose 接続を追加
- express router と mongoose スキーマモデルをexpress-restify-mongooseで対応つける
これだけでデータベースのコレクションposts
に対するCRUD機能が使えるようになります。
データ操作の機能や検索条件、その他オプションはこちらで確認できます。
express-restify-mongoose
4. まとめ
- たったこれだけのコードでMongoDBへのCRUD機能を持ったRESTful APIが実現できました。
node.js+Express+mongooseの掲示板をReact view(SSR)で実装
前回の「node.js+Express+mongooseで掲示板を作ってみる」ではview engineをpugで実装しました。 これを機能はそのままに Reactをview engineとして使ってみます。
目次
- 目次
- 1. express-react-views パッケージインストール
- 2. app.js で view engine を変更する
- 3. ViewテンプレートをReact,JSXで記述
- 4. Reactテンプレートへのスタイルの適用
- 5. まとめ
1. express-react-views パッケージインストール
express-react-views は React, React-DOM を用いたテンプレートをサーバーサイドでコンパイル(トランスパイル)してHTMLを出力するパッケージです。
他のviewエンジンと役割は同じです。
プロジェクトディレクトリ内kakiapp/
でインストールします
$ cd kakiapp
$ npm install express-react-views react react-dom
+ express-react-views@0.10.4
+ react@16.0.0
+ react-dom@16.0.0
前回使ったpubは削除します
$ npm uninstall pug
2. app.js で view engine を変更する
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 up = require('./routes/up'); var app = express(); //// 1. mongoose connection var mongoose = require('mongoose'); // mongoose 利用 mongoose.Promise = global.Promise; mongoose.connect('mongodb://localhost/app1', {useMongoClient: true}); // 接続 //// end 1 //// 2. mongoose model 定義 // Schema 定義 var postSchema = mongoose.Schema({ kakikomi: String, name: String }); // model var Post = mongoose.model('Post',postSchema); // Post modelを router moduleで共有する app.set('Post',Post); //// end 2 // 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); //// 3. 投稿画面 app.use('/up', up); //// end 3 // 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;
今回の変更はviewにpugを使用していた以下のところ
// view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug');
を
// view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jsx'); app.engine('jsx', require('express-react-views').createEngine());
と変更しました。views/ ディレクトリの テンプレートを全てをjsxで書き直します
3. ViewテンプレートをReact,JSXで記述
スタイルなしバージョン
ページレイアウト(共通)
layout.jsx
var React = require('react'); class Layout extends React.Component { render() { return ( <html> <head><title>{this.props.title} - 掲示板</title></head> <body> <header> <h1>掲示板</h1> <nav> <ul> <li><a href="/">トップ</a></li> <li><a href="/up">投稿</a></li> </ul> </nav> </header> {this.props.children} </body> </html> ); } } module.exports = Layout;
トップページ
index.jsx
var React = require('react'); var Layout = require('./layout'); class Index extends React.Component { render() { var list = []; for (var i in this.props.posts) { list.push( <li> <div className="box"> <div>{this.props.posts[i].kakikomi}</div> <p className="name">{this.props.posts[i].name}</p> </div> </li> ); } return ( <Layout title={this.props.title}> <div> <h2>{this.props.title}</h2> <ul> {list} </ul> </div> </Layout> ); } } module.exports = Index;
投稿ページ
up.jsx
var React = require('react'); var Layout = require('./layout'); class Up extends React.Component { render() { return ( <Layout> <h2>投稿フォーム</h2> <form action="/up" method="POST"> <label>内容</label> <textarea name="kakikomi"></textarea> <label>名前</label> <input type="text" name="name" /> <button type="submit">投稿</button> </form> </Layout> ); } } module.exports = Up;
投稿後ページ
up_post.jsx
var React = require('react'); var Layout = require('./layout'); class UpPost extends React.Component { render() { return ( <Layout> <h2>投稿しました</h2> <p>{this.props.kakikomi}</p> <p>{this.props.name}</p> </Layout> ); } } module.exports = UpPost;
4. Reactテンプレートへのスタイルの適用
layout.jsx
var React = require('react'); var style = { header: {marginBottom:"40px"}, navul: {display:"flex",listStyle:"none" }, navli: {marginRight:"20px"} } class Layout extends React.Component { render() { return ( <html> <head> <title>{this.props.title} - 掲示板</title> <link rel="stylesheet" href="/stylesheets/style.css" /> </head> <body> <header style={style.header}> <h1>掲示板</h1> <nav> <ul style={style.navul}> <li style={style.navli}><a href="/">トップ</a></li> <li style={style.navli}><a href="/up">投稿</a></li> </ul> </nav> </header> {this.props.children} </body> </html> ); } } module.exports = Layout;
index.jsx
var React = require('react'); var Layout = require('./layout'); var style = { ul: {listStyle:"none"}, name: {fontSize:"0.8em",color:"#555"}, box: {border:"1px solid #888", padding:"20px", marginBottom:"40px"} } class Index extends React.Component { render() { var list = []; for (var i in this.props.posts) { list.push( <li> <div style={style.box}> <div>{this.props.posts[i].kakikomi}</div> <p style={style.name}>{this.props.posts[i].name}</p> </div> </li> ); } return ( <Layout title={this.props.title}> <div> <h2>{this.props.title}</h2> <ul style={style.ul}> {list} </ul> </div> </Layout> ); } } module.exports = Index;
up.jsx
var React = require('react'); var Layout = require('./layout'); var style = { textarea: {width:"100%", height:"60px"}, button: {marginTop:"30px"} } class Up extends React.Component { render() { return ( <Layout> <h2>投稿フォーム</h2> <form action="/up" method="POST"> <label>内容</label> <textarea name="kakikomi" style={style.textarea}></textarea> <label>名前</label> <input type="text" name="name" /> <button type="submit" style={style.button}>投稿</button> </form> </Layout> ); } } module.exports = Up;
やったこと
- layout.jsxでは外部ファイルstyle.css と 埋め込みを試しています
- その他はstyleをオブジェクトで定義し、各コンポーネント内に埋め込み
- スタイルのパラメータのハイフンを含むものはローワーキャメルケースにする必要がある。
ブラウザでの動作
- 前回の「node.js+Express+mongooseで掲示板を作ってみる」と全く同じになりました。
5. まとめ
- express-react-viewsをインストール
- app.js で jsx に express-react-viewsのエンジンを登録
- テンプレートをReactで書く
- スタイルをオブジェクトにしてjs内で定義・適用してみる
node.js + Express + mongoose で掲示板アプリを作ってみる
前回の「node.js+mongodbで掲示板アプリを作ってみる」ではnode.jsとmongodbのnative driverだけでapp.jsのみで実装しました。
- 独自ルーティング
- デザインもHTMLも混在
- dbのドキュメント構造がわかりにくい
- その他Webアプリケーションで共通の処理手順をそのつど書く
ため、ソースに手を加えるたびに煩雑になってきます。
目次
- 目次
- 0.MVCモデルの掲示板アプリを作る
- 1. 新しいnodeをインストール
- 1. Expressアプリケーションのプロジェクトを作る
- 2. mongooseをインストールして使えるようにする
- 3. mongoose接続とSchema定義を追加する
- 4. 投稿画面を追加
- 5.トップページを書き込み一覧表示にする
- 6. まとめ
0.MVCモデルの掲示板アプリを作る
今回はExpressとmongooseを使用してきれいな構造&少ない手順にします。
Express node.jsのスタンダードなwebアプリケーションフレームワーク
mongoose node.jsとmongodbのためのODMパッケージ
前提となる開発環境はMacOS High Sierra
1. 新しいnodeをインストール
新しくプロジェクトを開始するときはやったほうがいいでしょう
$ nodebrew install-binary stable
Fetching: https://nodejs.org/dist/v9.0.0/node-v9.0.0-darwin-x64.tar.gz
######################################################################## 100.0%
Installed successfully
$ nodebrew use 9.0.0
$ nodebrew migrate-package 8.8.1
$ npm upgrade
やったこと
- nodebrewをつかって新しいバージョンのnodeをインストール
- インストールされた最新を use
- 以前使用したバージョンのグローバル環境に入れていたパッケージを新しいバージョンに migrate
- パッケージをアップグレード
1. Expressアプリケーションのプロジェクトを作る
npm で express-generator が入っているとexpressコマンドで簡単にひな形をつくってくれます。
$ express -h Usage: express [options] [dir] Options: --version output the version number -e, --ejs add ejs engine support --pug add pug engine support --hbs add handlebars engine support -H, --hogan add hogan.js engine support -v, --view <engine> add view <engine> support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade) -c, --css <engine> add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css) --git add .gitignore -f, --force force on non-empty directory -h, --help output usage information
オプション含めてどんなviewエンジンを利用するか決めます。今回は標準のpugを使います。 てきとうなディレクトリで
$ express -v pug kakiapp create : kakiapp create : kakiapp/package.json create : kakiapp/app.js create : kakiapp/public create : kakiapp/routes create : kakiapp/routes/index.js create : kakiapp/routes/users.js create : kakiapp/views create : kakiapp/views/index.pug create : kakiapp/views/layout.pug create : kakiapp/views/error.pug create : kakiapp/bin create : kakiapp/bin/www create : kakiapp/public/javascripts create : kakiapp/public/images create : kakiapp/public/stylesheets create : kakiapp/public/stylesheets/style.css install dependencies: $ cd kakiapp && npm install run the app: $ DEBUG=kakiapp:* npm start $ cd kakiapp $ npm install ~ $ DEBUG=kakiapp:* npm start > kakiapp@0.0.0 start /Users/haranaga/node/kakiapp > node ./bin/www kakiapp:server Listening on port 3000 +0ms
やったこと
- expressプロジェクトのファイルを指定したディレクトリ内に全部そろえてくれる
- 作られたディレクトリに移動してローカルに依存パッケージをnpm install
- サーバーを起動
ブラウザでの動作
- localhost:3000/ にアクセスすると「Express Wellcome to Express」と表示される
できたファイル群
├── app.js アプリ本体 ├── bin/ 起動スクリプト置き場所 ├── node_modules/ 依存パッケージ ├── package-lock.json いつもの ├── package.json いつもの ├── public/ 静的ファイルを公開する場所 ├── routes/ コントローラーの置き場所 └── views/ Viewテンプレートの置き場所
これでnode.js+Expressの環境が整いました。
生成されたアプリケーションは / アクセスで上記の表示 /users へのアクセスで
respond with a resource
とtextを表示するだけのシンプルなものです。
デフォルトで設定される機能
- path: ローカルディレクトリのパス文字列のコントロール
- serve-favicon: faviconの設定を任せる
- morgan: ログ記録
- cookie-parser: cookie制御
- body-parser: POST Bodyを読み込んでくれる
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 up = require('./routes/up'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); // 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;
2. mongooseをインストールして使えるようにする
プロジェクトのディレクトリ内で
$ cd kakiapp $ npm install mongoose --save
3. mongoose接続とSchema定義を追加する
ソースを変更
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 up = require('./routes/up'); // 投稿画面 var app = express(); //// 1. mongoose connection var mongoose = require('mongoose'); // mongoose 利用 mongoose.Promise = global.Promise; mongoose.connect('mongodb://localhost/app1', {useMongoClient: true}); // 接続 //// end 1 //// 2. mongoose model 定義 // Schema 定義 var postSchema = mongoose.Schema({ kakikomi: String, name: String }); // model var Post = mongoose.model('Post',postSchema); // Post modelを router moduleで共有する app.set('Post',Post); //// end 2 // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); // 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); //// 3. 投稿画面 app.use('/up', up); //// end 3 // 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;
やったこと コメント //// 1 〜 3 を追加しました。
- mongoose でmongodb接続
- mongoose スキーマ定義と掲示板データを保存するコレクションのモデル定義
- route /up を投稿画面として追加
4. 投稿画面を追加
先にapp.js
でapp.use('/up',up);
を追加しました。
├── routes │ ├── index.js │ ├── up.js * 投稿画面コントローラー │ └── users.js └── views ├── error.pug ├── index.pug ├── layout.pug ├── up.pug * 投稿フォーム └── up_post.pug * 投稿後ページ
これらを追加します
routes/up.js
var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('up'); }); router.post('/', (req,res,next) => { var Post = module.parent.exports.get('Post'); var post = new Post(); post.kakikomi = req.body.kakikomi; post.name = req.body.name; var promise = post.save(); promise.then((doc) => { res.render('up_post',doc); }); }); module.exports = router;
やったこと
- /up/ の GETリクエストで view/up.pug を表示する
- /up/ への POSTリクエストで mongodbの postsコレクションへデータを追記して追加したデータをviewに渡して、view/up_post.pug を表示する
投稿フォーム
views/up.pug
extends layout block content h2 投稿フォーム form(action="/up" method="POST") p 内容: textarea(name="kakikomi" style="width:100%;height:60px;") p 名前: input(type="text" name="name") div(style="margin-top:30px;") button(type="submit") 投稿
views/up_post.pug
extends layout block content h2 投稿しました p #{kakikomi} p #{name}
ブラウザでの動作
- /up で フォームを表示
- 投稿ボタンで「投稿しました」画面を表示
5.トップページを書き込み一覧表示にする
トップページ / を書き込まれたデータの一覧表示画面にします.
コントローラー routes/index.js
と ビューの views/index.pug
を編集します
routes/index.js
var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { var Post = module.parent.exports.get('Post'); Post.find().sort({_id:-1}).exec((err, posts) => { res.render('index', { title: '書き込み一覧', posts: posts }); }); }); module.exports = router;
views/index.pug
extends layout block content h2= title style ul {list-style:none;} .name {font-size:0.8em;color:#555;} .box {border:1px solid #888;padding:20px;margin-bottom:40px;} ul each post in posts li .box div #{post.kakikomi} p(class="name") #{post.name}
やったこと
- / のGETリクエストでmongodbのコレクションpostsから全件データを_id降順で取得します。
- 取得したデータをviewのposts変数に渡して views/index.pugを表示します
ブラウザでの動作
- トップページにアクセスすると先に投稿した書き込みを一覧表示します。
6. まとめ
- express-generator パッケージのコマンドライン express で雛形を作成
- mongodbを利用したオブジェクトモデリング mongoose をnpmインストール
- app.js にmongoose利用の設定と接続
- app.js に mongoose Schemaの Post モデルを定義(kakikomiフィールドとnameフィールド)
- 投稿画面用に /up ルートを追加
- 投稿画面機能を routes/up.jsに フォーム表示とPOSTでデータ保存の処理
- トップページ / を 書き込み一覧ページとしデータを取得して表示する
検討事項
プロジェクトの各機能をファイルとディレクトリに分けて構造化するにあたって、セッション共通の依存性注入(Dependency Injection)をexpressでどのように実現するかいろいろと方法があるようです。
今回はエントリーポイントの app.js内で mongoose.model を app.set()し、呼び出される各モジュールで module.exports.get()して参照する方法で実装しました。
以上でnode.js + Epress + mongoose を使って簡単な掲示板サービスができました。
node.js と mongodbで掲示板アプリを作ってみる
前回つくった掲示板サービスはnode.jsのhttpサーバー起動中の配列に書き込みデータを保存したため、サーバー再起動でデータが消えてしまいます。
今回はmongodbにデータを保存してみます。
1. mongodbのインストールと起動
前提となる開発環境はMacOS High Sierra
インストールして起動
$ brew install mongodb ~ $ brew services start mongodb
mongo クライアント(mongo shell)で接続確認します。
$ mongo MongoDB shell version v3.4.9 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.4.9 Server has startup warnings: 2017-11-02T12:13:12.092+0900 I CONTROL [initandlisten] 2017-11-02T12:13:12.093+0900 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database. 2017-11-02T12:13:12.093+0900 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted. 2017-11-02T12:13:12.093+0900 I CONTROL [initandlisten] >
warningが出ていますが普通に使えます。アクセスコントロールを設定すれば出なくなります。
次にデータベースを作って、コレクションsampleに適当なデータを入れて確認してみる。
mongo
> use app1 > db.sample.insert({no:1,name:"ichi-bit",age:38}) WriteResult({ "nInserted" : 1 }) > db.sample.find() { "_id" : ObjectId("59fa915f7f36c7f99b4ac89e"), "no" : 1, "name" : "ichi-bit", "age" : 38 }
動作確認完了
2. node.jsでmongodbのdriverを使う
npmでmongodbをインストール
$ cd app1
$ npm install mongodb
これで
app.js
const mongodb = require('mongodb');
として利用できます。
3. 掲示板アプリをmongodb利用に変更
前回のソース
app.js
const http = require('http'); // httpサーバーmodule const hostname = 'localhost'; // ホスト名 const port = 3000; // port番号 // httpサーバーの定義 const server = http.createServer((req, res) => { // 全リクエストを処理 res.statusCode = 200; // http ステータスコード res.setHeader('Content-Type', 'text/html; charset=UTF-8'); // HTMLを返す 日本語返すので charsetもセット // ルーティング (url により振り分ける) switch (req.url) { // req.url にリクエストされたパスが入る case '/': topPage(req,res); // トップページ用関数 break; case '/up': upPage(req,res); // 投稿ページ用関数 break; default: notFoundPage(req,res); // その他ページ用関数 Not Foundページにします break; } }); // サーバー起動 server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); /////////////////////// 各ページの機能 ////////////////////////// // ヘッダ function header(req,res) { res.write('<html><head><title>掲示板</title><style>* {box-sizing:border-box;}</style></head><body style="position:relative;height:100%;">'); res.write('<header style="border:1px solid #888;padding:40px;">掲示板</header>'); res.write('<nav><ul><li><a href="/">トップ</a></li><li><a href="/up">投稿</a></li></nav>'); } // フッタ function footer(req,res) { res.write('<footer style="position:absolute;bottom:0;width:100%;border:1px solid #888;text-align:center;padding:20px;">フッター</footer>\n'); // 共通のフッター res.end('</body></html>'); // res.endでもコンテンツを返せる } // トップページ function topPage(req,res) { header(req,res); res.write('<h2>トップページ</h2>\n'); res.write('<ul>'); for(let row of posts) { res.write('<li>'+row+'</li>\n'); // htmlescapeは省略してます } res.write('</ul>'); footer(req,res); } // 投稿ページ var posts = []; // 掲示板データ function upPage(req,res) { header(req,res); // リクエストメソッドで処理を変える // GETの処理 if (req.method === 'GET') { res.write('<h2>投稿します</h2>\n'); res.write('<form action="/up" method="post"><div><textarea name="kakikomi" style="width:80%;height:100px"></textarea><div><div><input type="submit" value="投稿"></div></form>'); footer(req,res); return; } // POSTの処理 if (req.method === 'POST') { // POSTデータを受け取る(共通) // dataイベントでPOSTされたデータがちょっとずつ来るのでdataに蓄積する let body = []; req.on('data', (chunk) => { body.push(chunk); }).on('end', () => { body = Buffer.concat(body).toString(); //パースする const querystring = require('querystring'); parsedBody = querystring.parse(body); if (parsedBody.kakikomi) { posts.push(parsedBody.kakikomi); res.write('<h2>投稿しました</h2>\n'); res.write(decodeURIComponent(parsedBody.kakikomi)); // htmlescape省略 } footer(req,res); }); } } // その他のページ function notFoundPage(req,res) { res.statusCode = 404; // http ステータスコードを返します header(req,res); res.write('<h2>ページはありません</h2>'); footer(req,res); }
これをこう変えます
app.js
const http = require('http'); // httpサーバーmodule const hostname = 'localhost'; // ホスト名 const port = 3000; // port番号 // httpサーバーの定義 const server = http.createServer((req, res) => { // 全リクエストを処理 res.statusCode = 200; // http ステータスコード res.setHeader('Content-Type', 'text/html; charset=UTF-8'); // HTMLを返す 日本語返すので charsetもセット // ルーティング (url により振り分ける) switch (req.url) { // req.url にリクエストされたパスが入る case '/': topPage(req,res); // トップページ用関数 break; case '/up': upPage(req,res); // 投稿ページ用関数 break; default: notFoundPage(req,res); // その他ページ用関数 Not Foundページにします break; } }); // サーバー起動 server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); /////////////////////// 各ページの機能 ////////////////////////// // ヘッダ function header(req,res) { res.write('<html><head><title>掲示板</title><style>* {box-sizing:border-box;}</style></head><body style="position:relative;height:100%;">'); res.write('<header style="border:1px solid #888;padding:40px;">掲示板</header>'); res.write('<nav><ul><li><a href="/">トップ</a></li><li><a href="/up">投稿</a></li></nav>'); } // フッタ function footer(req,res) { res.write('<footer style="position:absolute;bottom:0;width:100%;border:1px solid #888;text-align:center;padding:20px;">フッター</footer>\n'); // 共通のフッター res.end('</body></html>'); // res.endでもコンテンツを返せる } // トップページ function topPage(req,res) { header(req,res); res.write('<h2>トップページ</h2>\n'); ////////////// ② mongodbからデータを取得する /////////////////////// getKakikomi((posts) => { res.write('<ul>'); for(let row of posts) { console.log(row); res.write('<li>'+row.kakikomi+'</li>\n'); // htmlescapeは省略してます } res.write('</ul>'); footer(req,res); }); } // 投稿ページ var posts = []; // 掲示板データ function upPage(req,res) { header(req,res); // リクエストメソッドで処理を変える // GETの処理 if (req.method === 'GET') { res.write('<h2>投稿します</h2>\n'); res.write('<form action="/up" method="post"><div><textarea name="kakikomi" style="width:80%;height:100px"></textarea><div><div><input type="submit" value="投稿"></div></form>'); footer(req,res); return; } // POSTの処理 if (req.method === 'POST') { // POSTデータを受け取る(共通) // dataイベントでPOSTされたデータがちょっとずつ来るのでdataに蓄積する let body = []; req.on('data', (chunk) => { body.push(chunk); }).on('end', () => { body = Buffer.concat(body).toString(); //パースする const querystring = require('querystring'); parsedBody = querystring.parse(body); if (parsedBody) { ////////////// ② mongodbに保存する /////////////////////// postKakikomi(parsedBody,() => { res.write('<h2>投稿しました</h2>\n'); res.write(decodeURIComponent(parsedBody.kakikomi)); // htmlescape省略 footer(req,res); }); } }); } } // その他のページ function notFoundPage(req,res) { res.statusCode = 404; // http ステータスコードを返します header(req,res); res.write('<h2>ページはありません</h2>'); footer(req,res); } /////////////////////// ③ mongodbデータの機能定義 //////////////////////////// // mongodb利用 const mongodb = require('mongodb'); var MongoClient = mongodb.MongoClient; var DB; MongoClient.connect('mongodb://localhost:27017/app1', function(err, db) { console.log("Connected successfully to server"); DB = db; }); // 書き込み全件取得 var getKakikomi = (callback) => { DB.collection('posts').find().toArray((err,docs) =>{ callback(docs) }); } // 書き込みを保存 var postKakikomi = (json, callback) => { DB.collection('posts').insertOne(json,(err, result) =>{ console.log('inserted'); callback(); }); }
①〜③の箇所が今回編集したところです。
やったこと
- ①mongodbから書き込みデータを全件取得を実行してデータを表示
getKakikomi((posts) => { res.write('<ul>'); for(let row of posts) { console.log(row); res.write('<li>'+row.kakikomi+'</li>\n'); // htmlescapeは省略してます } res.write('</ul>'); footer(req,res); });
- ②formのbodyを保存する処理
postKakikomi(parsedBody,() => { res.write('<h2>投稿しました</h2>\n'); res.write(decodeURIComponent(parsedBody.kakikomi)); // htmlescape省略 footer(req,res); });
- ③mongodbへの機能をまとめて定義
- mongodb データベースapp1へ接続しDBをコネクションプールとして使用
// mongodb利用 const mongodb = require('mongodb'); var MongoClient = mongodb.MongoClient; var DB; MongoClient.connect('mongodb://localhost:27017/app1', function(err, db) { console.log("Connected successfully to server"); DB = db; });
- function getKakikomi で posts コレクションをfindして返す機能定義
// 書き込み全件取得 var getKakikomi = (callback) => { DB.collection('posts').find().toArray((err,docs) =>{ callback(docs) }); }
- function postKakikomi で posts コレクションにデータを追加する機能定義
// 書き込みを保存 var postKakikomi = (json, callback) => { DB.collection('posts').insertOne(json,(err, result) =>{ console.log('inserted'); callback(); }); }
ブラウザで確認
- 前回と全く同じ動きをします
- サーバーを再起動してもデータは保存されています。
4. まとめ
これでmongodbに掲示板データを保存して表示することができるようになりました。
node.jsだけで掲示板アプリを作ってみる(後編)
「node.jsだけで掲示板アプリを作ってみる(前編)」のつづきです。
全体構造がわかるようにapp.js というファイル1つで実現します。
1. 前回までのまとめ
- node.js でhttpサーバー起動、レスポンスを返す
- URL / 及び /up で表示を変える(ルーティング)
- HTMLを表示する。(スタイルとHTMLタグ)
app.js
const http = require('http'); // httpサーバーmodule const hostname = 'localhost'; // ホスト名 const port = 3000; // port番号 // httpサーバーの定義 const server = http.createServer((req, res) => { // 全リクエストを処理 res.statusCode = 200; // http ステータスコード res.setHeader('Content-Type', 'text/html; charset=UTF-8'); // HTMLを返す 日本語返すので charsetもセット // 全ページ共通HTMLヘッド res.write('<html><head><title>掲示板</title><style>* {box-sizing:border-box;}</style></head><body style="position:relative;height:100%;">'); res.write('<header style="border:1px solid #888;padding:40px;">掲示板</header>'); res.write('<nav><ul><li><a href="/">トップ</a></li><li><a href="/up">投稿</a></li></nav>'); // ルーティング (url により振り分ける) switch (req.url) { // req.url にリクエストされたパスが入る case '/': res.write('<h1>トップページです</h1>\n'); break; case '/up': res.write('<h1>投稿ページです</h1>\n'); break; default: res.write('<h1>その他のページです</h1>\n'); break; } // 全ページ共通HTMLフッター res.write('<footer style="position:absolute;bottom:0;width:100%;border:1px solid #888;text-align:center;padding:20px;">フッター</footer>\n'); // 共通のフッター res.end('</body></html>'); // res.endでもコンテンツを返せる }); // サーバー起動 server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
前回までのこれを
app.js
const http = require('http'); // httpサーバーmodule const hostname = 'localhost'; // ホスト名 const port = 3000; // port番号 // httpサーバーの定義 const server = http.createServer((req, res) => { // 全リクエストを処理 res.statusCode = 200; // http ステータスコード res.setHeader('Content-Type', 'text/html; charset=UTF-8'); // HTMLを返す 日本語返すので charsetもセット // 全ページ共通HTMLヘッド(下で関数化 header(req,res)) // ルーティング (url により振り分ける) switch (req.url) { // req.url にリクエストされたパスが入る case '/': topPage(req,res); // トップページ用関数 break; case '/up': upPage(req,res); // 投稿ページ用関数 break; default: notFoundPage(req,res); // その他ページ用関数 Not Foundページにします break; } // 全ページ共通HTMLフッター(下で関数化 footer(req,res)) }); // サーバー起動 server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); /////////////////////// 各ページの機能 ////////////////////////// // ヘッダ function header(req,res) { res.write('<html><head><title>掲示板</title><style>* {box-sizing:border-box;}</style></head><body style="position:relative;height:100%;">'); res.write('<header style="border:1px solid #888;padding:40px;">掲示板</header>'); res.write('<nav><ul><li><a href="/">トップ</a></li><li><a href="/up">投稿</a></li></nav>'); } // フッタ function footer(req,res) { res.write('<footer style="position:absolute;bottom:0;width:100%;border:1px solid #888;text-align:center;padding:20px;">フッター</footer>\n'); // 共通のフッター res.end('</body></html>'); // res.endでもコンテンツを返せる } // トップページ function topPage(req,res) { header(req,res); res.write('<h2>トップページ</h2>\n'); footer(req,res); } // 投稿ページ function upPage(req,res) { header(req,res); res.write('<h2>投稿します</h2>\n'); footer(req,res); } // その他のページ function notFoundPage(req,res) { header(req,res); res.write('<h2>ページはありません</h2>'); footer(req,res); }
やったこと
- ページ別に処理を関数化
- ヘッダとフッタも関数化
- その他のページはNot Foundにする予定
2. 投稿ページ
投稿用関数にフォーム表示
app.js
// 投稿ページ function upPage(req,res) { res.write('<h2>投稿します</h2>\n'); }
これを
// 投稿ページ function upPage(req,res) { res.write('<h2>投稿します</h2>\n'); res.write('<form action="/up" method="post"><div><textarea style="width:80%;height:100px"></textarea><div><div><input type="submit" value="投稿"></div></form>'); }
やったこと
- formを表示するようにします。
- ここではおなじURL(/up)にsubmit するようにします
3. 投稿データを保存する
サーバー起動中のメモリ上に掲示板データを保存するようにします。 /up では httpリクエストメソッドがGETのときはformを表示し、POSTのときは投稿データを保存するようにします。
この方式ではサーバーを落とすとデータも消えます。立ち上げている間だけ蓄積するようにするコードです。
app.js
// 投稿ページ var posts = []; // 掲示板データ function upPage(req,res,data) { header(req,res); // リクエストメソッドで処理を変える // GETの処理 if (req.method === 'GET') { res.write('<h2>投稿します</h2>\n'); res.write('<form action="/up" method="post"><div><textarea name="kakikomi" style="width:80%;height:100px"></textarea><div><div><input type="submit" value="投稿"></div></form>'); footer(req,res); return; } // POSTの処理 if (req.method === 'POST') { // POSTデータを受け取る(共通) // dataイベントでPOSTされたデータがちょっとずつ来るのでbodyに蓄積する let body = []; req.on('data', (chunk) => { body.push(chunk); }).on('end', () => { body = Buffer.concat(body).toString(); //パースする const querystring = require('querystring'); parsedBody = querystring.parse(body); if (parsedBody.kakikomi) { posts.push(parsedBody.kakikomi); res.write('<h2>投稿しました</h2>\n'); res.write(decodeURIComponent(parsedBody.kakikomi)); // htmlescape 省略 } footer(req,res); }); } }
やったこと
- /up アクセス時のGET時とPOST時の処理分け
- POST時にreqのイベントでPOST値を受け取る
- POST値をパース
- 投稿データを内部配列に格納
ブラウザでの動作
- /up にアクセス
- フォームに書き込み送信
- 内部のposts配列に書き込みを追加
- 「投稿しました」と書き込んだ文字列を表示する
4. 投稿された書き込みを表示する
トップページで書き込まれた内容をリスト表示します。
app.js
// トップページ function topPage(req,res) { header(req,res); res.write('<h2>トップページ</h2>\n'); footer(req,res); }
これを
// トップページ function topPage(req,res) { header(req,res); res.write('<h2>トップページ</h2>\n'); res.write('<ul>'); for(let row of posts) { res.write('<li>'+row+'</li>\n'); // htmlescapeは省略してます } res.write('</ul>'); footer(req,res); }
やったこと
- グローバル配列のpostsをループして表示
ブラウザでの動作
- /up で書き込みを投稿後トップページで一覧を表示する
- /up で投稿を繰り返すと追加されていく。
- / トップでリストを確認できる
- サーバー再起動でデータは消えます。
5. Not Found ページの実装
/と/up 以外のURLでリクエストされた場合はNot foundとします
app.js
// その他のページ function notFoundPage(req,res) { res.write('<h2>ページはありません</h2>'); }
これを
// その他のページ function notFoundPage(req,res) { res.statusCode = 404; // http ステータスコードを返します header(req,res); res.write('<h2>ページはありません</h2>'); footer(req,res); }
やったこと
- レスポンスのステータスコードを404にする
5. まとめ
できたもの
- トップページ(/)で投稿データのリストを表示
- 投稿ページ(/up)でフォームを表示する
- 投稿アップ(/up)のメソッドPOSTでデータを取り込む
- CSSとHTMLもコミコミなので分けたくなる。→ フレームワーク、テンプレート利用などへの進化
- データもどこかに保存したい → データベース活用への進化
以上node.jsだけの1ソース掲示板アプリでした。
付録. 全ソース
app.js
const http = require('http'); // httpサーバーmodule const hostname = 'localhost'; // ホスト名 const port = 3000; // port番号 // httpサーバーの定義 const server = http.createServer((req, res) => { // 全リクエストを処理 res.statusCode = 200; // http ステータスコード res.setHeader('Content-Type', 'text/html; charset=UTF-8'); // HTMLを返す 日本語返すので charsetもセット // ルーティング (url により振り分ける) switch (req.url) { // req.url にリクエストされたパスが入る case '/': topPage(req,res); // トップページ用関数 break; case '/up': upPage(req,res); // 投稿ページ用関数 break; default: notFoundPage(req,res); // その他ページ用関数 Not Foundページにします break; } }); // サーバー起動 server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); /////////////////////// 各ページの機能 ////////////////////////// // ヘッダ function header(req,res) { res.write('<html><head><title>掲示板</title><style>* {box-sizing:border-box;}</style></head><body style="position:relative;height:100%;">'); res.write('<header style="border:1px solid #888;padding:40px;">掲示板</header>'); res.write('<nav><ul><li><a href="/">トップ</a></li><li><a href="/up">投稿</a></li></nav>'); } // フッタ function footer(req,res) { res.write('<footer style="position:absolute;bottom:0;width:100%;border:1px solid #888;text-align:center;padding:20px;">フッター</footer>\n'); // 共通のフッター res.end('</body></html>'); // res.endでもコンテンツを返せる } // トップページ function topPage(req,res) { header(req,res); res.write('<h2>トップページ</h2>\n'); res.write('<ul>'); for(let row of posts) { res.write('<li>'+row+'</li>\n'); // htmlescapeは省略してます } res.write('</ul>'); footer(req,res); } // 投稿ページ var posts = []; // 掲示板データ function upPage(req,res) { header(req,res); // リクエストメソッドで処理を変える // GETの処理 if (req.method === 'GET') { res.write('<h2>投稿します</h2>\n'); res.write('<form action="/up" method="post"><div><textarea name="kakikomi" style="width:80%;height:100px"></textarea><div><div><input type="submit" value="投稿"></div></form>'); footer(req,res); return; } // POSTの処理 if (req.method === 'POST') { // POSTデータを受け取る(共通) // dataイベントでPOSTされたデータがちょっとずつ来るのでbodyに蓄積する let body = []; req.on('data', (chunk) => { body.push(chunk); }).on('end', () => { body = Buffer.concat(body).toString(); //パースする const querystring = require('querystring'); parsedBody = querystring.parse(body); if (parsedBody.kakikomi) { posts.push(parsedBody.kakikomi); res.write('<h2>投稿しました</h2>\n'); res.write(decodeURIComponent(parsedBody.kakikomi)); // htmlescape省略 } footer(req,res); }); } } // その他のページ function notFoundPage(req,res) { res.statusCode = 404; // http ステータスコードを返します header(req,res); res.write('<h2>ページはありません</h2>'); footer(req,res); }
node.jsだけで掲示板アプリを作ってみる(前編)
node.jsだけで掲示板アプリを作ってみる
いろいろ便利なライブラリやパッケージを利用する前に素のnode.jsで一通りの機能を作ってみる
1. nodeのインストール
開発環境は http://ichi-bit.hateblo.jp/entry/2017/10/31/171806 で整えた。 コマンドラインで
$ node -v
v8.8.1
となればOK
2. ディレクトリ作成
app1という名前で作ります
$ mkdir app1 $ cd app1
3. Hello World
まずはおなじみの hello world から
app.js
const http = require('http'); // httpサーバーmodule const hostname = 'localhost'; // ホスト名 const port = 3000; // port番号 // httpサーバーの定義 const server = http.createServer((req, res) => { // 全リクエストを処理 res.statusCode = 200; // http ステータスコード res.setHeader('Content-Type', 'text/plain'); // テキストを返す res.write('Hello '); // res.write でコンテンツを送る res.end('World!\n'); // res.endでもコンテンツを返せる }); // サーバー起動 server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
保存してnodeコマンドで起動する。.js を省いてファイル名で指定できる
$ node app
Server running at http://localhost:3000/
ブラウザでアクセスると「Hello World」が表示される。 http://localhost:3000/hoge/ でもどんなアドレスでも同じ処理が行われる。 Ctrl-Cで終了する。
ソースを変更するたびに再起動しなくて良いようにnode-devを使って起動する。
$ node-dev app
Server running at http://localhost:3000/
これでソースの変更のタイミングで自動再起動してくれる。
4. URL毎に処理を変える
app.js
の
// httpサーバーの定義 const server = http.createServer((req, res) => { // 全リクエストを処理 res.statusCode = 200; // http ステータスコード res.setHeader('Content-Type', 'text/plain'); // テキストを返す res.write('Hello '); // write でコンテンツを送る res.end('World!\n'); // res.endでもコンテンツを返せる });
ここの箇所を
// httpサーバーの定義 const server = http.createServer((req, res) => { // 全リクエストを処理 res.statusCode = 200; // http ステータスコード res.setHeader('Content-Type', 'text/plain; charset=UTF-8'); // テキストを返す 日本語返すので charsetもセット // ルーティング (url により振り分ける) switch (req.url) { // req.url にリクエストされたパスが入る case '/': res.write('トップページです\n'); break; case '/up': res.write('投稿ページです\n'); break; default: res.write('その他のページです\n'); break; } res.end('以上!\n'); // res.endでもコンテンツを返せる });
こう変えます。
やったこと
- レスポンスヘッダに文字コードを指定。
- req.urlにより処理を場合分けする。
ブラウザで確認
- http://localhost:3000/ で 「トップページです以上!」
- http://localhost:3000/up で 「投稿ページです以上!」
- http://localhost:3000/任意のパス で 「その他のページです以上!」
と表示されるようになりました。
5.HTMLを表示させる
app.js
const http = require('http'); // httpサーバーmodule const hostname = 'localhost'; // ホスト名 const port = 3000; // port番号 // httpサーバーの定義 const server = http.createServer((req, res) => { // 全リクエストを処理 res.statusCode = 200; // http ステータスコード res.setHeader('Content-Type', 'text/html; charset=UTF-8'); // HTMLを返す 日本語返すので charsetもセット // 全ページ共通HTMLヘッド res.write('<html><head><title>掲示板</title><style>* {box-sizing:border-box;}</style></head><body style="position:relative;height:100%;">'); res.write('<header style="border:1px solid #888;padding:40px;">掲示板</header>'); res.write('<nav><ul><li><a href="/">トップ</a></li><li><a href="/up">投稿</a></li></nav>'); // ルーティング (url により振り分ける) switch (req.url) { // req.url にリクエストされたパスが入る case '/': res.write('<h1>トップページです</h1>\n'); break; case '/up': res.write('<h1>投稿ページです</h1>\n'); break; default: res.write('<h1>その他のページです</h1>\n'); break; } // 全ページ共通HTMLフッター res.write('<footer style="position:absolute;bottom:0;width:100%;border:1px solid #888;text-align:center;padding:20px;">フッター</footer>\n'); // 共通のフッター res.end('</body></html>'); // res.endでもコンテンツを返せる }); // サーバー起動 server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
HTTPヘッダのContent-Typeを変更
スタイルやHTMLを出力して、共通のタグとURLによってコンテンツを変更する。
ブラウザで確認するとヘッダーとフッターはどのページにも表示し、メニューのリンクでページ遷移を確認できます。
つづく
macOS High Sierra(v10.13)でJavaScript開発環境を整える
macOS High SierraでJavaScript開発環境を整える
0. 開発するもののイメージ
- Webサイト、Webアプリケーション
- 将来的にスマホアプリにも対応できるように考える
- なるべくデファクトスタンダードを利用する
- 開発言語はトータルで一つにまとめたい
- サーバーサイドは node.js + Express
- フロントエンドは react
- データベースは mongodb
- サーバーインフラはAWSもしくはGCP
1. Homebrew をインストール
macOS用のパッケージマネージャー。いろんなソフトをインストールして使うことができる。https://brew.sh/index_ja.html
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
2. nodebrew をインストール
node.jsを色んなバージョンで動かすことができる。https://github.com/hokaccha/nodebrew
$ brew install nodebrew
3. node.jsをインストール
nodebrew を使って最新安定バージョンを入れる
$ nodebrew install-binary stable
$ nodebrew ls
v8.6.0
v8.7.0
v8.8.1
current: v8.8.1
4. 開発で使うnode パッケージをグローバルに入れる
npm を使って開発で使うパッケージをグローバル環境に入れる
node-dev
node.jsアプリのサーバーをソース変更のたびに自動立ち上げするツール
https://github.com/fgnass/node-devexpress-generator
node.jsで動くwebフレームワーク express のプロジェクト構成を生成するツール
http://expressjs.com/ja/starter/generator.htmlserve
node.jsアプリを実行する
https://www.npmjs.com/package/servecreate-react-app
フロントエンドをreactで構築する環境を整えてくれるツール
https://github.com/facebookincubator/create-react-app
$ npm install -g node-dev $ npm install -g express-generator $ npm install -g serve $ npm install -g create-react-app
5. 動作確認
$ node -v v8.8.1 $ express --version 4.15.5 $ serve ┌───────────────────────────────────────────────────┐ │ │ │ Serving! │ │ │ │ - Local: http://localhost:5000 │ │ - On Your Network: http://192.168.11.15:5000 │ │ │ │ Copied local address to clipboard! │ │ │ └───────────────────────────────────────────────────┘ $ node-dev Usage: node-dev [options] script [arguments]