Vue-CLI3 ベースのアプリ開発で JSON API が使える express server を使う

## はじめに

Webpack を使ったフロントエンド開発では、Webpack dev server を使うと、ファイルの変更をチェックして自動的にビルド・ブラウザのリロードをやってくれます。このとき、サーバ側に REST API の機能を持たせたい…ということがあります。

それを Vue-CLI3 ベースのプロジェクトでやる方法について紹介します。

## 使う道具

Ubuntu 18.04 でやっています。

```
hagiwara@dev01:~/tmp$ nodejs --version
v8.10.0
hagiwara@dev01:~/tmp$ npm --version
6.6.0
hagiwara@dev01:~/tmp$ vue --version
3.3.0
hagiwara@dev01:~/tmp$
```

## Vue-CLI3 ベースのプロジェクトを用意する

とりあえずサンプルのプロジェクトをサクッと立てます。

```
hagiwara@dev01:~/tmp$ vue create foobar

Vue CLI v3.3.0
? Please pick a preset: default (babel, eslint)

Vue CLI v3.3.0
✨  Creating project in /home/hagiwara/tmp/foobar.
🗃  Initializing git repository...
⚙  Installing CLI plugins. This might take a while...

(省略)

🎉  Successfully created project foobar.
👉  Get started with the following commands:

$ cd foobar
$ npm run serve

hagiwara@dev01:~/tmp$
```

ひとまず dev server 起動してみましょう。

```
hagiwara@dev01:~/tmp/foobar$ npm run serve

> foobar@0.1.0 serve /home/hagiwara/tmp/foobar
> vue-cli-service serve

DONE  Compiled successfully in 4841ms

  App running at:
  - Local:   http://localhost:8080/
  - Network: http://10.0.2.15:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.
```

