Menulis Aplikasi AngularJS dengan Socket.IO

Pengantar

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

Saya telah menulis sebelumnya 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 memandu cara menulis aplikasi pesan instan. Aplikasi ini dibangun berdasarkan tutorial saya sebelumnya (menggunakan tumpukan node.js serupa di server), jadi sebaiknya Anda mempelajarinya terlebih dahulu jika tidak memahami Node.js atau Express.

Buka Demo

Seperti biasa, Anda bisa mendapatkan produk jadinya di GitHub.

Prasyarat

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

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 zip.

Setelah mendapatkan seed, Anda perlu mengambil beberapa dependensi dengan npm. Buka terminal ke direktori yang berisi seed tersebut, lalu jalankan:

npm install

Dengan menginstal dependensi ini, Anda dapat menjalankan aplikasi kerangka:

node app.js

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

Menentukan Fitur Aplikasi

Ada lebih dari beberapa cara untuk menulis aplikasi {i>chat<i}, jadi mari kita jelaskan fitur minimal yang akan kita miliki. Hanya akan ada satu ruang chat yang mencakup semua pengguna. Pengguna dapat memilih dan mengubah namanya, tetapi nama tersebut harus unik. Server akan menerapkan keunikan ini dan mengumumkan saat pengguna mengubah nama mereka. Klien harus mengekspos daftar pesan, dan daftar pengguna yang saat ini ada di ruang chat.

Front End yang 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 memberikan kolom dan tambahan:

/* 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 pada window, sebaiknya enkapsulasinya dalam sistem Injeksi Dependensi AngularJS. Jadi, kita akan mulai dengan menulis layanan untuk menggabungkan objek socket yang ditampilkan oleh Socket.IO. Ini luar biasa karena akan mempermudah pengujian pengontrol kita nanti. Buka public/js/services.js dan ganti konten 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 di $scope.$apply. Kode ini memberi tahu AngularJS bahwa kode perlu memeriksa status aplikasi dan memperbarui template jika ada perubahan setelah menjalankan callback yang diteruskan ke aplikasi. Secara internal, $http berfungsi dengan cara yang sama; setelah beberapa XHR ditampilkan, kode ini akan memanggil $scope.$apply, sehingga AngularJS dapat memperbarui tampilannya.

Perlu diperhatikan bahwa layanan ini tidak menggabungkan seluruh Socket.IO API (yang tersisa sebagai latihan untuk pembaca ;P ). Namun, layanan ini mencakup metode yang digunakan dalam tutorial ini, dan akan mengarahkan Anda ke arah yang benar jika ingin memperluasnya. Saya mungkin meninjau kembali menulis wrapper lengkap, tetapi itu 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 tambahkan logika untuk mengirim dan menerima pesan. Buka js/public/controllers.js dan ganti konten 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 &lt; $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 &lt; $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 perutean 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 mendefinisikan objek untuk mempertahankan status server, sehingga nama pengguna 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 mendefinisikan sekumpulan nama, namun dengan API yang lebih masuk akal untuk domain server chat. Mari kita hubungkan ini 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 begitu, pengajuan permohonan Anda sudah selesai. Cobalah dengan menjalankan node app.js. Aplikasi akan diperbarui secara real-time, berkat Socket.IO.

Kesimpulan

Ada banyak hal 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 riwayat pesan terkini untuk kepentingan pengguna baru yang bergabung dengan aplikasi.

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

Referensi

Angular Socket.IO Seed Aplikasi Pesan Instan Selesai AngularJS Express Socket.IO`