2017/07/20

[Vue.js][CSS3]会話風吹き出しでLINE風チャットアプリをチャチャッとつくる

フロントエンドエンジニアという肩書きになってからもうすぐ2年が経とうとしているのだが、未だに思い通りにCSSを操ることができない。圧倒的CSS力の欠如だ。

CSS力を高めるために、難易度がそれほど高くなく、かつよく使われそうな会話風吹き出しを作って練習しようと思った。でも、それだけじゃ物足りないので、Vue.jsも使って動的なものを作りたい。

当記事では、会話風吹き出しをつかってLINE風チャットアプリのつくり方を解説する。
※ ただし、サーバサイド(WebSocketとかデータベース周り)については触れない

UI・コンポーネント設計


作成するLINE風チャットアプリのUIは下図のとおり。

LINE風というだけあって、まんまパ k…参考にさせていただいた。
ポイントは以下の2つ。

  • 誰が発言したのかわかること
    • アイコン・スクリーンネームを表示する
    • 表示位置をわける
  • メッセージ入力フォームは常に表示され、邪魔にならないこと
    • 下部にシンプルな入力フォームをつくる


つづいてコンポーネント設計を行う。
Vue.jsはコンポーネント指向の強いフレームワークで、いくつかのコンポーネント(部品)を組み合わせてつくる。

全部入りコンポーネントをつくることも可能だが、メンテナンス性が低下してしまう。
データの結合度が高くなったり、ViewModelが肥大化したり、再利用ができなかったり、デメリットしかない。

そのため、コンポーネント設計が重要になる。


今回のチャットアプリでは、2つのコンポーネントをつくる。

  • メッセージを表示するバルーン(Balloon) → BalloonをまとめたものがTimeline
  • メッセージを入力するフォーム(ChatForm)


各コンポーネントのプロパティについても書いているが、詳細は実装するときに説明する。



LINE風チャットアプリをつくる


設計もできたので、実際にプログラミングしていく。
本来ならvue-cliでスキャフォールディングして、コンポーネントをvueファイルに分割して、vue-loaderで結合する、みたいな開発方法が一般的なのだが、それほど大きくないアプリケーションなのでそれらのツールは使わない。


ただし、CSSだけはカオスになりやすいのでSCSSを使って、RSCSSにのっとり実装する。
要はjsFiddle上で動かせるような構成にする。

RSCSSについては、以下の記事を参照ください。


Balloonコンポーネントを実装する

会話風吹き出し用のコンポーネントを実装する。
主な処理は、親コンポーネントからデータを受け取り吹き出しで表示する。

// index.js
const Balloon = {
  template: `<div class="conversation-balloon" :class="speaker">
  <div class="avatar">
    <img src="{アイコンのURL}">
    <p class="name">{{ name }}</p>
  </div>
  <p class="message">{{ message }}</p>
</div>`,
  props: {
    name: {
      type: String,
      required: true
    },
    speaker: {
      type: String,
      required: true,
      validator: value => {
        return ['my', 'other'].includes(value);
      }
    },
    message: {
      type: String,
      required: true
    }
  }
};
/* style.sass */

$my-balloon-color: #85FF49;
$other-balloon-color: #FFFFFF;

.conversation-balloon {
  &.my {
    text-align: right;
    > .avatar {
      float: right;
    }
    > .message {
      margin-right: 20px;
      background-color: $my-balloon-color;
      text-align: left;

      &::before {
        right: -20px;
        transform: rotate(-25deg);
        border-left: 18px solid $my-balloon-color;
      }
    }
  }

  &.other {
    text-align: left;
    > .avatar {
      float: left;
    }
    > .message {
      margin-left: 20px;
      background-color: $other-balloon-color;
      
      &::before {
        left: -20px;
        transform: rotate(25deg);
        border-right: 18px solid $other-balloon-color;
      }
    }
  }
  
  >.avatar {
    width: 80px;
    
    // floatは各コンポーネントで定義
    &::after {
      clear: both;
    }
    
    > img {
      display: block;
      margin: 0 auto;
      width: 60px;
      height: 60px;
      border-radius: 50%;
      
      margin-bottom: 5px;
    }
    > .name {
      width: 100%;
      text-align: center;
      color: white;
      font-size: 0.8rem;
      word-wrap: break-word;
    }
  }
  
  > .message {
    display: inline-block;
    max-width: 280px;
    padding: 10px 30px;
    border-radius: 30px;
    font-size: 1.3rem;
    min-height: 30px;
    word-wrap: break-word;
    
    position: relative;
    
    &::before {
      content: '';
      display: block;
      position: absolute;
      top: 5px;
      border: 8px solid transparent;
      // left/right, border-right, tranform は各コンポーネントで定義
    }
  }
}

親コンポーネントからpropsでデータを受け取る。
  • name
    • 文字列、必須
    • 表示するスクリーンネーム
  • speaker
    • 文字列、必須、 my | other であること
    • 吹き出しを左に出すか、右に出すかを決める
  • message
    • 文字列、必須
    • 表示するメッセージ