`localhost:8080` にアクセスするとこんな感じで動きます。
![a01.png](https://qiita-image-store.s3.amazonaws.com/0/154230/1898f25f-c073-4c27-a33c-273cc1c89bdd.png)

## vue-cli-plugin-express で API Server を作る

[vue-cli-plugin-express - npm](https://www.npmjs.com/package/vue-cli-plugin-express) を使います。

### インストール
```
hagiwara@dev01:~/tmp$ cd foobar/
hagiwara@dev01:~/tmp/foobar$ vue add express

📦  Installing vue-cli-plugin-express...

(省略)

✔  Successfully installed plugin: vue-cli-plugin-express

? Should serve vue app? Yes
? Where will be located your server? ./srv

🚀  Invoking generator for vue-cli-plugin-express...
✔  Successfully invoked generator for plugin: vue-cli-plugin-express
   The following files have been updated / added:

     srv/index.js
     vue.config.js
     package.json

   You should review these changes with git diff and commit them.

hagiwara@dev01:~/tmp/foobar$
```
途中の質問についてはデフォルト回答ですが、"Should serve vue app?" は yes にしてください。この後出てくる "Fallback" 動作に関する設定です。インストールすると、表示があるとおり、いくつかファイルが変更・追加されてます。

### 設定

デフォルト設定で動かしましょう。追加された `srv/index.js` に express server のコードがコメントアウトされた状態で貼っているので、とりあえず GET api のみ使えるようにします。

```javascript:srv/index.js
import express from 'express';

export default app => {
  app.use(express.json());

  app.get('/foo', (req, res) => {
    res.json({msg: 'foo'});
  });
}
```

### API Server を動かす

```
hagiwara@dev01:~/tmp/foobar$ npm run express
DONE  Mon Jan 28 2019 17:37:14 GMT+0900 (JST)  

  ♻️  Server running at:
    - Local:   http://localhost:3000/
    - Network: http://10.0.2.15:3000/


  ⚙  You're in development mode. to start the application, run yarn serve.

  🎉 Fallback to this server enabled: you can use relative routes in your code!

  🔀 api routes found:
    - /foo: GET
```

`localhost:3000/foo` をたたいてみましょう。
![a02.png](https://qiita-image-store.s3.amazonaws.com/0/154230/788a337e-6707-5ad4-a1d6-6d51b1a55558.png)
ちゃんと JSON データが返ってきてますね。


## アプリ (Frontend) からAPIを使う

ここまでだとだからどうしたって話なんですよね。まだアプリはアプリ、API Server は API Server で個別に動いているだけです。

### アプリ側サーバ起動

`npm run express` して API Server を起動している状態で、`npm run serve` してアプリ用の dev server も起動しましょう。このとき dev server 側で

```
INFO  Starting development server...
DONE  Express server  Fallback will be done on your server. You can use relative calls to it in your code.
```

というメッセージが出ているのを確認しておいてください。このあと "Fallback" ってなんのこっちゃ、という話をします。

### アプリから REST API をたたく

API `/foo` をたたいたとき返っている `msg` の値をサンプルアプリ側で表示してみます。

まず API Server 側で適当なメッセージを返すように変更。

```javascript:srv/index.js
import express from 'express';

export default app => {
  app.use(express.json());

  app.get('/foo', (req, res) => {
    res.json({msg: 'Welcome to Your Vue.js App FROM API SERVER'});
  });
}
```
アプリ側は `/foo` をたたいて取得した JSON を元にメッセージを設定します。

```diff:src/App.vue
hagiwara@dev01:~/tmp/foobar$ git diff src/App.vue
diff --git a/src/App.vue b/src/App.vue
index fcc5662..2d6dac4 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,7 +1,7 @@
<template>
   <div id="app">
     <img alt="Vue logo" src="./assets/logo.png">
-    <HelloWorld msg="Welcome to Your Vue.js App"/>
+    <HelloWorld v-bind:msg="message"/>
   </div>
</template>
@@ -12,6 +12,18 @@ export default {
   name: 'app',
   components: {
     HelloWorld
+  },
+  computed: {
+    message () {
+      const req = new XMLHttpRequest
+      let message = ''
+      req.open('GET', '/foo', false)
+      req.onload = function () {
+        message = JSON.parse(req.responseText).msg
+      }
+      req.send(null)
+      return message
+    }
   }
}
</script>
hagiwara@dev01:~/tmp/foobar$
```

`HelloWorld` というコンポーネントに `msg` で渡した文字列が表示されるので、そこを computed (算出プロパティ) の `message` で置き換えます。で、`message` の中身は単純に `/foo` をたたく→ JSON をパース → `msg` の値を取り出し、というだけ。(エラーチェックも何もない雑な処理ですがまあサンプルなので。)

アプリはこうなります。ちゃんと `/foo` を GET して取得してきた値で更新できています。
![a03.png](https://qiita-image-store.s3.amazonaws.com/0/154230/95714462-3ce3-ceae-f56d-f365aa1a14d3.png)


これをやってみると、以下の点気づくかと思います。

* App.vue を編集するとアプリが自動的にリビルドされてブラウザ上でリロードされる。
  * これはもともと使ってる dev server の動作
* index.js を編集すると API サーバ側がリロードされる。
  * express plugin もサーバ側ソースコードの更新を watch してリロードしてくれます。
* アプリ側 dev server は tcp/8080, API server は tcp/3000 で起動しているのに、アプリ側コードではそれらの違いを気にしなくても動いている。
  * ここが "Fallback" といってる動作ですね。

express-plugin の中身の動作まで細かく追いかけてるわけではないのですが、簡単な動作確認ができます。API server, Dev server の両方が動いている状態で、API サーバだけ落としてみましょう。その上でアプリ側を更新 (ブラウザ F5) すると、こういうエラーが出ます。

```
Proxy error: Could not proxy request /foo from localhost:8080 to http://0.0.0.0:3000/.
```
……ということで、内部的に Proxy してくれてるみたいですね。

## Mode (develop/production) の設定

上記の話は development mode での利用方法でした。Production では下記のようにします。

まず `npm run build` してパッケージングする (`dist` ディレクトリができます)。

```
hagiwara@dev01:~/tmp/foobar$ npm run build
(省略)
hagiwara@dev01:~/tmp/foobar$ ls dist
css  favicon.ico  img  index.html  js
hagiwara@dev01:~/tmp/foobar$
```

この状態で `npm run express:run` を実行する。

```
hagiwara@dev01:~/tmp/foobar$ npm run express:run

> foobar@0.1.0 express:run /home/hagiwara/tmp/foobar
> vue-cli-service express:run


DONE  Mon Jan 28 2019 18:42:00 GMT+0900 (JST)

  ♻️  Server running at:
    - Local:   http://localhost:3000/
    - Network: http://10.0.2.15:3000/


  📦 You're in production mode. To build the application, run yarn build.

  🎉 Fallback to the app enabled: your application is served!

  🔀 api routes found:
    - /foo: GET
```
これで `localhost:3000` にアクセスするとアプリが動きます。(express plugin でアプリケーションを含めて起動している)

## まとめ

[vue-cli-plugin-express](https://www.npmjs.com/package/vue-cli-plugin-express) を使うと API サーバを含むフロントエンド開発がお手軽にやれるようになります。

* アプリ側は API Server の URL をいちいち気にしなくてよい
* サーバ側は API の定義だけ書けばよい
* development の場合
  * API, App (dev server) をそれぞれ起動する
  * `npm run express` : API server (default, tcp/3000)
  * `npm run serve` : App dev server (default, tcp/8080)
* production の場合 
  * `npm run build` でビルド (packing)
  * `npm run serve` で App/API server を起動 (default, tcp/3000)

## おまけ

### for Heroku

ここまでできていればあとは Node/Express なアプリをデプロイするのと同じなのでそれほど困らないはず。

* デフォルトでは vue-cli が生成した .gitignore に dist ディレクトリが入っているので注意
* サーバのポート番号設定を Heroku にあわせる

```javascript
const port = process.env.PORT || 3000
app.set('port', port)
```
* `heroku config:set NPM_CONFIG_PRODUCTION=false` しておく。
  *  [Node.js now installs `devDependencies` by default](https://devcenter.heroku.com/changelog-items/1376) ということなので特にいじってなければ問題ないです。package.json 見ればわかるんですが、`npm run express` は中で `vue-cli-server` を呼んでいます。で、このパッケージ類が `devDependencies` になってるんですよね。この辺いじると後々面倒だろうから素直に `devDependencies` 含めて全部パッケージ入れましょう。

### vue-cli-plugin-express 以外の方法

vue-cli-plugin-express を使わない場合どうするか、というと…… Dev server 自体が express middleware になっているので、ソレを組み込んだサーバスクリプトを書き始めることになると思います。下記参照…。見ればわかるんですが、まあそれなりに面倒なので。express 周りに詳しい人はいろいろやれて良いのかもしれないけど。

* [Running Vue CLI 3 generated project with custom Express server · GitHub](https://gist.github.com/icebob/0dda386fceb8e14b91d84d057fac676f)
  * [Vue-cli 3 plugin for custom express server - Get Help - Vue Forum](https://forum.vuejs.org/t/vue-cli-3-plugin-for-custom-express-server/37989)