JavaScriptだけでWebサイト開発

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

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で追加しておく

動作確認

{ 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で立ち上げておきます。このサーバーと 今回作るアプリが連携します。

開発環境

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)

github.com

create-react-app で React フロントエンドの掲示板を作ってみる(リスト表示)

create-react-appはFacebookが開発したReact web アプリケーションの環境構築パッケージです。
React はそのコードを書いて動かす以前に前提となる開発環境を整備するのに苦労することから適用を断念しがちでした。このツールによって導入がしやすくなりました。

作るもの

  • これまで作ってきた掲示板と同じもの
  • フロントエンドが今回作るもの。前回作った掲示板APIと連携して書き込みデータをやりとりする。

サーバーAPIを立ち上げておく

前回作った「掲示板REST API」を localhost:3000で立ち上げておきます。このサーバーと 今回作るアプリが連携します。

開発環境

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;
}

ブラウザでの動作

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を作ってみる

開発環境

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機能が使えるようになります。

method URL
GET http://localhost/api/v1/Post/count
GET http://localhost/api/v1/Post
POST http://localhost/api/v1/Post
DELETE http://localhost/api/v1/Post
GET http://localhost/api/v1/Post/:id
PUT http://localhost/api/v1/Post/:id
POST http://localhost/api/v1/Post/:id
PATCH http://localhost/api/v1/Post/:id
DELETE http://localhost/api/v1/Post/:id

データ操作の機能や検索条件、その他オプションはこちらで確認できます。
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 パッケージインストール

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をオブジェクトで定義し、各コンポーネント内に埋め込み
  • スタイルのパラメータのハイフンを含むものはローワーキャメルケースにする必要がある。

ブラウザでの動作

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モデルの掲示板アプリを作る

今回は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 を追加しました。

  1. mongoose でmongodb接続
  2. mongoose スキーマ定義と掲示板データを保存するコレクションのモデル定義
  3. route /up を投稿画面として追加

4. 投稿画面を追加

先にapp.jsapp.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. まとめ

  1. express-generator パッケージのコマンドライン express で雛形を作成
  2. mongodbを利用したオブジェクトモデリング mongoose をnpmインストール
  3. app.js にmongoose利用の設定と接続
  4. app.js に mongoose Schemaの Post モデルを定義(kakikomiフィールドとnameフィールド)
  5. 投稿画面用に /up ルートを追加
  6. 投稿画面機能を routes/up.jsに フォーム表示とPOSTでデータ保存の処理
  7. トップページ / を 書き込み一覧ページとしデータを取得して表示する

検討事項

プロジェクトの各機能をファイルとディレクトリに分けて構造化するにあたって、セッション共通の依存性注入(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により処理を場合分けする。

ブラウザで確認

と表示されるようになりました。

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 を使って開発で使うパッケージをグローバル環境に入れる

$ 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]
©ichi-bit