18
18

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 1 year has passed since last update.

Vue.jsで簡単なチャットアプリを作ってみた

Posted at

Vue.jsの学習の為、チャットアプリを作成してみました。
Vue.jsの記法を学んだ後に、どのように成果物を作ればいいかの参考に少しでもなればと思い記事を作ってみました。

実装はGitHubに上げております。
フロント(Vue.js)
https://github.com/tkNoPrivate/chat-project-front
サーバー(Java)
https://github.com/tkNoPrivate/chat-project-server

構成

Vueアプリ構成
vuetify
ルーティング
APIアクセス
エラー処理
アプリ紹介
まとめ

Vueアプリ構成

Vueアプリ構成図.jpg

Vue CLIというコマンドラインツールを使用してプロジェクトの雛形を作成しています。
index.htmlで全画面を描画することになります。 SPAというらしいです。

Viewsディレクトリ

URL毎に切り替える画面を実装したVueファイルを格納しています。routerという公式プラグインで切り替え制御を行います。

componentsディレクトリ

共通で使用する部分など、さらに細かい部品を実装したVueファイルを格納しています。

実装

1. index.htmlにて、idにappを指定します。

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <!-- 省略 -->
  </head>
  <body>
    <!-- 省略 -->
    <div id="app"></div> 
    <!-- built files will be auto injected -->
  </body>
</html>

2. main.jsApp.vueファイルをマウントします。
App.vueをインポートして、templateレンダリングをしています。

main.js
import Vue from 'vue'
import App from './ App.vue'

// ~ 省略 ~

Vue.config.productionTip = false

// ~ 省略 ~
new Vue({ // App.vueをマウント
  // ~ 省略 ~
  render: h => h(App) 
}).$mount('#app') 

3. App.vueでは画面描画に使用するVueファイルを配置します。

App.vue
<template>
  <v-app>
    <Header @searchPost="setPost" />
    <Message v-show="!isShowDialog" />
    <router-view ref="post" />
  </v-app>
</template>

<script>
import Header from "./components/Header";
import Message from "./components/Message";
import Constant from "./common/constant";
import messageStore from "./store/message-store";
import userStore from "./store/user-store";

export default {
  components: {
    Header,
    Message,
  },
  async mounted() {
    const currentPath = this.$route.path;
    if (
      currentPath !== "/" &&
      currentPath !== "/login" &&
      currentPath !== "/account/signup"
    ) {
      userStore.setUserStore();
    }
  },
  data() {
    return {
      searchText: "",
    };
  },
  computed: {
    isShowDialog() {
      return messageStore.state.isShowDialog;
    },
  },
  errorCaptured(err) {
    const status = err.response.status;
    if (status !== 404 && status !== 500) {
      const data = err.response.data.fields;
      const errorFields = data.fields ? data.fields : [];
      messageStore.setMessageInf(Constant.ERROR, data.messages, errorFields);
    }

    return false;
  },
  methods: {
    setPost(searchText) {
      this.$refs.post.searchPost(searchText);
    },
  },
};
</script>


<Header /> タグや <Message /> タグがありますが、これが実装したコンポーネントです。
ヘッダー、メッセージコンポーネントは前画面共通で表示するのでApp.vueに記述します。

<router-view /> タグは、URLが変わった際にテンプレートが切り替わる部分となります。
routerという公式プラグインを使用して制御しています。それについてはルーティングで記載します。

vuetify

デザインは、vuetifyというフレームワークを使用しています。
これを使用することで、簡単に良い感じの見た目になります。

以下公式サイトを参考に実装します。
https://vuetifyjs.com/ja/

ルーティング

各URLに対応したテンプレートを切り替えるには、routerというプラグインを使用します。

前準備

routerの定義ファイルを作成します。今回はrouter/index.jsに実装しています。

index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Index from "../views/Index"; // 1
import AccountSignup from "../views/AccountSignup";
import Login from "../views/Login";
// ~ 省略 ~

Vue.use(VueRouter);

