126
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Node.js, Express, sequelize, React で始めるモダン WEB アプリケーション入門(React編)

Last updated at Posted at 2018-04-01

この記事について

Node.js, Express, sequelize, React で始めるモダン WEB アプリケーション入門(Express/sequelize編) の続編です。

前回までは Express スタートアップを行い、Sequelize を使って DB 操作をするところまで行った。

今回は View をインタラクティブにするため React フレームワークを使う。

尚、ここで作成したアプリケーションは GitHub - ryu-sato/express-sample_app に公開してある。

また、View の見た目を整えるために、HTML/CSS/JavaScript のフレームワークでメジャーな Bootstrap を React で使う方法を番外編※として投稿した。

Node.js, Express, sequelize, React で始めるモダン WEB アプリケーション入門(bootstrap番外編)

React概要

React はインタラクティブな UI を作るための JavaScript ライブラリである。
DOM にステータスを持たせ ReactDOM として管理することでコンポーネント化している。

View を JavaScript を使ったインタラクティブな UI とするために利用できるよう準備していく。

React と Express の結合

まず、React を使うためには react, react-dom パッケージが必要となる。
その他、トランスパイラ(Babel)、モジュールバンドラ(Webpack)、Lintツール(ESLint)等、JavaScript アプリケーション開発を効率化するためのツールを一気に導入できる react-scripts パッケージを導入する。(create-react-app を参考にした)

react,react-dom,react-scriptsをインストールする
> npm install --save react react-dom react-scripts

次に React コンポーネントを描画する src/index.js と public/index.html を作成する。

src/index.js
var React = require('react');
var ReactDOM = require('react-dom');

class EmployeeList extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return(
      <div>
        Hello React
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <EmployeeList />,
  document.getElementById('root')
);
public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

これで react-scripts start を実行してサーバを起動させた後、パス / へアクセスすると React コンポーネントが表示されるようになった。

しかし、react-scripts start で実行されるアプリは、前編で作成していた Express + sequelize を利用した app.js とは別のアプリケーションとして実行される。ここで双方を連携させるために、役割の便宜上 Express + sequelize をバックエンドと呼び、React (react-scripts) をフロントエンドと呼ぶこととして、バックエンドは ORM を用いて DB の CRUD 操作を API 経由で行い、フロントエンドはバックエンドと API 連携するよう修正していくこととする。

まずは、バックエンド側は view を廃止して API 経由で DB を操作するアプリケーションへ修正する。

Node.js のエントリポイントの準備

※ ここでは、Node.js で実行する対象となる JavaScript ファイルのことを Node.js のエントリポイントと呼ぶこととする。

Node.js のエントリポイントの準備として下記の修正を行う。

  • sequelize-cli によって自動生成された bin/www を Node.js のエントリポイントとして起動していたが内容が不明な点もあるのでファイルを削除する
  • app.js をリネームして server.js として更に TCP の Listen 処理を追記する(待ち受けポートは TCP/3000 番)

バックエンドの準備

view フォルダ配下全てと view を設定するコードを削除し、更に下記の変更を行う。

API の設計

  • /_api/employee/ への GET メソッド要求を受けたら、
    • 全 employee モデル一覧を JSON 形式で返答する。
  • /_api/employee/:id への GET メソッド要求を受けたら、
    • 該当 ID の employee モデルを JSON 形式で返答する。
  • /_api/employee/ への POST メソッド要求を受けたら、
    • パラメータに即した employee を新規作成する。
  • /_api/employee/:id への POST メソッド要求を受けたら、
    • 該当 ID の employee をパラメータに即した値に更新する。
  • /_api/employee/:id への DELETE メソッド要求を受けたら、
    • 該当 ID の employee を削除する。

API実装ポイント

  • /controllers/api_employee_controller.js を作成して API 処理を実装する
    • sequelize を使うために models/index.js を読み込む - 例: var models = require('../models');
    • モデル操作するために、sequelize インスタンス用関数を呼び出す
      • 例: models.Employee.all()
      • 書式: ${requireしたモジュールを代入した変数名}.${モデル名}.${sequelizeのORMインスタンス用関数}
    • JSON 形式でデータを応答するために res.json 関数を使う
      • 例: res.json(${JSONDATAを記述}) を記述する
  • /controllers/index.js 内に上記で作成した api_employee_controller.js をモジュール変数に代入する
    • /routes/api.js を作成して /_api/employee に対する CRUD 操作をコントローラへルーティングする

