入門: Vue.jsでCRUDアプリケーションを作る 

## はじめに

フロントエンド開発、最近はやはりJSで書くのが基本だとは思うけどReactやAngularは少し敷居が高いのでVue.jsに手を出してみました。
とりあえず、自分へのメモの意味合でVue JSのCRUDアプリケーションのチュートリアルを書いてみます。CRUDが書ければあとは必要に応じて普通に拡張できるでしょうし。たぶん。

今回はAPIサイドをJavaのQuarkusで、フロントエンドをVue.jsで書いています。

## サーバサイドをQuarkusで作成

### プロジェクト作成

まずサーバサイドを作成します。Mavenでまずは以下のようにQuarkusプロジェクトを生成します。なお、mavenのバージョンが3.6.2より低い場合は事前に上げておくこと。

```bash
$  mvn io.quarkus:quarkus-maven-plugin:2.12.2.Final:create \
    -DprojectGroupId=crud \
    -DprojectArtifactId=crud-server \
    -DclassName="crud.ItemResource" \
    -Dpath="/items"
```

つづいて、必要なプラグインをインストール。今回はDB周りとJAX-RS周りを入れています。

```bash
$ cd crud-server/
$ ./mvnw quarkus:add-extension -Dextensions="quarkus-hibernate-orm-panache,quarkus
-jdbc-h2,quarkus-resteasy-jackson,quarkus-resteasy-jsonb"
```

ビルドの確認

```bash
$ ./mvnw compile quarkus:dev
```

### 永続化層の作成

まずは、application.propertiesの設定を変更。

```properties
# DB Config
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:
quarkus.hibernate-orm.database.generation = drop-and-create
```

続いてEntityの作成。getter/setterは長いので省略。

```java
@Entity
public class Item {
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(
            name = "UUID",
            strategy = "org.hibernate.id.UUIDGenerator"
    )
    @Column(name = "id", updatable = false, nullable = false)
    private UUID id;
    private String name;
    private int price;

    public Item() { }

    public Item(UUID id, String name, int price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
    public UUID getId() {
        return id;
    }
    public void setId(UUID id) {
        this.id = id;
    }
- 略 -
}
```

最後にRepository.

```java
@ApplicationScoped
public class ItemRepository implements PanacheRepository<Item> {

    public Item findById(UUID id) {
        return this.find("id", id).list().get(0);
    }

    public void update(UUID id, Item item) {
        var x = findById(id);
        x.setName(item.getName());
        x.setPrice(item.getPrice());
    }
}
```

### エンドポイント作成

エンドポイントとしてJAX-RSで簡単なREST APIを作成する

```java
@Path("/items")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public class ItemResource {
    @Inject
    ItemRepository repository;

    @GET
    @Path("{id}")
    public Item get(@PathParam("id") UUID id) {
        return repository.findById(id);
    }
    @GET
    public List<Item> list() {
        return repository.listAll();
    }
    @POST
    public void create(Item item) {
        repository.persist(item);
    }
    @PUT
    @Path("{id}")
    public void update(@PathParam("id") UUID id, Item item) {
        repository.update(id, item);
    }
    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") UUID id) {
        repository.delete("id", id);
    }
}
```

### CORSの設定

このままではポート番号が違うため例えローカルホストであってもCORSでエラーになる。
そのため、CORS対応に設定をapplication.propertiesに追加する. この例ではQuarkus側(API)がポート8080, Vue.js側が5173で起動する想定。

本番環境では構成によっては同一サーバで動くと思うので、その場合はCORSに関する設定はしなくて良い。

```properties
# HTTP Config
quarkus.http.port=3000
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:8080,http://172.19.27.127:5173
quarkus.http.cors.methods=GET,PUT,POST,DELETE
```

### 動作確認

サーバサイドの動作確認を行います。

```bash
$ ./mvnw compile quarkus:dev
```

crulでAPIを実行。

```bash
$ URL=http://localhost:8080/items
$ JSON_TYPE="Content-Type:application/json"

$ curl -XGET ${URL} -H $JSON_TYPE
[]

$ curl -XPOST ${URL} -H $JSON_TYPE -d '{"name" : "new item 02", "price" : 128}'
$ curl -XPOST ${URL} -H $JSON_TYPE -d '{"name" : "new item 02", "price" : 128}'
$ curl -XGET ${URL} -H $JSON_TYPE
[{"id":"57cda117-474a-48bf-85f3-8f83dd33dac9","name":"new item 01","price":64},{"id":"bc5cc9fd-0ba9-4b02-91e8-93fdf9ed59fd","name":"new item 02","price":128}]

$ curl -XPUT ${URL}/"57cda117-474a-48bf-85f3-8f83dd33dac9" -H $JSON_TYPE -d '{"name" : "new item 01", "price" : 1024}'
$ curl -XGET ${URL} -H $JSON_TYPE
[{"id":"bc5cc9fd-0ba9-4b02-91e8-93fdf9ed59fd","name":"new item 02","price":128},{"id":"57cda117-474a-48bf-85f3-8f83dd33dac9","name":"new item 01","price":1024}]

$ curl -XDELETE ${URL}/"bc5cc9fd-0ba9-4b02-91e8-93fdf9ed59fd" -H $JSON_TYPE
$ curl -XGET ${URL} -H $JSON_TYPE
[{"id":"57cda117-474a-48bf-85f3-8f83dd33dac9","name":"new item 01","price":1024}]
```