const routes = [ // 1
  {
    path: "/",
    name: "index",
    component: Index,
  },
  {
    path: "/account/signup",
    name: "AccountSignup",
    component: AccountSignup,
  },
  {
    path: "/login",
    name: "Login",
    component: Login,
  },
  // ~ 省略 ~
];

const router = new VueRouter({ // 2
  mode: "history", // historyを指定することでURLに#が含まれなくなる。デフォルトは含まれる為、指定しておく
  base: process.env.BASE_URL, // 基底URLを指定
  routes, // 定義したオブジェクト達
});

// ~ 省略 ~

export default router; // 3

実装ポイント

  • viewsディレクトリにて実装したファイルをインポートし、下記を定義したオブジェクトを作成します。(コメント1)
key value
path URLを指定
name 名前を定義
component コンポーネント名を指定
  • VueRouterのインスタンスを生成し、定義したオブジェクト達を引数に渡します。(コメント2)

  • エクスポートし(コメント3)、エントリポイントのmain.jsにて宣言を追加する。

main.js
new Vue({
  router, // routerの宣言
  vuetify,
  render: h => h(App)
}).$mount('#app')

処理の実装

構成で触れたApp.vueにrouter-viewタグがあったかと思います。

App.vue
<template>
  <!-- 省略 -->
    <router-view />
  <!-- 省略 -->
</template>

URLが切り替わると、ここが切り替わって画面が変わります。

リンク

リンクを作成する場合は下記のようにパスを指定します。

<router-link to="/">リンク</router-link>

プログラム遷移

Scriptから遷移する場合は下記のようにパスを指定します。

this.$router.push("/")

APIアクセス

今回作成したアプリでは、ユーザー情報や投稿をDBに保存しています。
その際に、axiosという非同期通信が出来るJavaScriptのライブラリを使用しています。

前準備

main.jsaxiosを使用するための記述をすることで使用可能です。

main.js
import Vue from 'vue'
import App from './ App.vue'
// ~ 省略 ~
import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.config.productionTip = false
Vue.use(VueAxios, axios) // axiosの使用宣言。VueAxiosを使用することで記述が簡単になるのであわせて宣言。

// ~ 省略 ~

処理の実装

基本的な書き方はこんな感じです。

基本構文
<script>
import axios from "axios";

export default {
// ~ 省略 ~
  data() {
    return {
      // ~ 省略 ~
    };
  },
  methods: {
    get() {
      axios
        .get("/user")
        .then((res) => {
         // 正常時処理
        })
        .catch((e) => {
         // エラー処理
        });
    },
  },
};
</script>


実装ポイント

  • get(またはpost)の引数にURLを指定する。
  • 処理が正常終了すると、thenに処理が入る。上記だとresにレスポンス情報が入っている。
  • APIでエラーが発生した場合、catchに処理が入る。

axios.get(URL)Promiseというオブジェクトを返します。
Promiseには正常またはエラーレスポンスが保管されています。それをthencatchで取り出して処理をすることになります。

async/await

APIの戻り値を使用して処理をしたい場合は、その後の処理をthenの中に書く必要があり可読性が悪くなりそうです。
なので私はasync/awaitなるもので実装することにしました。

async/awaitで書いた
<script>
import axios from "axios";

export default {
// ~ 省略 ~
  data() {
    return {
      // ~ 省略 ~
    };
  },
  methods: {
    async get() {
      const res = await axios.get("/user")
        .catch((e) => {
         // エラー処理
        });

      console.log(res);
    },
  },
};
</script>

実装ポイント

  • 関数にasyncをつける。
  • APIアクセス処理の頭にawaitをつけると、その処理の終了を待つ。
  • 戻り値を変数で受け取る事ができる。

これで、非同期処理でも同期処理のように上から順に処理を記述でき可読性が上がります。
レスポンス情報を使用しない場合など、後の処理に依存しない場合はawaitをつけなくても良いです。

エラー処理