上記を実装すると、以下のようになる。

Expressにより/_api/配下へのアクセスを受けたら/routes/apiで記述したルーティングを行うよう設定する(/server.js)
var express = require('express');
var server = express();
var api = require('./routes/api');
server.use('/_api/', api);
Expressにより/_api/employee配下へのアクセスをapi_employee_controllerへ渡すルーティング設定(/routes/api.js)
var express = require('express');
var router = express.Router();
var controllers = require('../controllers');

/* API of controling employees */
router.get('/employees/', controllers.api_employee_controller.index);
router.get('/employees/:id(\\d+)', controllers.api_employee_controller.show);
router.post('/employees/', controllers.api_employee_controller.create);
router.put('/employees/:id(\\d+)', controllers.api_employee_controller.update);
router.delete('/employees/:id(\\d+)', controllers.api_employee_controller.destroy);

module.exports = router;
CRUDを提供するAPI処理(/controllers/employee_controller.js)
var models = require('../models');

/**
 * show all employee list
 */
exports.index = function(req, res, next) {
  models.Employee.all().then(employees => {
    res.json({ employees: employees });
  });
};

/**
 * show employee details
 */
exports.show = function(req, res, next) {
  models.Employee.findById(req.params.id).then(employee => {
    res.json({ employee: employee });
  });
};

/**
 * create employee
 */
exports.create = function(req, res, next) {
  var properties = ["name", "department", "gender", "birth", "joined_date", "payment", "note"];
  var new_values = {};
  properties.forEach(function(prop) {
    new_values[prop] = req.body[prop];
  });
  models.Employee.create(
    new_values
  ).then(new_employee => {
    res.redirect(302, '/employees');
  });
};

/**
 * update employee
 */
exports.update = function(req, res, next) {
  console.log('exports.update is executed');
  models.Employee.findById(req.params.id).then(employee => {
    var properties = ["name", "department", "gender", "birth", "joined_date", "payment", "note"];
    var update_values = {};
    properties.forEach(function(prop){
      update_values[prop] = req.body[prop];
    });
    employee.update(update_values);
    res.redirect(302, "/employees/" + employee.id);
  });
};

/**
 * destroy employee
 */
exports.destroy = function(req, res, next) {
  models.Employee.destroy
  ({
    where: { id: req.params.id }
  }).then(employee => {
    res.redirect(302, "/employees");
  });
};
全controllerを読み込むモジュール(/controllers/index.js)
'use strict';
var api_employee_controller = require('./api_employee_controller');
exports.api_employee_controller = api_employee_controller;

フロントエンドの準備

react-scripts は次のディレクトリ構造を認識する。

react-scriptsで実行されるアプリのディレクトリ構造
/
  public/
    index.html ... HTTPのインデックスファイル
    favicon.ico ... お気に入り表示用アイコン
      : (HTTPパスに対応するディレクトリ構造で応答するファイルを作成していく)
  src/
    index.js ... JavaScript のエントリーポイント
    index.css ... CSSファイル
      : (ReactコンポーネントやCSSファイルを作成していく)

まずは簡単に、employee一覧を表示するReactコンポーネントを作成することとする。

Employee一覧表示用HTMLファイル(/public/index.html)※id=rootのdivがReactによってレンダリングされる
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>
Employee一覧をバックエンドから取得して表示する(/src/index.js)
var React = require('react');
var ReactDOM = require('react-dom');

class EmployeeList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      employees: [],
    };

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

  loadAjax() {
    return fetch(this.props.url)
      .then((response) => response.json())
      .then((responseJson) =>
        this.setState({
          employees: responseJson.employees,
        })
      )
      .catch((error) => {
        console.error(error);
      });
  }

  componentWillMount() {
    this.loadAjax();
  }

  render() {
    const employee_list = this.state.employees.map((e) => <li>{e.name}</li>);
    return(
      <ul>
        {employee_list}
      </ul>
    );
  }
}

// ========================================

ReactDOM.render(
  <EmployeeList url="http://localhost:3001/_api/employee" />,
  document.getElementById('root')
);