APIが適切に動作してるのが分かります。OpenAPIのUIで確認したいなら下記のURLから確認。
http://localhost:3000/swagger-ui/

## クライアントサイドをVue.jsで作成

続いて本命のクライアントサイドアプリをVue.jsで実装します。

### プロジェクトの作成

まずはvue-cliのインストールを行います。

```bash
$ yarn global add @vue/cli
```

続いてプロジェクトの作成。選択肢はマニュアルを選択してRouterを追加します。面倒なので今回はLintも外します。後の選択肢は好みで。

```bash
$ vue create crud-client
Vue CLI v4.2.3
? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◯ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
```

開発サーバを立てて動作確認をします。

```bash
$ cd crud-client
$ yarn serve
```

### ネットワークライブラリの追加

JS標準のFetch APIだと記述が冗長なのでaxiosを使います。

```bash
$ yarn add axios vue-axios
```

### UIフレームワークの追加

UIフレームワークとしてBootstrapを入れます。合わせてリッチなアラートを提供するsweetalertもインストールします。

```bash
$ yarn add bootstrap vue-sweetalert2
```

### Vue Configの設定をする

通常、本番環境やテスト環境と開発環境ではAPIの接続先が異なると思います。APIサーバをローカルに立てたりするので。
なので、開発サーバの設定を変更して、そこで接続先を書き換えます。ルートディレクトリにvue.config.jsを追加して以下のように修正します。

また、タイトルも同様にここで設定するので追加しておきます。

```vue.config.js
module.exports = {
    devServer: {
        proxy: {
            "^/items": {
                target: "http://localhost:3000",
                ws: false,
                pathRewrite: {
                    "^/items": "/items"
                }
            }
        }
    },
    pages: {
        index: {
            entry: 'src/main.js',
            title: 'My CRUD Apps',
        }
    }
};
```

### ライブラリのインポート

先ほど、yarnでインストールしたライブラリを読み込むためにmain.jsを以下のように修正します。

```src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import axios from 'axios';
import VueAxios from 'vue-axios';
import VueSweetalert2 from 'vue-sweetalert2';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../node_modules/sweetalert2/dist/sweetalert2.min.css';

Vue.config.productionTip = false
Vue.use(VueSweetalert2);
Vue.use(VueAxios, axios);

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
```

### CRUD画面を作る

本題のCRUD画面を作ります。メイン画面はviews配下に作るのが作法のようです。
とりあえず不要なファイルを消しましょう。

```bash
$ rm src/views/About.vue
$ rm src/views/Home.vue
```

#### 作成画面

Script側で定義したデータモデルに対してリアクティブにテンプレート側の値が変更されます。
サーバ側でエラーが発生すれば`message`にメッセージとして表示されるようにしています。
`this`を多用しているので無名関数をアロー演算子ではなくfunctionで作成してしまうとスコープが変わって動かなくなるので注意。

また、作成完了時には一覧画面に遷移しますが、遷移後に作成完了のメッセージを一覧画面に表示する代わりにSweetAlertでメッセージを出しています。
これは、Vue.jsではJavaEEやRailsのFlush領域のように画面遷移に際して値をバックエンドで受け渡す仕組みが無いためエラーメッセージを同じUIで出すのは無駄に困難です。なので似たUXとなるリーズナブルなUIを選択しています。
 
```src/views/Create.vue
<template>
  <div class="container">
    <div class="card">
      <div class="card-header">
        <h3>Add Item</h3>
      </div>
      <div class="card-body">
        <form v-on:submit.prevent="addItem">
          <div v-show="message" class="alert alert-danger">{{message}}</div>
          <div class="form-group">
            <label>Item Name:</label>
            <input type="text" class="form-control" v-model="item.name" />
          </div>
          <div class="form-group">
            <label>Item Price:</label>
            <input type="text" class="form-control" v-model="item.price" />
          </div>
          <div class="form-group">
            <input type="submit" class="btn btn-primary" value="Add Item" />
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  components: {
    name: "AddItem"
  },
  data() {
    return {
      item: {},
      message: ""
    };
  },
  methods: {
    addItem() {
      let uri = "/items/";
      this.axios
        .post(uri, this.item)
        .then(() => {
          this.$swal({
            icon: "success",
            text: "Created Success!"
          });
          this.$router.push({ name: "Index" });
        })
        .catch(error => {
          this.message = `status: ${error.response.status}, message: ${error.response.data}`;
        });
    }
  }
};
</script>
```