APIアクセスにてエラーが発生した場合はメッセージを上部に表示する挙動にしています。
その際にaxiosのinterceptorsというものを使用してエラーを検知しています。
axiosでのAPIアクセス前後に処理を挟み込むことが出来ます。

処理フロー

Vueアプリエラー処理フロー図.jpg

APIアクセスした際に、エラーが返却された場合はinterceptorsへ処理が移り、App.vueへエラーをスローします。スローされた情報をもとにエラーメッセージをmessage-store.jsに設定します。
Message.vueではmessage-store.jsを参照しています。グローバルに管理したいデータはstoreパターンという方法を使用しています。

実装

実際のコードを見てみます。
interceptorsaxiosInterceptor.jsにて実装しました。

axiosInterceptor.js
import axios from "axios";
import router from "./router";

//1
const axiosInstance = axios.create({ baseURL: "http://localhost:8080" });
// ~ 省略 ~

axiosInstance.interceptors.response.use( 
  (response) => { //2
    return response;
  },
  (err) => { //3
    const status = err.response.status;
    switch (status) {
      case 401:
        router.push("/login");
        break;
      case 404:
      case 500:
        router.push(`/error/${status}`);
        break;
    }
    throw err;
  }
);

export default axiosInstance; //4

実装ポイント

  • 基底URLを定義したaxiosインスタンスを作成(コメント1)
  • 正常レスポンスの場合は、そのまま返却しそれぞれのAPIアクセス箇所で処理を継続する。(コメント2)
  • エラーレスポンスの場合は、エラーをApp.vueへスローする。401の場合は未認証の為ログイン画面へ遷移、404、500はそれぞれエラーページへ遷移する。(コメント3)
  • エクスポートする。(コメント4)

APIアクセスの際は、axiosInterceptor.jsでエクスポートしたaxiosを使用すればAPIアクセス処理でエラー処理を毎回記述する必要がなくなります。

<script>
import axiosInstance from "../axiosInterceptor";

export default {
// ~ 省略 ~
  data() {
    return {
      // ~ 省略 ~
    };
  },
  methods: {
    async get() {
            //エラーが発生した場合はaxiosInterceptorでcatchする
      const res = await axiosInstance.get("/user") 
    },
  },
};
</script>

axiosInterceptor.jsでスローされたエラーはApp.vueでハンドリングします。
コンポーネントでエラーが発生した場合、ハンドリングしない限り親に伝播されていきます。
App.vueはルートとなるため、そこでエラーハンドリングを実装しました。

App.vue
<template>
    <v-app>
        <!-- 省略 -->

        <!-- 1 -->
    <Message v-show="!isShowDialog" />

    <!-- 省略 -->
  </v-app>
</template>
<script>
import Header from "./components/Header";
import Message from "./components/Message";
import Constant from "./common/constant";
import messageStore from "./store/message-store";

export default {
   components: {
    Header,
    Message,
  },

  //~ 省略 ~

  //伝播されたエラーをハンドリング
  errorCaptured(err) { //2
    const status = err.response.status;
    if (status !== 404 && status !== 500) {
      const data = err.response.data.fields;
      const errorFields = data.fields ? data.fields : [];
      messageStore.setMessageInf(Constant.ERROR, data.messages, errorFields);
    }
    return false; //3
  },

  //~ 省略 ~

};
</script>

実装ポイント

  • メッセージコンポーネントに渡すデータを定義する。(コメント1)
  • VueのライフサイクルであるerrorCapturedを使用することでコンポーネントでスローされたエラーをハンドリングすることが出来る。伝播されたエラー情報を基に、エラーメッセージを設定する。(コメント2)
  • falseをreturnすることで、伝播がここでストップする。(コメント3)

App.vueからmessage-store.jsにメッセージ情報を設定します。

message-store.js
import Vue from "vue";

const messageStore = Vue.observable({//1
  state: {//2
    type: "",
    messages: [],
    errorFields: [],
    isShowDialog: false,
  },
  setMessageInf(type, messages, errorFields) {//3
    this.state.type = type;
    this.state.messages = messages;
    this.state.errorFields = errorFields;
  },
  clearMessageInf() {
    this.state.type = "";
    this.state.messages = [];
    this.state.errorFields = [];
  },
  onDialogShowFlg() {
    this.state.isShowDialog = true;
  },
  offDialogShowFlg() {
    this.state.isShowDialog = false;
  },
});

