ExpressサーバーからReactまでのフロントエンドハンズオン 第１章〜UIサーバー編〜

# 概要
今時のフロントエンドってどうやって実装すればいいのか、実際に作りながら説明する
ところどころ省略しているものもあるが、業務運用に耐えうる設計を前提に書いたつもりである

以下の構成で紹介する。この投稿は第１章

- 第１章 UIサーバー編(今ここ)
- 第２章 [フロントエンド編](https://qiita.com/tyoukan__/items/40048984a73bd9443a45)
- 第３章 フロントエンド-redux編 (まだない)

# 構成

以下のような構成を想定している

<img width="1402" alt="スクリーンショット 2018-12-10 23.04.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/9398707e-b120-2282-6968-618406cb4cab.png">

ここではUIサーバーとフロントエンドの実装を、ログイン画面の作成を通じて説明する

# UIサーバー
UIサーバーはフロントエンド、つまりブラウザとのやりとりの役割を持つ
よくある以下のような構成から、ブラウザとのやりとり部分だけ切り出したものである
<img width="1415" alt="スクリーンショット 2018-12-10 23.04.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/2b5a0afc-765e-a0bb-c70e-e56996d4fd50.png">


具体的な役割は以下

- セッションの管理
- 静的リソースのホスト
- バックエンドとの橋渡し

UIサーバーがセッションの管理や静的リソースのホストを行うことで、バックエンドはJSONでのやりとりを前提としたAPIのみの役割になる


# 扱う技術・キーワード
- Express

# 作るもの
- ログイン済み状態なら、アプリケーション画面(ログイン後の画面)を返す
- 未ログイン状態なら、ログイン画面を返す。アプリケーション画面のリソースは取得できない
- エラー時にはエラーページを返す
- ログイン状態はセッションで管理する
- バックエンドのログインAPIを使ってログインする
- バックエンドのログインAPIはダミーで実装する

完成したものはこちら
https://github.com/yas-tyoukan/frontend-basic-sample


# 前提
node(8以上推奨)が入っていること

# 用意
はじめにディレクトリだけ作る

```
frontend-sample/
　├ backend/
　└ ui/
```


# expressでUIサーバを作る
expressとは
参考： [Node.jsのフレームワーク「Express」とは【初心者向け】](https://techacademy.jp/magazine/16119)

ユーザーがログイン状態かどうかをセッションで管理する
expressサーバは、以下の3つのHTMLを返す

- ログイン状態であればログイン後のページ
- ログイン状態でなければログイン前のページ
- その他エラーがあればエラーページ

SPA(Single Page Application)で作成するので、他のページはない想定

## UIサーバに必要なパッケージのインストール
```sh

$ cd ../ui
$ npm init
  (対話が始まるので適当でいい)
npm i -S express csurf axios express-session body-parser ejs
```

## expressサーバ(UIサーバ)の実装

主な機能は、以下

- 状態に応じて以下の3つのページを返す
  - ログイン状態であればログイン後のページ
  - ログイン状態でなければログイン前のページ
  - その他エラーがあればエラーページ
- ブラウザからのAPIリクエスト(ここではログインのみ)をバックエンドに橋渡しする
- 静的リソースを返す
    - ログイン状態に関わらずリソース
    - ログイン状態である場合にのみ返すリソース

以下、これまでに作ったファイルと、これから作るファイルのディレクトリ構成

```
frontend-sample/
　├ backend/
　└ ui/
   ├ node_modules/         // npm i した時に作られる
   ├ index.js              // 起動設定や共通処理
   ├ package.json          // npm i した時に作られる
   ├ package-lock.json     // npm i した時に作られる
   └ router/
     ├ index.js            // 基本のルーティング設定
     └ api/
       ├ index.js          // APIのルーティング設定
       └ login/
         └ index.js        // ログインAPIのルーティング設定
```

### ui/index.jsの作成
`ui/index.js`を以下のように作成する

```javascript

// frontend-sample/ui/index.js
const http = require('http');
const express = require('express');
const expressSession = require('express-session');
const bodyParser = require('body-parser');
// redis使う場合は以下のようなパッケージを使う。今回は省略
// const connectRedis = require('connect-redis');
// const IORedis = require('ioredis');

// -------- 環境変数のチェック -------- //
// 必要な環境変数が足りてるか、起動時に分かるようにしている
// 今回は省略
// 以下例
//  if(process.env.SESSION_SECRET_KEY) {
//    throw new Error('env SESSION_SECRET_KEY is not set.')
//  }

const app = express();

// -------- X-Powered-Byヘッダの無効化 -------- //
app.disable('x-powered-by');

// -------- サーバーの起動 -------- //
const server = http.Server(app);
// 環境変数でポートを設定などする。今回は省略して固定
const port = 3000;
server.listen(port);

// -------- セッションの設定 -------- //
// redisを使うならその設定が必要
// スケーリングする場合はメモリではダメ
// 開発環境ならメモリ、そうでないならredisのように分けると開発環境でredis不要になる
// 今回は省略してメモリストアを固定で使っている
const sessionStore = new expressSession.MemoryStore();


// ------ cookie,sessionの設定 ------- //
// expressSessionのオプションは以下参照
// https://github.com/expressjs/session
const session = expressSession({
  store: sessionStore,
  secret: 'catIsKawaii', // 環境変数で設定などする。今回は省略して固定値
  resave: false,
  saveUninitialized: false,
  rolling: true,
  proxy: false, // reverse proxy経由などの場合はtrueにする。環境で分けるようにする。今回は省略
  cookie: {
    secure: false, // httpsならtrueにする。環境で分けるなどする。今回は省略
    httpOnly: true,
    rolling: true,
    maxAge: 3600000, // ミリ秒で指定。環境変数で設定するべきだが、今回は省略
  },
});

app.use(session);

// ------ テンプレートエンジンの設定 ------ //
// ejsを使う
app.set('views', 'views/pages');
app.set('view engine', 'ejs');

// ------ bodyParserの設定 ------- //
// bodyParserの設定は以下参照
// https://github.com/expressjs/body-parser
// クエリパラメータのパースの設定
app.use(bodyParser.urlencoded({
  // extended: trueにするとオブジェクトのネストや配列を保持したまま受け取れる。"foo[bar]=baz"->{foo:{bar:'baz'}}
  extended: true,
  limit: '10mb',
}));

// リクエストボディの容量制限
app.use(bodyParser.json({
  limit: '10mb',
}));

// ------ ルーティング ------ //
app.use('/', require('./router'));

// -------------------------------------------------
//  以下、何のルーティングにもマッチしないorエラー
// -------------------------------------------------

// いずれのルーティングにもマッチしない(==NOT FOUND)
app.use((req, res) => {
  res.status(404);
  res.render('error', {
    param: {
      status: 404,
      url: req.url,
      message: 'not found',
    },
  });
});

// エラーハンドリング
// 引数が4つの関数を設定すると、エラーハンドラ扱いになる
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
  // 想定されるerrの内容によって場合分けなど
  if (err.code === 'EBADCSRFTOKEN') {
    // CSRFTokenのエラー
    res.status(403);
    res.json(err);
    return;
  }
  if (req.method !== 'GET' || /\/api\/.*/.test(req.url)) {
    // GET以外のエラー、または、'/api/*'へのアクセスならエラーオブジェクトを返す
    res.status(500 || err.status);
    res.json(err);
    return;
  }
  // エラーページを返す
  res.render('error');
});

module.exports = app;
```

### ui/router/\*\*/\* にルーティング設定の作成

ルーティングに関する設定は、別ファイルに切り出している。
以下のように、`ui/router/index.js`を作成し、ルーティング設定を記述する

```javascript
// ui/router/index.js
const express = require('express');
const csurf = require('csurf');

const router = express.Router();

// ------ ルーティングのログ出力など共通処理 ------ //
router.all('/*', (req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
});

// ------- CSRF対策のミドルウェア設定 ------- //
router.use(csurf({
  cookie: false,
}));

// csrfTokenを格納
router.use((req, res, next) => {
  // CSRF対策トークンを入れる
  const locals = res.locals;
  locals.csrfToken = req.csrfToken();
  return next();
});

// -------- 認証チェックが不要なルーティング設定 ここから -------- //
// csrfToken単体で取得
router.get('/csrf-token', (req, res) => {
  res.json({ token: res.locals.csrfToken });
});

// 静的ファイルのルーティング
router.use(express.static('public'));

// ログイン
// eslint-disable-next-line no-unused-vars
router.use('/login', (req, res, next) => {
  // ログインページを返す
  res.render('login');
});

// ログアウト
router.get('/logout', (req, res, next) => {
  // 未ログインの場合は何もせずに/loginにリダイレクト
  if (!req.session.user) {
    res.redirect('/login');
    return;
  }
  // ログイン済みの場合はセッションを破棄してから/loginにリダイレクト
  req.session.destroy((err) => {
    if (err) {
      next(err);
      return;
    }
    res.redirect('/login');
  });
});

// ログインページに飛ばすURLの正規表現
// ログイン後のページではルーティングせずに、ログインページだけでルーティングするURLがあればここに追加する
// /login/*以下と、/logoutはログインページでルーティングする
const urlsRoutedLoginPage = /^\/(login(\/.*)?|logout)$/;

// ログイン前にアクセス可能なAPI
// パスワードリセットAPIへのアクセスなど、login前でも使用するAPIがあればここに追加する
const apisAccessibleWithoutLogin = /^\/api\/login$/;

// ログイン画面
router.get(urlsRoutedLoginPage, (req, res) => {
  res.render('login');
});

// -------- 認証チェックが不要なルーティング設定 ここまで -------- //

// ------------------ 認証チェック ------------------ //
router.use((req, res, next) => {
  if (apisAccessibleWithoutLogin.test(req.url)) {
    // ログイン不要でアクセスできるAPIへのアクセスは認証チェックしない
    next();
    return;
  }
  // ログイン済みかどうかチェック
  const { session } = req;
  const authenticated = session && session.authenticated;
  if (authenticated) {
    // ログイン済みならOK
    next();
    return;
  }
  // ----- 以下は未ログインの場合 ----- //
  // GET以外のアクセス及びAPIアクセスの禁止
  if (req.method !== 'GET' || /\/api\/.*/.test(req.url)) {
    // 401を返して終了
    // ui/index.jsのエラーハンドリングで処理される
    next({ status: 401 });
    return;
  }
  // APIアクセスでないGETアクセスは、すべてログインページを返す
  res.redirect('/login');
});

// -------- 認証チェックが必要なルーティング設定 -------- //

// 静的ファイルのルーティング
router.use(express.static('public_authenticated'));

// APIのルーティング
router.use('/api', require('./api'));

// ログイン後のページのルーティング
router.get('/*', (req, res) => {
  res.header('Content-Type', 'text/html');
  res.render('app');
});

module.exports = router;
```
`res.render('login');` としているところは、`ui/index.js`で`app.set('views', 'views/pages');` としたことにより、`views/pages/login.ejs`の内容を返すようになる。 ejsファイルの作成は後にして、次にAPIへのルーティングを実装する

APIへのルーティングに関する設定は、別ファイルに切り出している
今回はログインAPIを作成する
`POST: /api/login`でリクエストを送る想定
以下のファイルを作成して、ログインAPIのルーティングと実装を行う

- `ui/router/api/index.js`
- `ui/router/api/login/index.js`

```javascript
// ui/router/api/index.js
const router = require('express').Router();

// ------ APIのルーティング ここから ------ //
router.use('/login', require('./login'));
// ------ APIのルーティング ここまで ------ //

router.all('/*', (req, res) => {
  // APIのルーティングにマッチしなかったものは404をJSONで返す
  res.status(404).json({ url: req.url, message: 'not found' });
});

module.exports = router;
```
APIが増えたらloginと同様に、`// ------ APIのルーティング ここから ------ //`の部分に追加する
すでに`router/index.js`で、loginのAPIへのアクセスはログイン前に可能で、それ以外のAPIへのアクセスはログイン前にはできないように設定している

次に、ログインAPIのルーティングと実装をする

```javascript
// ui/router/api/login/index.js
const router = require('express').Router();
const axios = require('axios');

router.route('/').post((req, res, next) => {
  const { id, password } = req.body;
  if (!id || !password) {
    // idまたはpasswordがない場合は、バックエンドに送らずにエラーとして処理
    res.status(401).json({ message: 'failed' });
    return;
  }

  // 環境変数を元にURLを生成する。今回は省略して固定
  const url = 'localhost:3001/login';

  axios.post(url, { id, password }).then(({ body }) => {
    if (!body.user) {
      // userが渡されなかったらログイン失敗とみなす
      res.status(401).json({ message: 'failed' });
      return;
    }
    // 取得したデータを元にセッションを再生成
    req.session.regenerate((error) => {
      if (error) {
        // セッション再生成に失敗したらエラー
        next(error);
        return;
      }
      // セッションに必要な情報を格納
      const { session } = req;
      const { user } = body;
      session.user = user;
      res.json({ user });
    });
  }).catch(next);
});

module.exports = router;
```


## ページの作成
ejsを使う 
参考 [Expressにおけるejsの使い方](https://qiita.com/kamihork/items/1b13d2157979d1837849)

以下、3つのページを作成する

- 未ログイン時のページ
- ログイン済の時のページ
- エラーページ

共通部分は部分ファイルに抜き出している（コード中の`include()`している部分)

以下、現時点で作成したディレクトリと、これから作るファイルのディレクトリ構成

```
frontend-sample/
　├ backend/
　└ ui/
   ├ node_modules/
   ├ router/
   ├ package.json
   ├ package-lock.json
   ├ index.js
   └ views/
     ├ pages/
       ├ login.ejs
       ├ app.ejs
       └ error.js
     └ partials/
       └ head.ejs   // APIのルーティング設定    
```

### 各ページ(ejs)の実装
```html
<%# ui/views/pages/login.ejs %>
<!DOCTYPE html>
<html lang="ja">
<%- include('../partials/head', { title: 'ログイン' }) %>
<body>
  <div id="root"></div>
  <script src="/js/login.js"></script>
</body>
</html>
```

```html
<%# ui/views/pages/app.ejs %>
<!DOCTYPE html>
<html lang="ja">
<%- include('../partials/head', { title: 'ログインした人だけ見られるページ' }) %>
<body>
  <div id="root"></div>
  <script src="/js/app.js"></script>
</body>
</html>
```

```html
<%# ui/views/pages/error.ejs %>
<!DOCTYPE html>
<html lang="ja">
<head>
<title>ページが表示できません</title>
</head>
<body>
  <p>ページが表示できません</p>
</body>
</html>
```
エラーページのデザインについては省略している。必要ならstyleやscriptなどを追加する。

各ページで`include`している部分ファイル

```html
<%# ui/views/partials/head.ejs %>
<head>
  <meta charset="utf8">
  <meta name="csrf-token" content="<%= csrfToken %>">
  <meta
    name="viewport"
    content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
  />
  <title><%= title %></title>
</head>
```

ejsは`locals`のプロパティにアクセスできるので、`ui/router/index.js`で`locals.csrfToken = req.csrfToken();`と書いたことにより、`csrf-token`を埋め込める

UIサーバーはここまでで完成

## 動作確認
UIサーバーを立ち上げる

```sh
$ cd ui/
$ node index.js
```
エラーが出て立ち上がらなかったら、何かが（あるいはこの投稿が）おかしいのでエラーメッセージを元に修正すること

別ウィンドウで以下で確認

ログインページの取得

```sh
$ curl localhost:3000/login
(login.ejsの内容が帰ってくる)
```

ログインAPIへのPOST(CSRFTokenのエラー)

```sh
$  curl -X POST -H "Content-Type: application/json" -d '{"id":"user1", "password":"p"}' localhost:3000/api/login
{"code":"EBADCSRFTOKEN"}
```

## ダミーのバックエンドのログインAPIを作る
ログインAPIがアクセスするバックエンドのエンドポイントをダミーで実装する

`json-server`を使う
`json-server`は簡単にモックのAPIを作れるので便利

```sh
$ cd backend
$ npm init
  (対話が始まるので適当でいい)
$ npm i json-server
```

以下、作られたファイルとこれから作るファイルのディレクトリ構成

```
frontend-sample/
　├ ui/
　├ backend/
   ├ node_modules/
   ├ package.json
   ├ package-lock.json
   ├ db.json             // json-serverの立ち上げに必要（ここでは空ファイル）
   └ login-mock.json     // ログインAPIのモック実装
```

### ログインAPIのモックを作成する
`POST: /login` して、 `{username:'user1', password:'p'}` なら200, それ以外なら401を返すだけのものにした。

```js
// backend/login-mock.js
module.exports = (req, res, next) => {
  if (req.method === 'POST' && req.path === '/login') {
    const { id, password } = req.body;
    if (id === 'user1' && password === 'p') {
      res.status(200).json({ user: { id } });
    } else {
      res.status(401).json({ message: 'failed' });
    }
    return;
  }
  next();
};
```

空でいいので`db.json`を作る

```sh
$ touch db.json
```

json-serverを立ち上げる
UIサーバと被らないようにポート3001にしている

```sh
$ npx json-server  db.json -m login-mock.js -p 3001
```

以下のように表示されればOK

```
  \{^_^}/ hi!

  Loading db.json
  Loading login-mock.js
  Done

  Resources

  Home
  http://localhost:3001

  Type s + enter at any time to create a snapshot of the database
```

## 動作確認

別ターミナルウィンドウで

```sh
$ curl -X POST -H "Content-Type: application/json" -d '{"id":"user1", "password":"p"}' localhost:3001/login
{
  "user": {
    "id": "user1"
  }
}
```

これでバックエンドのモックはOK



CSRFTokenを設定したログインAPIや、ログイン後の画面はフロントの実装をしてから確認する

## package.jsonにscriptを書く
サーバーの立ち上げなど、楽にするために、`package.json`に`script`を書いておくと便利

backend/package.json

```
{
  "name": "backend",
  "version": "0.0.0",
  "private": true,
  "description": "mock backend api",
  "dependencies": {
    "json-server": "^0.14.0"
  },
  "scripts": {
    "start": "json-server db.json -m login-mock.js -p 3001"
  }
}
```

以下で、モックサーバーが立ち上がるようになる

```sh
$ cd backend
$ npm run start
```


ui/package.json

```
{
  "name": "ui",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.18.0",
    "body-parser": "^1.18.3",
    "csurf": "^1.9.0",
    "ejs": "^2.6.1",
    "express": "^4.16.4",
    "express-session": "^1.15.6"
  },
  "scripts": {
    "start": "node index.js"
  }
}
```

以下で、UIサーバーが立ち上がるようになる

```sh
$ cd ui
$ npm run start
```

## 次やること
次に、UIサーバーが返すHTMLファイルに書かれている（=ejsファイルに書いた)、`js/login.js`と`js/app.js`を作成する。
これらはそれぞれ、`ui/public/js/login.js`と`public_authenticated/js/app.js`に配置すれば、UIサーバーが静的リソースとしてホストする。

ちなみに、前者はログイン前でもログイン後でもアクセスでき、後者はログイン後でないとアクセスできない。
その設定は、`ui/router/index.js`で記述している。
これによって、ログイン前にビジネスロジックの入ったコードを権限のないユーザーに見られるリスクを無くしている。

`ui/public/js/login.js`と`public_authenticated/js/app.js`にそれぞれjsファイルをおき、前者のjsが読み込まれたページに正しいログインフォームを実装すれば完成である
フロントエンド編では、それらのjsファイルを今時の技術を使って作りながら紹介する

以下、フロントエンド編で扱う技術（予定)

- react
- redux
- webpack
- babel
- eslint
- atomic design
- storybook
- less

第１章はここまで