Menulis Aplikasi AngularJS dengan Socket.IO

Pengantar

AngularJS adalah framework JavaScript yang luar biasa yang memberi Anda binding data dua arah yang mudah digunakan dan cepat, sistem perintah yang canggih yang memungkinkan Anda membuat komponen kustom yang dapat digunakan kembali, dan banyak lagi. Socket.IO adalah wrapper lintas browser dan polyfill untuk websocket yang memudahkan pengembangan aplikasi real-time. Kebetulan, keduanya bekerja sama dengan cukup baik.

Sebelumnya, saya telah menulis tentang menulis aplikasi AngularJS dengan Express, tetapi kali ini saya akan menulis tentang cara mengintegrasikan Socket.IO untuk menambahkan fitur real-time ke aplikasi AngularJS. Dalam tutorial ini, saya akan menjelaskan cara menulis aplikasi pesan instan. Tutorial ini dibuat berdasarkan tutorial saya sebelumnya (menggunakan stack node.js serupa di server), jadi sebaiknya lihat tutorial tersebut terlebih dahulu jika Anda tidak terbiasa dengan Node.js atau Express.

Membuka Demo

Seperti biasa, Anda dapat mendapatkan produk jadi di GitHub.

Prasyarat

Ada sedikit boilerplate untuk menyiapkan dan mengintegrasikan Socket.IO dengan Express, jadi saya membuat Seed Socket.IO Angular.

Untuk memulai, Anda dapat meng-clone repo angular-node-seed dari GitHub:

git clone git://github.com/btford/angular-socket-io-seed my-project

atau download sebagai file zip.

Setelah memiliki seed, Anda perlu mengambil beberapa dependensi dengan npm. Buka terminal ke direktori dengan seed, lalu jalankan:

npm install

Setelah menginstal dependensi ini, Anda dapat menjalankan aplikasi kerangka:

node app.js

dan lihat di browser Anda di http://localhost:3000 untuk memastikan bahwa seed berfungsi seperti yang diharapkan.

Memutuskan Fitur Aplikasi

Ada lebih dari beberapa cara untuk menulis aplikasi chat, jadi mari kita jelaskan fitur minimal yang akan dimiliki aplikasi kita. Hanya akan ada satu ruang chat yang menjadi milik semua pengguna. Pengguna dapat memilih dan mengubah namanya, tetapi nama tersebut harus unik. Server akan menerapkan keunikan ini dan mengumumkan saat pengguna mengubah namanya. Klien harus menampilkan daftar pesan, dan daftar pengguna yang saat ini berada di ruang chat.

Front End Sederhana

Dengan spesifikasi ini, kita dapat membuat frontend sederhana dengan Jade yang menyediakan elemen UI yang diperlukan. Buka views/index.jade dan tambahkan ini di dalam block body:

div(ng-controller='AppCtrl')
.col
  h3 Messages
  .overflowable
    p(ng-repeat='message in messages') : 

.col
  h3 Users
  .overflowable
    p(ng-repeat='user in users') 

.clr
  form(ng-submit='sendMessage()')
    | Message: 
    input(size='60', ng-model='message')
    input(type='submit', value='Send')

.clr
  h3 Change your name
  p Your current user name is 
  form(ng-submit='changeName()')
    input(ng-model='newName')
    input(type='submit', value='Change Name')

Buka public/css/app.css dan tambahkan CSS untuk menyediakan kolom dan overflow:

/* app css stylesheet */

.overflowable {
  height: 240px;
  overflow-y: auto;
  border: 1px solid #000;
}

.overflowable p {
  margin: 0;
}

/* poor man's grid system */
.col {
  float: left;
  width: 350px;
}

.clr {
  clear: both;
}

Berinteraksi dengan Socket.IO

Meskipun Socket.IO mengekspos variabel io di window, sebaiknya enkapsulasi dalam sistem Injeksi Dependensi AngularJS. Jadi, kita akan mulai dengan menulis layanan untuk menggabungkan objek socket yang ditampilkan oleh Socket.IO. Ini sangat bagus, karena akan mempermudah pengujian pengontrol kita nanti. Buka public/js/services.js dan ganti kontennya dengan:

app.factory('socket', function ($rootScope) {
  var socket = io.connect();
  return {
    on: function (eventName, callback) {
      socket.on(eventName, function () {  
        var args = arguments;
        $rootScope.$apply(function () {
          callback.apply(socket, args);
        });
      });
    },
    emit: function (eventName, data, callback) {
      socket.emit(eventName, data, function () {
        var args = arguments;
        $rootScope.$apply(function () {
          if (callback) {
            callback.apply(socket, args);
          }
        });
      })
    }
  };
});

Perhatikan bahwa kita menggabungkan setiap callback soket dalam $scope.$apply. Hal ini memberi tahu AngularJS bahwa AngularJS perlu memeriksa status aplikasi dan memperbarui template jika ada perubahan setelah menjalankan callback yang diteruskan ke AngularJS. Secara internal, $http berfungsi dengan cara yang sama; setelah beberapa XHR ditampilkan, $http akan memanggil $scope.$apply, sehingga AngularJS dapat memperbarui tampilannya sebagaimana mestinya.

Perhatikan bahwa layanan ini tidak menggabungkan seluruh Socket.IO API (yang dibiarkan sebagai latihan bagi pembaca ;P ). Namun, layanan ini mencakup metode yang digunakan dalam tutorial ini, dan akan mengarahkan Anda ke arah yang benar jika Anda ingin memperluasnya. Saya mungkin akan meninjau kembali penulisan wrapper lengkap, tetapi itu berada di luar cakupan tutorial ini.

Sekarang, dalam pengontrol, kita dapat meminta objek socket, seperti yang kita lakukan dengan $http:

function AppCtrl($scope, socket) {
  /* Controller logic */
}

Di dalam pengontrol, mari kita tambahkan logika untuk mengirim dan menerima pesan. Buka js/public/controllers.js dan ganti kontennya dengan kode berikut:

function AppCtrl($scope, socket) {

  // Socket listeners
  // ================

  socket.on('init', function (data) {
    $scope.name = data.name;
    $scope.users = data.users;
  });

  socket.on('send:message', function (message) {
    $scope.messages.push(message);
  });

  socket.on('change:name', function (data) {
    changeName(data.oldName, data.newName);
  });

  socket.on('user:join', function (data) {
    $scope.messages.push({
      user: 'chatroom',
      text: 'User ' + data.name + ' has joined.'
    });
    $scope.users.push(data.name);
  });

  // add a message to the conversation when a user disconnects or leaves the room
  socket.on('user:left', function (data) {
    $scope.messages.push({
      user: 'chatroom',
      text: 'User ' + data.name + ' has left.'
    });
    var i, user;
    for (i = 0; i < $scope.users.length; i++) {
      user = $scope.users[i];
      if (user === data.name) {
        $scope.users.splice(i, 1);
        break;
      }
    }
  });

  // Private helpers
  // ===============

  var changeName = function (oldName, newName) {
    // rename user in list of users
    var i;
    for (i = 0; i < $scope.users.length; i++) {
      if ($scope.users[i] === oldName) {
        $scope.users[i] = newName;
      }
    }

    $scope.messages.push({
      user: 'chatroom',
      text: 'User ' + oldName + ' is now known as ' + newName + '.'
    });
  }

  // Methods published to the scope
  // ==============================

  $scope.changeName = function () {
    socket.emit('change:name', {
      name: $scope.newName
    }, function (result) {
      if (!result) {
        alert('There was an error changing your name');
      } else {

        changeName($scope.name, $scope.newName);

        $scope.name = $scope.newName;
        $scope.newName = '';
      }
    });
  };

  $scope.sendMessage = function () {
    socket.emit('send:message', {
      message: $scope.message
    });

    // add the message to our model locally
    $scope.messages.push({
      user: $scope.name,
      text: $scope.message
    });

    // clear message box
    $scope.message = '';
  };
}

Aplikasi ini hanya akan menampilkan satu tampilan, sehingga kita dapat menghapus pemilihan rute dari public/js/app.js dan menyederhanakannya menjadi:

// Declare app level module which depends on filters, and services
var app = angular.module('myApp', ['myApp.filters', 'myApp.directives']);

Menulis Server

Buka routes/socket.js. Kita perlu menentukan objek untuk mempertahankan status server, sehingga nama pengguna bersifat unik.

// Keep track of which names are used so that there are no duplicates
var userNames = (function () {
  var names = {};

  var claim = function (name) {
    if (!name || userNames[name]) {
      return false;
    } else {
      userNames[name] = true;
      return true;
    }
  };

  // find the lowest unused "guest" name and claim it
  var getGuestName = function () {
    var name,
      nextUserId = 1;

    do {
      name = 'Guest ' + nextUserId;
      nextUserId += 1;
    } while (!claim(name));

    return name;
  };

  // serialize claimed names as an array
  var get = function () {
    var res = [];
    for (user in userNames) {
      res.push(user);
    }

    return res;
  };

  var free = function (name) {
    if (userNames[name]) {
      delete userNames[name];
    }
  };

  return {
    claim: claim,
    free: free,
    get: get,
    getGuestName: getGuestName
  };
}());

Ini pada dasarnya menentukan sekumpulan nama, tetapi dengan API yang lebih sesuai untuk domain server chat. Mari kita hubungkan ke soket server untuk merespons panggilan yang dilakukan klien kita:

// export function for listening to the socket
module.exports = function (socket) {
  var name = userNames.getGuestName();

  // send the new user their name and a list of users
  socket.emit('init', {
    name: name,
    users: userNames.get()
  });

  // notify other clients that a new user has joined
  socket.broadcast.emit('user:join', {
    name: name
  });

  // broadcast a user's message to other users
  socket.on('send:message', function (data) {
    socket.broadcast.emit('send:message', {
      user: name,
      text: data.message
    });
  });

  // validate a user's name change, and broadcast it on success
  socket.on('change:name', function (data, fn) {
    if (userNames.claim(data.name)) {
      var oldName = name;
      userNames.free(oldName);

      name = data.name;

      socket.broadcast.emit('change:name', {
        oldName: oldName,
        newName: name
      });

      fn(true);
    } else {
      fn(false);
    }
  });

  // clean up when a user leaves, and broadcast it to other users
  socket.on('disconnect', function () {
    socket.broadcast.emit('user:left', {
      name: name
    });
    userNames.free(name);
  });
};

Dengan demikian, permohonan akan selesai. Coba dengan menjalankan node app.js. Aplikasi akan diperbarui secara real-time, berkat Socket.IO.

Kesimpulan

Ada banyak hal lain yang dapat Anda tambahkan ke aplikasi pesan instan ini. Misalnya, Anda dapat mengirim pesan kosong. Anda dapat menggunakan ng-valid untuk mencegah hal ini di sisi klien, dan pemeriksaan di server. Mungkin server dapat menyimpan histori pesan terbaru untuk kepentingan pengguna baru yang bergabung ke aplikasi.

Menulis aplikasi AngularJS yang menggunakan library lain menjadi mudah setelah Anda memahami cara menggabungkannya dalam layanan dan memberi tahu Angular bahwa model telah berubah. Selanjutnya, saya berencana membahas penggunaan AngularJS dengan D3.js, library visualisasi yang populer.

Referensi

Angular Socket.IO Seed Finished Instant Messaging App AngularJS Express Socket.IO`