export default messageStore;

実装ポイント

  • Vue.observableを使用することで、オブジェクトをリアクティブにすることができる。コンポーネント側では算出プロパティ(computed)で変更を検知します。(コメント1)
  • stateオブジェクトにデータを定義する。(コメント2)
  • 設定用の関数を用意しておく。値の設定、更新はこの関数を使用することでどこでどんな値の変更が行われたかも追いやすくなる。(コメント3)

Message.vueではmessage-store.jsのstateを参照しています。

Message.vue
<template>
  <div>
    <!-- 1 -->
    <v-alert
      class="message"
      v-for="val in messages"
      :key="val"
      border="top"
      :color="type === 'info' ? 'blue lighten-2' : 'red lighten-2'"
      dark
      dense
      dismissible
    >
      {{ val }}
    </v-alert>
  </div>
</template>

<script>
import messageStore from "../store/message-store";//2
export default {
  name: "Message",
  watch: {//3
    $route() {
      messageStore.clearMessageInf();
    },
  },
  computed: {//4
    type() {
      return messageStore.state.type;
    },
    messages() {
      return messageStore.state.messages;
    },
  },
};
</script>

<style scoped>
.message {
  position: relative;
  top: 50px;
}
</style>

実装ポイント

  • Vuetifyのv-alertというものを使用している。メッセージは複数の場合もあるので配列になっている。info、errorで色を出し分けている。(コメント1)
  • message-store.jsをインポートする。(コメント2)
  • watchプロパティでrouterを監視する。画面遷移時にメッセージをクリアする。(コメント3)
  • computedでmessage-store.jsのstateの変更を検知する。(コメント4)

アプリ紹介

最後に、簡単にアプリ紹介をします。
まずトップ画面です。シンプルです。
チャットアプリ_トップ.png

トップから新規登録画面に遷移するとこんな感じです。
チャットアプリ_新規登録.png

エラー時はこんな感じです。
チャットアプリ_新規登録_エラー.png

ログイン画面です。
チャットアプリ_ログイン.png

パスワードは表示、非表示を切り替え可能です。
チャットアプリ_ログイン_パスワード非表示.png

チャットアプリ_ログイン_パスワード表示.png

ログインすると、投稿画面に遷移します。
チャットアプリ_投稿画面.png

「返信する」押下でコメント出来ます。
チャットアプリ_投稿画面_コメント返信.png

コメントはこんな感じで表示されます。
投稿と同じコンポーネントを使い回しています。
チャットアプリ_投稿画面_コメント表示.png

いいね!!も出来ます。
チャットアプリ_投稿画面_いいね.png

マウスオンでいいねしたユーザーが表示されます。
チャットアプリ_投稿画面_いいね2.png

投稿やコメントの編集、削除も出来ます。
チャットアプリ_投稿画面_編集.png

チャットアプリ_投稿画面_編集2.png

サイドメニューを開くと、参加している部屋が表示され、別の部屋に移動できます。
チャットアプリ_投稿画面_サイドメニュー.png

チャットアプリ_投稿画面_サイドメニュー項目押下.png

他にも、アカウント編集や部屋管理の画面がありますが長くなってしまうので割愛します。

まとめ

新しい言語やフレームワークを学ぶというのは、ワクワクもありますが大変なことが多いです。
今回は下記順番で学習してみました。

  1. 基本構文の学習
  2. アプリを作成してみる
  3. 品質を意識してみる

3については、まだまだ改善の余地はあるかと思います。
現場ではまだvueを使用したことがないので、その機会があれば今回の作成アプリの改善点も多く見えるのかなと思いました!

エンジニア経験3年になるので、動けば良いではなく、品質も意識していければと思います。

18
18
0

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
18
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?