ここでのポイントは次のとおり。

  • react-scripts build でビルドすることで /src/ 配下の JavaScript や CSS ファイルが /build/ 配下に配置され、/public/ 配下の HTML から呼び出すコードが追加される
    • /src/index.htmlに追加されるコードの例:
      <script type="text/javascript" src="/static/js/main.1a093980.js"></script>
    • /static/js/main.1a093980.js が /src/ 配下の JavaScript ファイルがマージされたものである
  • ブラウザが TCP port 3000 の / にアクセスする /public/ 配下の対応するファイルが開かれるようになる
  • /src/ 配下の JavaScript は /public/index.html から呼び出すことが出来る
  • /public/hoge/index.html のようにディレクトリ hoge を作成して、その配下に HTML ファイルを作成しても、/src/ 配下の JavaScript や CSS を呼び出すコードは追加されない

これで React が View として利用できるようになった。

後は、例として sequelize で作成した Employee モデルを CRUD 操作するためのView を React コンポーネントで用意する。

Emploeeモデルの詳細情報を表示するコンポーネント(/src/EmployeeDetail.js)
var React = require('react');

class EmployeeDetail extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      id: this.props.match.params.id,
      employee: {},
    };

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

  loadEmployee() {
    return fetch(`/_api/employees/${this.state.id}`)
      .then((response) => response.json())
      .then((responseJson) =>
        this.setState({
          employee: responseJson.employee,
        })
      )
      .catch((error) => {
        console.error(error);
      });
  }

  componentWillMount() {
    this.loadEmployee();
  }

  render() {
    const attributes_array = ["name", "department", "gender", "birth", "joined_date", "payment", "note"].map((attr) =>
      { return {
        name: attr,
        val: this.state.employee[attr] ? this.state.employee[attr].toString() : '...loading'
      } }
    );
    return (
      <dl>
        {attributes_array.reduce((accumulator, attr, idx) => {
          return accumulator.concat([
            <dt key={`attrname-${idx}`}>{attr.name}</dt>,
            <dd key={`attrval-${idx}`}>{attr.val}</dd>
          ]);
        },[])}
      </dl>
    );
  }
}

module.exports = EmployeeDetail;
Emploeeモデルの編集フォームを表示するコンポーネント(/src/EmployeeEdit.js)
var React = require('react');

class EmployeeEdit extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      id: this.props.match.params.id,
      employee: {},
    };

    this.loadEmployee = this.loadEmployee.bind(this);
    this.onChangeField = this.onChangeField.bind(this);
  }

  loadEmployee() {
    return fetch(`/_api/employees/${this.state.id}`)
      .then((response) => response.json())
      .then((responseJson) =>
        this.setState({
          employee: responseJson.employee,
        })
      )
      .catch((error) => {
        console.error(error);
      });
  }

  onChangeField(e) {
    var employee = this.state.employee;
    employee[e.target.name] = e.target.value;
    this.setState({
      employee: employee
    });
  }

  componentWillMount() {
    this.loadEmployee();
  }

  render() {
    const employee = this.state.employee || {};
    const id = (employee.id ? <div>ID: {employee.id}</div> : '');
    return (
      <form action={'/_api/employees/' + employee.id + '?_method=PUT'} method='post'>
        { /* cf. https://qiita.com/ozhaan/items/c1e394226c1d5acb7f0e */ }
        <input name="_method" type="hidden" value="PUT" readOnly />
        {id}
        <div>Name: <input type='text' name='name' value={employee.name} placeholder="Input Employee's Name" onChange={this.onChangeField} /></div>
        <div>Department: <input type='text' name='department' value={employee.department} placeholder="" onChange={this.onChangeField} /></div>
        <div>
          Gender:
            <input type='radio' name='gender' defaultValue='male' checked={employee.gender==="male"} onChange={this.onChangeField} /> male
            <input type='radio' name='gender' defaultValue='female' checked={employee.gender==="female"}  onChange={this.onChangeField} /> female
            <input type='radio' name='gender' defaultValue='other' checked={employee.gender!=="male"&&employee.gender!=="female"} onChange={this.onChangeField} /> other
        </div>
        <div>Birthday: <input type='text' name='birth' value={employee.birth} placeholder="Input Employee's Birthday" onChange={this.onChangeField} /></div>
        <div>Joined Date: <input type='text' name='joined_date' value={employee.joined_date} placeholder="Input Employee's Joined Date" onChange={this.onChangeField} /></div>
        <div>Payment: <input type='text' name='payment' value={employee.payment} placeholder="Input Employee's Payment" onChange={this.onChangeField} /></div>
        <div>Note: <input type='text' name='note' value={employee.note} placeholder="Input Note" onChange={this.onChangeField} /></div>
        <div><input type='submit' value='Submit' readOnly /></div>
      </form>
    );
  }
}