親コンポーネントからデータを渡す方法については、以下の記事を参照ください。


ChatFormコンポーネントを実装する

次にメッセージの入力フォームを実装する。
主な処理は、送信ボタンをクリックしたときに親コンポーネントにメッセージ内容を渡す。
//index.js
const ChatForm = {
  template: `<div class="chat-form">
  <div class="form-container">
    <input type="text" class="message" v-model="message">
    <button class="submit" @click="submit">送信</button>
  </div>
</div>`,
  props: {
    applyEvent: {
      type: String,
      required: true
    }
  },
  data () {
    return {
      message: ''
    }
  },
  methods: {
    submit () {
      this.$emit(this.applyEvent, this.message)
      this.message = '';
    }
  }
};
/* style.sass */

/* ボタンスタイルのリセット */
button {
  background-color: transparent;
  border: none;
  cursor: pointer;
  outline: none;
  padding: 0;
  appearance: none;
}

.form-container {
  position: relative;
  height: 40px;
  > .message {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 80px;
    width: 100%;
    
    font-size: 1.3rem;
    padding: 0 20px;
  }
  > .submit {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    
    width: 80px;
    text-align: center;
    background-color: #4F83E1;
    color: white;
    font-size: 1.6rem;
  }
}

親コンポーネントから、applyEventをもらっている。これは子コンポーネントから親コンポーネントにデータを渡すときに this.$emit('event-name', data') とするときのevent-nameにあたる。

子コンポーネントに親コンポーネントの情報を持たせたくないのでイベント名を渡しているが、もちろんリテラルで指定してもOK。

親子間のデータのやり取りについては、以下の記事に図解しているので参照ください。



メインコンポーネントを実装する


各種コンポーネントができたところで、それらを組み合わせてメインとなるコンポーネント(div#app)を実装する。
主な処理は2つある。
1つは、ChatFormコンポーネントで入力されたメッセージをうけとりポストする。
もう1つは、メッセージをBalloonコンポーネントに渡して、吹き出しを表示する。

他にもページ下部までスクロールさせたり、botによる返信をしたり、といろいろやっているがさほど重要ではない。


<!-- index.html -->
<div id="app">
  <main class="main-container">
    <div class="chat-timeline">
      <balloon 
        v-for="chat in chatLogs"
        :speaker="chat.speaker"
        :name="chat.name"
        :message="chat.message">
      </balloon>
    </div>
    <chat-form
      @submit-message="submit"
      apply-event="submit-message">
    </chat-form>
  </main>
</div>

// index.js
const app = new Vue({
  el: '#app',
  components: {
    balloon: Balloon,
    chatForm: ChatForm
  },
  data () {
    return {
     chatLogs: [
         { name: 'わたしだよ', speaker: 'my', message: 'hello'.repeat(10) },
         { name: 'bot', speaker: 'other', message: 'hello world' }
      ]
    }
  },
  methods: {
    submit (value) {
      this.chatLogs.push({
        name: 'わたしだよ',
        speaker: 'my',
        message: value
      });
      
      this.botSubmit();
      this.scrollDown();
    },
    botSubmit () {
      setTimeout(() => {
        this.chatLogs.push({
          name: 'bot',
          speaker: 'other',
          message: 'hello world'
        });
        
        this.scrollDown();
      }, 1000);
    },
    scrollDown () {
      const target = this.$el.querySelector('.chat-timeline');
      setTimeout(() => {
       const height = target.scrollHeight - target.offsetHeight;
        target.scrollTop += 10;

        if (height <= target.scrollTop) {
          return;
        } else {
          this.scrollDown();
        }
      }, 0);
    }
  }
});

/* style.sass */

/* base */
body {
  font-size: 62.5%;
  box-sizing: border-box;
}
button {
  background-color: transparent;
  border: none;
  cursor: pointer;
  outline: none;
  padding: 0;
  appearance: none;
}

/* timeline */
$tl-background-color: #88A4D4;

.chat-timeline {
  position: fixed;
  top: 0;
  bottom: 40px;
  left: 0;
  right: 0;
  
  min-width: 480px;
  padding: 30px;
  
  background-color: $tl-background-color;
  overflow-y: auto;

  display: flex;
  flex-direction: column;

  > .conversation-balloon {
    margin-bottom: 30px;
  }
}

本格的なアプリケーションをつくりたいなら、これにVuexという状態管理フレームワークを導入して、メッセージの取得・投稿はActionsで、メッセージの内容はStateで、メッセージの取得はGettersで、といろいろ分割する。

今回つくったチャットアプリ程度でVuexを入れると、逆に複雑になり面倒になってしまうので注意。


完成だ!

CSS力は5くらいあがったと思う。ただ未だに自信を持って書けないので、CSS1,000本ノックとかやりたい。


以上

written by @bc_rikko

0 件のコメント :

コメントを投稿