Membuat Aplikasi Web Realtime dengan Django, RabbitMQ dan Vue.js Bagian 4: Menghubungkan Vue dengan Backend

Bagus Aji Santoso 1 Februari 2018

Membuat Aplikasi Web Realtime dengan Django, RabbitMQ dan Vue.js Bagian 4: Menghubungkan Vue dengan Backend

Artikel ini merupakan terjemahan seri tutorial Real time Chat application built with Vue, Django, RabbitMQ and uWSGI WebSockets bagian keempat yang ditulis oleh Osaetin Daniel.

Daftar isi:

Di bagian 3, kita melihat bagaimana cara membuat API dengan django rest api. Dibagian ini kita akan membangun UI aplikasi Chat dan menghubungkannya dengan API yang baru kita bangun. Diakhir bagian ini kita akan memiliki aplikasi chat yang lengkap sehingga bisa kita bagikan ke teman-teman dan ber-chat-ing ria.

UI/UX Layar Chat

Sebelum melanjutkan, mari kita bahas sebentar UI/UX layar aplikasi Chat yang akan dibuat.

Realtime Django 4.1

Pertama, user harus menekan tombol Start Chatting, lalu di backend, kita membuat sesi chat baru dengan user tersebut sebagai owner (pemilik), selanjutnya, kita membawa user (mengganti URL dan antarmuka chat) ke antarmuka baru dimana user bisa saling berbagi pesan dan mengundang user lain.

Garis biru yang ada di layar Start Chat dan Join Chat menunjukkan bahwa mereka ditangani oleh satu component Vue. Bagian Join Chat akan membuka URL sesi chat yang valid lalu menampilkan jendela chat dengan perintah-perintah yang sebelumnya sudah ada.

Impelementasi

Penulis sudah membuat antarmuka chat menggunakan component Chat.vue menggunakan bootstrap.

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-6 offset-3">

        <div v-if="sessionStarted" id="chat-container" class="card">
          <div class="card-header text-white text-center font-weight-bold subtle-blue-gradient">
            Share the page URL to invite new friends
          </div>

          <div class="card-body">
            <div class="container chat-body">
              <div class="row chat-section">
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/f16000/fff&text=D" />
                </div>
                <div class="col-sm-7">
                  <span class="card-text speech-bubble speech-bubble-peer">Hello!</span>
                </div>
              </div>
              <div class="row chat-section">
                <div class="col-sm-7 offset-3">
                  <span class="card-text speech-bubble speech-bubble-user float-right text-white subtle-blue-gradient">
                    Whatsup, another chat app?
                  </span>
                </div>
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/333333/fff&text=A" />
                </div>
              </div>
              <div class="row chat-section">
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/f16000/fff&text=D" />
                </div>
                <div class="col-sm-7">
                  <p class="card-text speech-bubble speech-bubble-peer">
                    Yes this is Chatire, it's pretty cool and it's Open source
                    and it was built with Django and Vue JS so we can tweak it to our satisfaction.
                  </p>
                </div>
              </div>
              <div class="row chat-section">
                <div class="col-sm-7 offset-3">
                  <p class="card-text speech-bubble speech-bubble-user float-right text-white subtle-blue-gradient">
                    Okay i'm already hacking around let me see what i can do to this thing.
                  </p>
                </div>
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/333333/fff&text=A" />
                </div>
              </div>
              <div class="row chat-section">
                <div class="col-sm-7 offset-3">
                  <p class="card-text speech-bubble speech-bubble-user float-right text-white subtle-blue-gradient">
                    We should invite james to see this.
                  </p>
                </div>
                <div class="col-sm-2">
                  <img class="rounded-circle" src="http://placehold.it/40/333333/fff&text=A" />
                </div>
              </div>
            </div>
          </div>

          <div class="card-footer text-muted">
            <form>
              <div class="row">
                <div class="col-sm-10">
                  <input type="text" placeholder="Type a message" />
                </div>
                <div class="col-sm-2">
                  <button class="btn btn-primary">Send</button>
                </div>
              </div>
            </form>
          </div>
        </div>

        <div v-else>
          <h3 class="text-center">Welcome !</h3>

          <br />

          <p class="text-center">
            To start chatting with friends click on the button below, it'll start a new chat session
            and then you can invite your friends over to chat!
          </p>

          <br />

          <button @click="startChatSession" class="btn btn-primary btn-lg btn-block">Start Chatting</button>
        </div>

      </div>
    </div>
  </div>