module.exports = EmployeeEdit;
Emploeeモデルを新規作成するコンポーネント(/src/EmployeeNew.js)
var React = require('react');

class EmployeeNew extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      employee: {},
    };

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

  componentWillMount() {
  }

  onChangeField(e) {
    var employee = this.state.employee;
    employee[e.target.name] = e.target.value;
    this.setState({
      employee: employee
    });
  }

  render() {
    const employee = this.state.employee || {};
    const id = (employee.id ? <div>ID: {employee.id}</div> : '');
    return (
      <form action={'/_api/employees'} method='post'>
        { /* cf. https://qiita.com/ozhaan/items/c1e394226c1d5acb7f0e */ }
        <input name="_method" type="hidden" value="put" readOnly />
        {id}
        <div>Name: <input type='text' name='name' defaultValue={employee.name} placeholder="Input Employee's Name" onChange={this.onChangeField} /></div>
        <div>Department: <input type='text' name='department' defaultValue={employee.department} placeholder="" onChange={this.onChangeField} /></div>
        <div>
          Gender:
            <input type='radio' name='gender' defaultValue='male' checked={employee.gender==="male"} onChange={this.onChangeField} /> male
            <input type='radio' name='gender' defaultValue='female' checked={employee.gender==="female"} onChange={this.onChangeField} /> female
            <input type='radio' name='gender' defaultValue='other' checked={employee.gender!=="male"&&employee.gender!=="female"} onChange={this.onChangeField} /> other
        </div>
        <div>Birthday: <input type='text' name='birth' defaultValue={employee.birth} placeholder="Input Employee's Birthday" onChange={this.onChangeField} /></div>
        <div>Joined Date: <input type='text' name='joined_date' defaultValue={employee.joined_date} placeholder="Input Employee's Joined Date" onChange={this.onChangeField} /></div>
        <div>Payment: <input type='text' name='payment' defaultValue={employee.payment} placeholder="Input Employee's Payment" onChange={this.onChangeField} /></div>
        <div>Note: <input type='text' name='note' defaultValue={employee.note} placeholder="Input Note" onChange={this.onChangeField} /></div>
        <div><input type='submit' value='Submit' readOnly /></div>
      </form>
    );
  }
}

module.exports = EmployeeNew;
Emploeeモデルの一覧を表示するコンポーネント(/src/EmployeeList.js)
var React = require('react');
var rrd = require('react-router-dom');
var Link = rrd.Link;

// Employee一覧レンダリング用コンポーネント
class EmployeeList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      employees: [],
    };

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

  loadEmployeeList() {
    return fetch("/_api/employees")
      .then((response) => response.json())
      .then((responseJson) =>
        this.setState({
          employees: responseJson.employees,
        })
      )
      .catch((error) => {
        console.error(error);
      });
  }

  componentWillMount() {
    this.loadEmployeeList();
  }

  render() {
    const employee_list = this.state.employees.map((employee) =>
      <tr key={`EmployeeList-${employee.id}`}>
        <td>
          <Link to={`/employees/${employee.id}`}>{employee.id}</Link>
        </td>
        <td>{employee.name}</td>
        <td>{employee.department}</td>
        <td>{employee.gender}</td>
        <td>
          <Link to={`/employees/${employee.id}/edit`}><button>Edit</button></Link>
          <form action={'/_api/employees/' + employee.id + '?_method=DELETE'} method='post'>
            { /* cf. https://qiita.com/ozhaan/items/c1e394226c1d5acb7f0e */ }
            <input name="_method" type="hidden" value="DELETE" readOnly />
            <input name="id" type="hidden" value={employee.id} readOnly />
            <input type="submit" value="Delete" />
          </form>
        </td>
      </tr>
    );

    return(
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Department</th>
            <th>Gender</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          {employee_list}
        </tbody>
      </table>
    );
  }
}