#### 一覧画面

一覧画面では画面を表示した際に呼ばれるcreatedメソッドの中で、一覧を取得するAPIを呼んでいます。
また、削除のAPIコールと編集への遷移も行っています。

```src/views/Index.vue
<template>
  <div>
    <h1>Items</h1>

    <table class="table table-hover">
      <thead>
        <tr>
          <td>ID</td>
          <td>Item Name</td>
          <td>Item Price</td>
          <td>Actions</td>
        </tr>
      </thead>

      <tbody>
        <tr v-for="item in items" :key="item._id">
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.price }}</td>
          <td>
            <router-link :to="{name: 'Edit', params: { id: item.id }}" class="btn btn-primary">Edit</router-link>
          </td>
          <td>
            <button class="btn btn-danger" v-on:click="deleteItem(item.id)">Delete</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: []
    };
  },
  created: function() {
    this.fetchItems();
  },
  methods: {
    fetchItems() {
      let uri = "/items";
      this.axios.get(uri).then(response => {
        this.items = response.data;
      });
    },
    deleteItem(id) {
      let uri = "/items/" + id;
      this.axios.delete(uri).then(() => {
        this.fetchItems();
      });
    }
  }
};
</script>
```

#### 編集画面

編集画面は基本的に作成画面と同様です。違いはサーバサイドにIDで値を問い合わせてるかくらいですね。

```src/views/Edit.vue
<template>
  <div class="container">
    <div class="card">
      <div class="card-header">
        <h3>Edit Item</h3>
      </div>
      <div class="card-body">
        <form v-on:submit.prevent="updateItem">
          <div v-show="message" class="alert alert-danger">{{message}}</div>
          <div class="form-group">
            <label>Item Name:</label>
            <input type="text" class="form-control" v-model="item.name" />
          </div>
          <div class="form-group">
            <label>Item Price:</label>
            <input type="text" class="form-control" v-model="item.price" />
          </div>
          <div class="form-group">
            <input type="submit" class="btn btn-primary" value="Update Item" />
          </div>
        </form>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      item: {},
      message: ""
    };
  },
  created: function() {
    this.getItem();
  },
  methods: {
    getItem() {
      let uri = "/items/" + this.$route.params.id;
      this.axios.get(uri).then(response => {
        this.item = response.data;
      });
    },
    updateItem() {
      let uri = "/items/" + this.$route.params.id;
      this.axios.put(uri, this.item).then(() => {
        this.$swal({
          icon: "success",
          text: "Updated Success!"
        });
        this.$router.push({ name: "Index" });
      });
    }
  }
};
</script>
```

#### メインテンプレートの変更

App.vueを変更してメインのテンプレートを変更します。

```src/App.vue
<template>
  <div id="app" class="container">
    <nav class="navbar navbar-expand-sm bg-light">
      <ul class="navbar-nav">
        <li class="nav-item">
          <router-link :to="{ name: 'Create' }" class="nav-link">Add Item</router-link>
        </li>
        <li class="nav-item">
          <router-link :to="{ name: 'Index' }" class="nav-link">All Items</router-link>
        </li>
      </ul>
    </nav>
    <transition name="fade">
      <div class="gap">
        <router-view></router-view>        
      </div>
    </transition>
  </div>
</template>

<script>

export default {
}
</script>

<style>
    .fade-enter-active, .fade-leave-active {
      transition: opacity .5s
    }
    .fade-enter, .fade-leave-active {
      opacity: 0
    }
    .gap {
      margin-top: 50px;
    }
</style>
```

### ルーティングの変更

先ほど作成した画面をルーティングに登録するために`src/router/index.js`を修正します。

```src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Create from '../views/Create.vue'
import Edit from '../views/Edit.vue'
import Index from '../views/Index.vue'

Vue.use(VueRouter)

const routes = [
  {
    name: 'Index',
    path: '/',
    component: Index
  },
  {
    name: 'Create',
    path: '/create',
    component: Create
  },
  {
    name: 'Edit',
    path: '/edit',
    component: Edit
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router
```

## まとめ

とりあえずVue.jsとJavaで作ったAPIでCRUDを実現することができました。
正直、そこまでスケーラビリティを求めないならアプリではPHPやJSP/JSF、あるいはRailsで作った方が楽じゃないかの疑惑も拭えないですが.

次はログイン機能あたりを実装してみたいと思います。

それでは、Happy Hacking!

## 参考

- [第２回 – サンプル作成したVue.jsのプロジェクト解析](https://swallow-incubate.com/archives/blog/20190416/)