</template>

<script>
const $ = window.jQuery

export default {
  data () {
    return {
      sessionStarted: false
    }
  },

  created () {
    this.username = sessionStorage.getItem('username')
  },

  methods: {
    startChatSession () {
      this.sessionStarted = true
      this.$router.push('/chats/chat_url/')
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1,
h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}

.btn {
  border-radius: 0 !important;
}

.card-footer input[type="text"] {
  background-color: #ffffff;
  color: #444444;
  padding: 7px;
  font-size: 13px;
  border: 2px solid #cccccc;
  width: 100%;
  height: 38px;
}

.card-header a {
  text-decoration: underline;
}

.card-body {
  background-color: #ddd;
}

.chat-body {
  margin-top: -15px;
  margin-bottom: -5px;
  height: 380px;
  overflow-y: auto;
}

.speech-bubble {
  display: inline-block;
  position: relative;
  border-radius: 0.4em;
  padding: 10px;
  background-color: #fff;
  font-size: 14px;
}

.subtle-blue-gradient {
  background: linear-gradient(45deg,#004bff, #007bff);
}

.speech-bubble-user:after {
  content: "";
  position: absolute;
  right: 4px;
  top: 10px;
  width: 0;
  height: 0;
  border: 20px solid transparent;
  border-left-color: #007bff;
  border-right: 0;
  border-top: 0;
  margin-top: -10px;
  margin-right: -20px;
}

.speech-bubble-peer:after {
  content: "";
  position: absolute;
  left: 3px;
  top: 10px;
  width: 0;
  height: 0;
  border: 20px solid transparent;
  border-right-color: #ffffff;
  border-top: 0;
  border-left: 0;
  margin-top: -10px;
  margin-left: -20px;
}

.chat-section:first-child {
  margin-top: 10px;
}

.chat-section {
  margin-top: 15px;
}

.send-section {
  margin-bottom: -20px;
  padding-bottom: 10px;
}
</style>

Ingat bahwa @click merupakan alternatif lain untuk v-on:click

Karena tutorial ini tidak membahas desain, maka template di atas masih ala kadarnya. Meski begitu, kita tetap harus memberi perhatian pada chat dummy tersebut karena kita perlu membedakan pesan user dengan pesan yang lain.

Disini kita membuat sebuah properti bernama sessionStarted yang menentukan apakah sesi chat kita aktif atau tidak. Jika ada sebuah sesi chat yang aktif, kita akan merender jendela chat, jika tidak maka kita menampilkan layar Start Chatting.

Di dalam method created kita mengambil username dari sessionStorage lalu menyailnnya sebagai properti component Vue.

Pembaca mungkin bertanya mengapa tidak menggunakan data untuk component ini. Kita tidak memakainya karena properti username tidak reaktif. Kita tidak memerlukan UI untuk memberikan respon yang reaktif untuk mengganti nilainya.

Sampai saat ini, username tidak akan berubah selama user login (akan aneh kita username kita berubah-ubah).

Kita hanya perlu menyimpan properti yang bersifat reaktif di dalam data. Vue tidak akan mengamati secara reaktif atribut yang berada diluar data.

Berikut ini tampilan componen start chatting-nya:

Realtime Django 4.2

Jika tombol "Start Chatting" dipilih kita akan diberi halaman kosong dan berubah URL. Halaman kosong ini muncul karena tidak ada route yang memenuhi url /chats/chat_url. Beruntung router Vue memperbolehkan kita untuk secara mencocokkan parameter URL secara dinamis.

Kembali ke router index.js lalu ubah route Chat menjadi:

{
    path: '/chats/:uri?',
    name: 'Chat',
    component: Chat
},

Tanda tandanya diakhir memberitahu router vue bahwa parameter uri bersifat opsional sehingga ia bisa mendeteksi /chats, /chats/chat_url bahkan /chats/abazaba. Apapun yang muncul setelah /matchs akan cocok.

Kita juga bisa mengambil mengambil isi uri dengan:

this.$route.params yang akan mengembalikan: Object { uri: "chat_url" }. Kita akan membutuhkannya nanti.

Reload halaman tadi maka kita akan melihat layar Chat

Realtime Django 4.2

Memulai Sesi Baru

Untuk memulai sesi baru, kita cukup mengirim rikues POST ke endpoint API yang sudah kita buat di bagian 3.

created () {
  this.username = sessionStorage.getItem('username')

  // Setup headers for all requests
  $.ajaxSetup({
    headers: {
      'Authorization': `Token ${sessionStorage.getItem('authToken')}`
    }
  })
},

methods: {
  startChatSession () {
      $.post('http://localhost:8000/api/chats/', (data) => {
        alert("A new session has been created you'll be redirected automatically")
        this.sessionStarted = true
        this.$router.push(`/chats/${data.uri}/`)
      })

      .fail((response) => {
        alert(response.responseText)
      })
    }
}

Di dalam created kita mengatur header otorisasi rikues Ajax. Tanpa otorisasi tersebut, rikues akan gagal karena kita mencoba mengirim post sebagai user yang tidak terdaftar.

Mengirim Pesan

Lalu bagaimana caranya mengirim pesan?

Caranya dengan mengirimkannya ke endpoint messages. Kita hapus dulu pesan dummy dan menyimpan pesan asli ke dalam data sebagai sebuah Array.

Berikut ini adalah component Chat (tanpa CSS):

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-6 offset-3">

        <div v-if="sessionStarted" id="chat-container" class="card">
          <div class="card-header text-white text-center font-weight-bold subtle-blue-gradient">
            Share the page URL to invite new friends
          </div>

          <div class="card-body">
            <div class="container chat-body">
              <div v-for="message in messages" :key="message.id" class="row chat-section">
                <template v-if="username === message.user.username">
                  <div class="col-sm-7 offset-3">
                    <span class="card-text speech-bubble speech-bubble-user float-right text-white subtle-blue-gradient">
                      {{ message.message }}
                    </span>
                  </div>
                  <div class="col-sm-2">
                    <img class="rounded-circle" :src="`http://placehold.it/40/007bff/fff&text=${message.user.username[0].toUpperCase()}`" />
                  </div>
                </template>
                <template v-else>
                  <div class="col-sm-2">
                    <img class="rounded-circle" :src="`http://placehold.it/40/333333/fff&text=${message.user.username[0].toUpperCase()}`" />
                  </div>
                  <div class="col-sm-7">
                    <span class="card-text speech-bubble speech-bubble-peer">
                      {{ message.message }}
                    </span>
                  </div>
                </template>
              </div>
            </div>
          </div>

          <div class="card-footer text-muted">
            <form>
              <div class="row">
                <div class="col-sm-10">
                  <input type="text" placeholder="Type a message" />
                </div>
                <div class="col-sm-2">
                  <button class="btn btn-primary">Send</button>
                </div>
              </div>
            </form>
          </div>
        </div>

        <div v-else>
          <h3 class="text-center">Welcome !</h3>
          <br />
          <p class="text-center">
            To start chatting with friends click on the button below, it'll start a new chat session
            and then you can invite your friends over to chat!
          </p>
          <br />
          <button @click="startChatSession" class="btn btn-primary btn-lg btn-block">Start Chatting</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>

const $ = window.jQuery

export default {
  data () {
    return {
      sessionStarted: false,
      messages: [
        {"status":"SUCCESS","uri":"040213b14a02451","message":"Hello!","user":{"id":1,"username":"danidee","email":"osaetindaniel@gmail.com","first_name":"","last_name":""}},
        {"status":"SUCCESS","uri":"040213b14a02451","message":"Hey whatsup! i dey","user":{"id":2,"username":"daniel","email":"","first_name":"","last_name":""}}
      ]
    }
  },

  created () {
    this.username = sessionStorage.getItem('username')

    // Setup headers for all requests
    $.ajaxSetup({
      headers: {
        'Authorization': `Token ${sessionStorage.getItem('authToken')}`
      }
    })
  },

  methods: {
    startChatSession () {
      $.post('http://localhost:8000/api/chats/', (data) => {
        alert("A new session has been created you'll be redirected automatically")
        this.sessionStarted = true
        this.$router.push(`/chats/${data.uri}/`)
      })
      .fail((response) => {
        alert(response.responseText)
      })
    }
  }
}
</script>

Layar Chat akan terlihat seperti ini:Realtime Django 4.4

Layar Chat menampilkan pesan-pesan dari array.

Kita menggunakan v-if untuk membandingkan pengirim pesan apakah user yang sedang login. Berdasarkan hasilnya, kita bisa menentukan seperti apa pesan itu harus ditampilkan.

Pesan-pesan yang dikirim oleh user ditampilkan ke sebelah kanan dengan warna latar biru sementara yang dikirim oleh user lain ditampilkan di sebelah kiri dengan warna latar biru.

Dengan ini, kita cukup mengirim pesan baru ke dalam list dan Vue akan mengurus bagaimana menampilkan pesan-pesan tersebut.

<script>

const $ = window.jQuery

export default {
  data () {
    return {
      sessionStarted: false, messages: [], message: ''
    }
  },

  created () {
    ...
  },

  methods: {
    ...

    postMessage (event) {
      const data = {message: this.message}

      $.post(`http://localhost:8000/api/chats/${this.$route.params.uri}/messages/`, data, (data) => {
        this.messages.push(data)
        this.message = '' // clear the message after sending
      })
      .fail((response) => {
        alert(response.responseText)
      })
    }
  }
}
</script>

Kita menambah properti message ke objek data kita. Properti ini akan mengikuti perubahan teks yang ditulis didalam field input.

Mari beritahu Vue tentang template ini:

<form @submit.prevent="postMessage">
  <div class="row">
    <div class="col-sm-10">
      <input v-model="message" type="text" placeholder="Type a message" />
    </div>
    <div class="col-sm-2">
      <button class="btn btn-primary">Send</button>
    </div>
  </div>
</form>

@submit.prevent adalah versi pendek dari v-on:submit.prevent bagian .prevent mencegah form untuk dikirim ke server. Inilah salah satu alasan yang membuat penulis menyukai Vue.js. Ia memiliki method pembantu yang memudahkan.

Kita juga bisa melakukannya dengan event.preventDefault dibagian postMessage tapi cara itu bukan gaya Vue.

Jika semua berjalan dengan benar, kita seharusnya sudah bisa mengirim pesan dan menampilkannya di UI aplikasi.

Bergabung dengan Sesi Baru

Kita akhirnya bisa mengirim pesan tapi aplikasi chat ini masih hampa karena belum ada lawan bicara.. Bagaimana cara mengundang teman untuk ikut bercakap-cakap disana?

Kita juga memiliki masalah lain. Apabila tombol refresh diklik, kita akan dibawa ke layar Start Chatting. Baik pemilik sesi chat maupun teman-temannya yang bergabung tidak bisa melanjutkan sesi tersebut.

Untuk menyelesaikan masalah ini, kita perlu mengirim sebuah rikues PATCH ke /api/chats/ dan jika kita bisa mendapatkan user dari hasil yang dikembalikan dari server artinya mereka sudah berhasil ditambahkan ke sesi chat (atau sudah bergabung sebelumnya). Lalu, kita bisa mengambil riwayat pesan dan menampilkannya ke para member.

<script>
const $ = window.jQuery

export default {
  data () {
    return {
      sessionStarted: false, messages: [], message: ''
    }
  },

  created () {
    this.username = sessionStorage.getItem('username')

    // Setup headers for all requests
    $.ajaxSetup({
      headers: {
        'Authorization': `Token ${sessionStorage.getItem('authToken')}`
      }
    })

    if (this.$route.params.uri) {
      this.joinChatSession()
    }
  },

  methods: {
    startChatSession () {
      ...
    },

    postMessage (event) {
      ...
    },

    joinChatSession () {
      const uri = this.$route.params.uri

      $.ajax({
        url: `http://localhost:8000/api/chats/${uri}/`,
        data: {username: this.username},
        type: 'PATCH',
        success: (data) => {
          const user = data.members.find((member) => member.username === this.username)

          if (user) {
            // The user belongs/has joined the session
            this.sessionStarted = true
            this.fetchChatSessionHistory()
          }
        }
      })
    },

    fetchChatSessionHistory () {
      $.get(`http://127.0.0.1:8000/api/chats/${this.$route.params.uri}/messages/`, (data) => {
        this.messages = data.messages
      })
    }
  }
}
</script>

Sekarang refresh browser dan pembaca seharusnya sudah bisa untuk melanjutkan chat dan melihat riwayat sebelumnya.

Lalu, buka tab baru, login dan masuk ke URL chat tadi. Jika kode yang dimasukkan tadi sudah benar, maka kita akan mendapatkan riwayat pesan di tab baru ini yang artinya user lain bisa bergabung ke sesi chat baru.

REALTIME MESSAGING

Saat ini kita harus melakukan refresh untuk mengambil pesan baru. Idealnya kita ingin proses ini terjadi secara otomatis.

Solusinya sudah ada didepan mata.

  • Kita sudah memiliki method untuk mengambil semua pesan dari server
  • Kita sudah memiliki fungsi setInterval
  • Kita memiliki JavaScript
You got this
created () {
  this.username = sessionStorage.getItem('username')

  // Setup headers for all requests
  $.ajaxSetup({
    headers: {
      'Authorization': `Token ${sessionStorage.getItem('authToken')}`
    }
  })

  if (this.$route.params.uri) {
    this.joinChatSession()
  }

  setInterval(this.fetchChatSessionHistory, 3000)
},

Yang kita lakukan sangat biasa, kita hanya perlu menambah satu baris ini ke dalam created.

setInterval(this.fetchChatSessionHistory, 3000)

yang akan mengambil riwayat pesan setiap 3 detik sehingga memberi ilusi Realtime messaging pada user.

Yang baru kita lakukan tadi dinamakan polling. Untuk aplikasi, teknik ini tidak masalah, tapi jika aplikasi kita besar, polling bisa menjadi tidak efisien.

Ayo kita hitung-hitungan:

Untuk dua user di sebuah sesi (anggap mereka login dalam waktu yang bersamaan). Dalam 3 detik, mereka akan membuat 2 rikues. Dalam satu menit mereka akan membuat 40 rikues. Dalam satu jam itu artinya 2400 rikues. Hanya untuk 2 user saja!. Untuk 100 user yang aktif selama satu jam kita akan mendapat 240.000 rikues!

Server yang bagus tentu sanggup menangani 240k rikues per jam dengan mudah. Tapi, masalah utama disini adalah polling yang tidak perlu sehingga menambah beban kerja server yang seharusnya tidak perlu dilakukan secara terus menerus. (Ingat bahwa setiap rikues akan memanggil perintah SELECT di database).

Dalam jangka panjang, teknik ini tentu akan menyusahkan server dan yang paling parah, saat user tidak melakukan apa-apa browser tetap terus mengirim rikues bak ada pesan baru maupun tidak. Kita bisa mencari tahu kapan user idle dengan memeriksa waktu terakhir mereka mengetik sesuai dan memanggil clearInterval untuk menghentikan polling ke url, akan tetapi kita masih mengirimkan rikues-rikues yang tidak perlu karena kita tidak bisa memprediksi waktu yang tepat kapan user idle. Mereka berhenti mengetik bisa saja karena menunggu user lain membalas bukan berarti mereka tidak ingin menerima pesan baru.

Selain itu, setiap rikues juga menghabiskan bandwidth karena didalamnya ada headers, cookies juga informasi otentikasi yang kita tidak perlukan, kita hanya ingin sebuah pesan.

Seharusnya ada cara yang lebih efisien dibandingkan solusi yang sudah diterapkan.

Inilah masalah yang bisa ditangani oleh WebSockets dengan membuka koneksi dua arah antara server dengan klien yang artinya klien tidak perlu meminta server untuk informasi baru. Saat informasi baru tersedia, server akan langsung mengirimkannya ke server.

Lalu, jika klien perlu mengirim informasi ke server, ia bisa mengirimnya di koneksi yang sama.

That’s the exact problem WebSockets solve by opening a persistent bi-directional connection between the server and the client which means the client never needs to ask the server for new information. When it’s available, the server simply pushes it to the client.

WebSocket lebih efisien dibanding polling dan dibagian berikutnya kita akan belajar bagaimana menerapkannya dengan aplikasi kita menggunakan uWSGI tanpa mengubah banyak kode yang sudah kita tulis.