module.exports = EmployeeList;

index.html を修正して下記のとおり作成したページを表示できるようにする。

ページ設計

  • 常に Home へ戻るためのリンクを表示する
  • 常に Employee 一覧ページを表示するためのリンクを表示する
  • リンクをクリックした際にクライアントルーティングする
  • クライアントルーティングされたページをブラウザリロードできるようにする

ルーティング設定

フロントエンド側でクライアントサイドルーティングを行い、Employee 一覧ページへの遷移や Home ページへの遷移を行う。
但し、クライアントルーティングのみを行った場合、/ 以外の URL に移動(例: /employees 等)してからブラウザリロードを行うと /public/index.html が読み取れず 404 not found となる。

そこで、バックエンド側で /_api 以外のアクセスに関しては全て /public/index.html を参照するようにルーティングを設定する。こうすることで、ブラウザのリロードを行っても index.html が読み込まれ、その後クライアントルーティングによって適切なページ表示が行えるようになる。

クライアントサイドルーティングを行うパッケージとして、React の公式ページによると最もポピュラーなのは React Router であると記述があったため React Router を利用する。(参考: GitHub > facebook/create-react-app#Serving Apps with Client-Side Routing

react-router-domをパッケージに追加する
> npm install --save react-router-dom

次に /src/index.js でクライアントサイドルーティングを設定し、バックエンド側で Express によるルーティングを設定する。

フロントエンドのメイン(/src/index.js)
var React = require('react');
var ReactDOM = require('react-dom');
var rrd = require('react-router-dom');
var BrowserRouter = rrd.BrowserRouter;
var Route = rrd.Route;
var Link = rrd.Link;
var EmployeeList = require('./EmployeeList');
var EmployeeDetail = require('./EmployeeDetail');
var EmployeeNew = require('./EmployeeNew');
var EmployeeEdit = require('./EmployeeEdit');

class ExpressSampleApp extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/employees">employee</Link></li>
          </ul>
          <Route exact path="/" component={Home} />
          <Route exact path="/employees" component={EmployeeList} />
          <Route exact path='/employees/:id([0-9]+)' component={EmployeeDetail} />
          <Route exact path="/employees/new" component={EmployeeNew} />
          <Route exact path="/employees/:id([0-9]+)/edit" component={EmployeeEdit} />
          <Link to="/employees/new">New</Link>
        </div>
      </BrowserRouter>
    );
  }
}

class Home extends React.Component {
  render() {
    return (
      <div>
        <h2>Home</h2>
      </div>
    );
  }
}


// DOMのレンダリング処理
//   see. https://reactjs.org/docs/react-dom.html#render
ReactDOM.render(
  <ExpressSampleApp />,            // Appをレンダリングする
  document.getElementById('root')  // id=root要素に対してレンダリングする
);
バックエンド側のルーティング設定(とHTTPメソッドをOverrideする処理)(/server.js)
var express = require('express');
var path = require('path');
  : <snip>
// override HTTP method to using CRUD
server.use(methodOverride('_method'));

