Socket.IO ile AngularJS Uygulaması Yazma

Brian Ford
Brian Ford

Giriş

AngularJS, hem kullanımı kolay hem de hızlı iki yönlü veri bağlama, yeniden kullanılabilir özel bileşenler oluşturmanıza olanak tanıyan güçlü bir yönerge sistemi ve daha pek çok özellik sunan harika bir JavaScript çerçevesidir. Socket.IO, gerçek zamanlı uygulamaların geliştirilmesini kolaylaştıran tarayıcılar arası bir sarmalayıcı ve web soketleri için polyfill'dir. Bu iki araç birlikte oldukça iyi çalışır.

Daha önce Express ile AngularJS uygulaması yazma hakkında yazı yazdım. Bu sefer ise AngularJS uygulamasına gerçek zamanlı özellikler eklemek için Socket.IO'yu nasıl entegre edeceğiz hakkında yazacağım. Bu eğitimde, anlık mesajlaşma uygulaması yazma konusunda size yol göstereceğim. Bu eğitim, sunucu üzerinde benzer bir node.js paketi kullanan önceki eğitimimden yararlanır. Bu nedenle, Node.js veya Express'e aşina değilseniz önce bu eğitimi incelemenizi öneririm.

Demoyu açma

Her zaman olduğu gibi, bitmiş ürünü GitHub'dan indirebilirsiniz.

Ön koşullar

Socket.IO'yu ayarlamak ve Express ile entegre etmek için biraz şablon kod kullanmanız gerekir. Bu nedenle Angular Socket.IO Seed'i oluşturdum.

Başlamak için GitHub'dan angular-node-seed deposunu kopyalayabilirsiniz:

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

veya zip dosyası olarak indirebilirsiniz.

Tohumu aldıktan sonra npm ile birkaç bağımlı öğe almanız gerekir. Tohumun bulunduğu dizinde bir terminal açın ve aşağıdaki komutu çalıştırın:

npm install

Bu bağımlılıklar yüklendikten sonra iskelet uygulamayı çalıştırabilirsiniz:

node app.js

ve tohumun beklendiği gibi çalıştığından emin olmak için http://localhost:3000 adresindeki tarayıcınızda görün.

Uygulama özelliklerine karar verme

Sohbet uygulaması yazmanın birkaç farklı yolu vardır. Bu nedenle, uygulamamızda yer alacak temel özellikleri açıklayalım. Tüm kullanıcıların ait olacağı yalnızca bir sohbet odası olacaktır. Kullanıcılar adlarını seçebilir ve değiştirebilir ancak adlar benzersiz olmalıdır. Sunucu bu benzersizliği zorunlu kılar ve kullanıcılar adlarını değiştirdiğinde bunu duyurur. İstemci, mesajların ve şu anda sohbet odasında bulunan kullanıcıların listesini göstermelidir.

Basit bir kullanıcı arayüzü

Bu spesifikasyonla, Jade ile gerekli kullanıcı arayüzü öğelerini sağlayan basit bir ön uç oluşturabiliriz. views/index.jade dosyasını açın ve block body dosyasının içine şunu ekleyin:

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')

public/css/app.css dosyasını açın ve sütunlar ile taşmalar sağlamak için CSS'yi ekleyin:

/* 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;
}

Socket.IO ile etkileşime geçme

Socket.IO, window üzerinde bir io değişkeni göstermesine rağmen bu değişkeni AngularJS'nin Bağımlılık Enjeksiyon sistemine yerleştirmek daha iyidir. Bu nedenle, Socket.IO tarafından döndürülen socket nesnesini sarmalayacak bir hizmet yazarak başlayacağız. Bu, denetleyicimizi daha sonra test etmeyi çok daha kolaylaştıracağı için harika. public/js/services.js dosyasını açıp içeriği şu şekilde değiştirin:

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);
          }
        });
      })
    }
  };
});

Her soket geri çağırma işlevini $scope.$apply içine sarmaladığımıza dikkat edin. Bu, AngularJS'ye, kendisine iletilen geri çağırma işlevi çalıştırıldıktan sonra uygulamanın durumunu kontrol etmesi ve bir değişiklik varsa şablonları güncellemesi gerektiğini söyler. Dahili olarak $http aynı şekilde çalışır; bazı XHR döndükten sonra AngularJS'in görünümlerini buna göre güncelleyebilmesi için $scope.$apply çağrılır.

Bu hizmetin Socket.IO API'nin tamamını kapsamadığını unutmayın (bu, okuyucuya bir alıştırma olarak bırakılmıştır ;P ). Ancak bu eğitimde kullanılan yöntemleri kapsar ve daha fazla bilgi edinmek isterseniz sizi doğru yöne yönlendirir. Tam bir sarmalayıcı yazma konusuna tekrar dönebilirim ancak bu eğitim kapsamında değil.

Artık denetleyicimizde, $http ile yaptığımız gibi socket nesnesini isteyebiliriz:

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

Denetleyiciye mesaj gönderme ve alma mantığını ekleyelim. js/public/controllers.js dosyasını açıp içeriğini aşağıdakiyle değiştirin:

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 = '';
  };
}

Bu uygulamada yalnızca bir görünüm olacağından public/js/app.js adresindeki yönlendirmeyi kaldırabilir ve aşağıdaki şekilde basitleştirebiliriz:

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

Sunucuyu Yazma

routes/socket.js adlı kişiyi aç. Kullanıcı adlarının benzersiz olması için sunucunun durumunu korumak üzere bir nesne tanımlamamız gerekir.

// 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
  };
}());

Bu temel olarak bir dizi ad tanımlar ancak bir sohbet sunucusunun alanı için daha anlamlı olan API'ler kullanır. İstemcimizin yaptığı çağrılara yanıt vermek için bunu sunucunun soketine bağlayalım:

// 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);
  });
};

Bu işlemle birlikte başvuru tamamlanmış olur. node app.js komutunu çalıştırarak deneyin. Socket.IO sayesinde uygulama gerçek zamanlı olarak güncellenir.

Sonuç

Bu anlık mesajlaşma uygulamasına daha fazla şey ekleyebilirsiniz. Örneğin, boş mesajlar gönderebilirsiniz. Bunu istemci tarafında önlemek için ng-valid ve sunucu tarafında bir kontrol kullanabilirsiniz. Sunucu, uygulamaya katılan yeni kullanıcılar için mesajların son geçmişini saklayabilir.

Diğer kitaplıkları nasıl bir hizmete sarmalayacağınızı ve Angular'ı bir modelin değiştiği konusunda nasıl bilgilendireceğinizi öğrendikten sonra, diğer kitaplıkları kullanan AngularJS uygulamaları yazmak kolaydır. Bir sonraki yazımda, popüler görselleştirme kitaplığı D3.js ile AngularJS'i kullanmayı ele alacağım.

Referanslar

Angular Socket.IO Tohumu Bitmiş Anlık Mesajlaşma Uygulaması AngularJS Express Socket.IO`