// correspond Express and React
server.use(express.static(path.join(__dirname, 'build')));
server.get('/*', function(req, res) {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
  : <snip>

以上でフロントエンドが用意できた。

アプリケーションの起動

アプリケーションを起動するためには、react-scripts build でビルドした後に、node server.js を実行すると起動する。

npm コマンドで実行したい場合は、適宜 package.json を修正する。

package.json
{
    : <snip>
  "scripts": {
    "build": "react-scripts build",
    "start": "node server.js"
  },
    : <snip>
}

尚、コードを修正する度にフロントエンド側に対する修正であればリビルドが必要となり、バックエンド側に対する修正であればアプリを再起動する必要がある。これらを自動的に行うソリューションは今後の課題として見送る。

Express と React 連携の終わりに

以上で Express と React が連携できた。

尚、express-react-view パッケージにより React を Express のテンプレートエンジンとして利用すると React をサーバサイドレンダリングできるが、クライアントサイドでの React レンダリングやイベント処理が出来ないので注意。詳細は本ページ内 express-react-view について を参照のこと。名前からして Express と React を連携できそうだったので利用して躓いた。

フロントエンドにページを追加する流れ

フロントエンドにページを追加するためには次の流れで修正を行うことになる。

  • /src 配下にページ用の React コンポーネントを追加する
  • /src/index.js の ExpressSampleApp の render 内に <Route> を使って作成した React コンポーネントを表示する

MVCモデル構造

今回は MVC モデルに基づき下記構造で Model, View, Controller を配置した。
特に経験に裏づく配置ではないため、ただのプラクティスの1つである。

routes ディレクトリを作成するのは express-generator で作成されたディレクトリ構造を踏襲し、models ディレクトリは sequelize が作成したディレクトリ構造を踏襲している。(views ディレクトリは削除した)
また、src, public ディレクトリは react-scripts がビルドするディレクトリ構造を踏襲している。
そこに、Rails を参考にして controllers ディレクトリを新規に作成した構造である。

MVCモデル構造
/
  controllers/
    index.js
    employee_controllers.js
      : (モデルごとにコントローラを作成していく)
  models/
    index.js
    employee.js
      : (モデルを作成していく)
  routes/
    index.js
    employee.js
      : (コントローラごとにルーティング設定を作成していく)
  public/
    index.html
  src/
    employee/
      employee_detail.js
      employee_edit.js
      employee_new.js
      employee_list.js
     : (ビューごとに作成していく)
    error.js
    index.js

下記記事にもあるように、基本的にはフレームワーク標準のディレクトリ構造が望ましいであろう。

尚、controllers ディレクトリ(及びクラス)を routes と別個に作成した結果感じたメリットとしては、ルーティングした結果、同一の処理を行いたい(controller の同一メソッドを呼び出したい)場合に冗長にならずに記述出来たことが挙げられるが、大きなメリットであるとは感じられなかった。
(例: HTTP GET / へのアクセスと HTTP GET /employee へのアクセスを同一にしたい場合に express (var serverに格納) を使って、server.get('/', employee_controllers.index), server.get('/employee', employee_controllers.index) と記述出来た。)

新規モデルを作成する際の流れ

新規モデルを作成する場合は models ディレクトリ配下にファイルを作成する。

新規モデル作成(ローカルインストールしている場合はnode_modules/.bin配下のsequelizeを利用する)
$ sequelize model:create --name MODEL_NAME --attributes MODEL_ATTRIBUTES

詳細は sequelize model:create --help を確認のこと。

その後、migration ファイルを修正してから DB マイグレーションを行う。

DBマイグレーションの実行
$ sequelize db:migrate --env development

作成したモデルをDBへマイグレーションしたら、新規作成したモデル用に controller を新規作成し、モデルへの操作を行うルーティングを設定する。

新規作成したモデル(M)に対応するVCの設定
controllers/
  NEWMODEL.js
routes/
  NEWMODEL.js
views/
  index.js
  show.js
    : <snip>

コードが変更されたときに自動読み込みを行う

コードが変更された際に自動で読み込みを行う方法を記述します。

Node.js(Express, Sequelize) が担う DB データの応答(API) と React が担うビューでそれぞれ異なるツールを用意する必要があります。

Node.js を更新するツールとして個人的に知っているのは nodemon, node-dev です。

どちらも設定なしで利用することができ、nodemon の方が人気('18/09/28 現在 nodemon は star 16,013 であるのに対して node-dev は star 1,226) ですが、node-dev はファイルシステムを監視せずに Node.js の require をフックして実際に読み込まれた(requireされた)コードを監視するため、不要なファイルを更新しても再起動が起こらず優秀だと思いますので、node-dev を使うことにします。

React が担うビューを変更時に自動ビルドする場合は、react-scripts で使用している webpack-dev-server を使います。

node-dev を導入する

node-dev をパッケージとしてインストールして、node コマンドの代わりに node-dev コマンドを使うだけです。

node-devをインストールする
$ npm install --save-dev node-dev
node-devを使ってアプリを起動する
$ ./node_modules/.bin/node-dev server.js

webpack-dev-server を導入する

react-scripts が webpack-dev-server を使っているため、react-scripts を導入していれば別途インストールする必要はありません。

react-scripts start を実行することで webpack-dev-server が立ち上がりますが、Node.js(Express, Sequelize) が担う DB データの応答(API) と連携させる必要があります。

今回のように、フロントエンド部とバックエンド部を同じポート番号を使ってルートによって分ける環境の場合、package.json に proxy 設定を追加することで webpack-dev-server が良しなにプロクシする(HTTP Header の Accept が text/html ではないアクセスを別サーバへプロクシする)ようなので、この設定を使うことにします。(参考 URL)

開発環境と本番環境で次のような設定となる想定です。

  • 開発環境
    • Node.js が TCP/3010 ポートで LISTEN する
    • webpack-dev-server が TCP/3000 ポートで LISTEN する
      • 良しなに Node.js へプロクシする
  • 本番環境
    • Node.js が TCP/3000 ポートで LISTEN する
package.json
{
  : <snip>
  "//": "react-scriptsにおいてHTTP HEADERのAcceptがtext/htmlであるリクエストをプロクシする先のサーバ",
  "proxy": "http://localhost:3010"
}

Node.js を起動する際に、ポート番号を 3010 で指定する必要がありますが、以上で react-scripts start を実行すれば React 側の開発環境が起動します。

$ ./node_modules/.bin/react-scripts start

package.json にスクリプトを記述する

環境変数でポート番号の設定をし、さらに node-dev, react-scripts を実行する度に長いコマンドを入力するのは手間なので package.json の scripts に記述して npm run で実行できるようにします。

ここで、NODE_ENV 環境変数を設定する方法を Windows/Linux どちらで実行するかで変更する必要が無いように env-cmd を使うことにします。

また、本番環境で SourceMap がデフォルトで作成されるため、これを解除することにしま
す。

package.json
{
  : <snip>
  "scripts": {
    "build:dev": "env-cmd config/env.dev.js react-scripts start",
    "build:prod": "env-cmd config/env.prod.js react-scripts build",
    "build": "npm run build:prod",
    "server:dev": "env-cmd config/env.dev.js node-dev server.js",
    "server:prod": "env-cmd config/env.prod.js node server.js",
    "start": "npm run server:prod",
  },
  : <snip>
}
config/env.dev.js
module.exports = {
  NODE_ENV: 'development',
  PORT: 3010
};
config/env.prod.js
module.exports = {
  NODE_ENV: 'production',
  GENERATE_SOURCEMAP: false
};

これで、下記コマンドを実行することでコードが変更された際に自動でサーバ再起動又はビルドが実行されるようになります。

$ npm run build:dev
$ npm run server:dev

試しにコードを変更して自動読み込みが行われるか確認する

動作することを確認するために、試しに src/employees/employees_list.js の Table タグを修正してヘッダを変更したとします。

src/employees/employees_list.js
  : <snip>
      // th タグの値を ID から Identifier へ変更した
      <Table responsive>
        <thead>
          <tr>
            <th>Identifier</th>
            <th>Name</th>
            <th>Department</th>
            <th>Gender</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          {employee_list}
        </tbody>
      </Table>
  : <snip>

ブラウザのリロードが自動で行われ、修正した内容が反映されていることが確認できれば OK です。

一方で、サーバ側の修正として新しいルーティングパスを作成した場合を考えてみます。

例えば、 /_api/test へアクセスした場合も /_api/employees と同じく employee 一覧を応答するよう変更したとします。

routes/api.js
var express = require('express');
var router = express.Router();
var controllers = require('../controllers');

/* API of controling employees */
router.get('/employees/', controllers.api_employee_controller.index);
router.get('/employees/:id(\\d+)', controllers.api_employee_controller.show);
router.post('/employees/', controllers.api_employee_controller.create);
router.put('/employees/:id(\\d+)', controllers.api_employee_controller.update);
router.delete('/employees/:id(\\d+)', controllers.api_employee_controller.destroy);

router.get('/test', controllers.api_employee_controller.index); // 追加

module.exports = router;

画面右下からトースターが表示され、サーバが再起動されたことが通知されます。

サーバの再起動が完了したらブラウザで http://localhost:3010/_api/test へアクセスし employees 一覧が応答されれば OK です。

(http://localhost:3000/_api/test へブラウザで通常のアクセスをしても API の応答結果が表示されません。ブラウザの開発コンソール等で HTTP Header を編集して Accept を text/html ではない形式に変更する必要があります)

終わりに

以上で Node.js, Express, sequelize, React を連携できた。
今後はモデルやビューの追加していき、アプリケーションを作成していけばよい。

おまけ

React で View を操作する

React Tutorial の内容を参考にして、React における View の操作概要を記述する。

React 概要

React は UI を宣言型、効率的、フレキシブルに作成できる特徴を持つ JavaScript ライブラリである。(参考: https://reactjs.org/tutorial/tutorial.html#what-is-react

  • React へレンダリングする内容を伝えることで、React は関連するデータが変更された場合に効率的にレンダリング内容をアップデートする
  • React コンポーネントは React.Component クラスを継承して宣言する
  • React コンポーネントは props プロパティを持つ
  • React コンポーネントは state プロパティを持ち、constructor で定義・初期化される
    • state プロパティは setState() メソッドを使って値を設定する(以下は例)
      • 例えば setState({ value: 'x' }) 等、オブジェクトで指定する方法
    • state プロパティは state.SOMENAME を使って参照できる
  • React コンポーネントのレンダリング内容は render() 関数の返値で指定する
    • 多くの場合 JSX 形式でレンダリング内容を記述する
    • レンダリング内容は複数の React element で構成される
    • JSX 形式で記述された内容はタグごとに React.createElement() 関数を利用する記述形式へ変換される

props を使ったコンポーネント間の値の受け渡し

親子関係にある React コンポーネントの親から子へ値を渡す場合は、親の render() メソッド内で子を呼び出す箇所でオプションとして渡す。

ここで渡された値は子において、props.SOMENAME を使って参照できる。

例: https://reactjs.org/tutorial/tutorial.html#passing-data-through-props

その他、親が子において発生した特定のイベント(マウスクリックイベント等)を感知したい場合、callback 関数を onClicik={() => this.handleClick()} 等と指定し、子コンポーネント側から onClicik={() => this.props.onClicik()} を呼び出す方法がある。

state による DOM コンポーネントの値の保持と更新処理

React コンポーネントは state プロパティを持つ。
この state には自身のコンポーネントを表示するために必要な値を格納する。
(props がオブジェクト間の値の受け渡しに使うものに対して、state はコンポーネントの状態や操作によって変化する値を格納するものと捉えている)

state が更新されると render() 関数が呼び出されコンポーネントが更新される。
state を API から取得する場合は、constructor で初期化を行った後、レンダリングされる前に呼び出される componentWillMount 関数内で AJAX(fetch 関数や Axios ライブラリを使う) を用いてデータが取得出来たら state の値を設定する流れとなる。
※ state が更新されると自動的に再 render される

参考情報: https://reactjs.org/docs/state-and-lifecycle.html

express-react-view による Express と React 連携

前編までの流れでテンプレートエンジンで react コンポーネントが利用できれば良いと思い、名前からして Express と React を併用できると思われた express-react-view をテンプレートエンジンとして指定してみたところ、
views ディレクトリ配下に React コンポーネントを使った View を用意できるようになるが、SSR(Server Side Rendering) により static な HTML が出力される。そのため、クライアントサイドでレンダリングしたい場合には利用できない。express-react-view の公式ドキュメントにも下記のとおり、その旨が記述されている。

This is an Express view engine which renders React components on server. It renders static markup and does not support mounting those views on the client.

テンプレートエンジンにexpress-react-viewを指定する
  : <snip>
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jsx');
app.engine('jsx', require('express-react-views').createEngine());
  : <snip>

Bootstrap を使う ※'18/04/05追記

View の見た目を整えるために、HTML/CSS/JavaScript のフレームワークでメジャーな Bootstrap を React で使う方法を番外編として投稿した。

Node.js, Express, sequelize, React で始めるモダン WEB アプリケーション入門(bootstrap番外編)

スペシャルサンクス

編集リクエスト

下記の方より編集リクエストを頂いて記事を修正しました。ご指摘ありがとうございます。

日付 ユーザ名
2018年07月04日 22時12分 (JST) mosin_nozomi さん

指摘・修正内容については編集履歴機能にお任せして割愛させて頂きます。

126
145
29

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
126
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?