Socket.IO ile AngularJS Uygulaması Yazma

Burak Ford
Brian Ford

Giriş

AngularJS, size hem kullanımı kolay hem de hızlı olan iki yönlü veri bağlama olanağı, yeniden kullanılabilir özel bileşenler oluşturmanıza ve çok daha fazlasını kullanmanıza olanak tanıyan güçlü bir yönerge sistemi sunan mükemmel bir JavaScript çerçevesidir. Socket.IO, gerçek zamanlı uygulama geliştirmeyi kolaylaştıran websockets için tarayıcılar arası sarmalayıcı ve çoklu dolgudur. Ne yazık ki ikisi birlikte gayet iyi çalışıyor!

Daha önce Express ile AngularJS uygulaması yazma hakkında yazmıştım, ancak bu sefer AngularJS uygulamasına gerçek zamanlı özellikler eklemek için Socket.IO'yu nasıl entegre edeceğimi anlatacağım. Bu eğiticide anlık mesajlaşma uygulaması yazma işlemini adım adım anlatacağım. Bu çalışma, (sunucuda benzer bir node.js yığını kullanarak) önceki eğiticimi temel almaktadır. Bu nedenle Node.js veya Express'e aşina değilseniz önce bu web sitesini kontrol etmenizi öneririm.

Demoyu açın

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

Ön koşullar

Socket.IO'yu kurmak ve Express ile entegre etmek için biraz ortak metin var, bu yüzden Angular Socket.IO Seed'i oluşturdum.

Başlamak için GitHub'dan angle-node-seed deposunu klonlayabilirsiniz:

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

veya zip olarak indirin.

Başlangıç noktası edindikten sonra, npm ile birkaç bağımlılığı belirlemeniz gerekir. Çekirdek dizinin bulunduğu bir terminali açın ve şu komutu çalıştırın:

npm install

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

node app.js

ve başlangıç noktasının beklendiği gibi çalıştığından emin olmak için http://localhost:3000 adresindeki tarayıcıda kontrol edin.

Uygulama Özelliklerine Karar Verme

Sohbet uygulaması yazmanın birkaç farklı yolu vardır. Bu nedenle bizde bulunan minimum özellikleri açıklayalım. Tüm kullanıcıların ait olacağı tek bir sohbet odası olacak. Kullanıcılar adlarını seçip 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 bildirir. İstemci, mesajların listesini ve o anda sohbet odasında olan kullanıcıların listesini göstermelidir.

Basit Bir Kullanıcı Arabirimi

Bu spesifikasyon ile, Jade ile gerekli kullanıcı arayüzü öğelerini sağlayan basit bir kullanıcı arabirimi yapabiliriz. views/index.jade öğesini açın ve block body içine 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 öğesini açın ve sütunları ve 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şimde bulunma

Socket.IO, window üzerinde bir io değişkeni gösterse de bunu AngularJS'nin Bağımlılık Yerleştirme sistemine kapsüllemek daha iyidir. Şimdi, Socket.IO tarafından döndürülen socket nesnesini sarmalayacak bir hizmet yazarak başlayacağız. Kumandamızı daha sonra test etmemizi kolaylaştıracağından bu muhteşem bir özellik. public/js/services.js dosyasını açın ve içeriği şununla 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 yuva geri çağırmasını $scope.$apply ile sarmaladığımıza dikkat edin. Bu, AngularJS'ye, uygulamanın durumunu kontrol etmesi ve kendisine iletilen geri çağırmayı çalıştırdıktan sonra bir değişiklik olduysa şablonları güncellemesi gerektiğini bildirir. $http dahili olarak aynı şekilde çalışır; bir miktar XHR geri döndükten sonra, AngularJS'nin görünümlerini uygun şekilde güncelleyebilmesi için $scope.$apply yöntemini çağırır.

Bu hizmetin tüm Socket.IO API'sını sarmadığını (okuyucu için bir alıştırma olarak ;P ) unutmayın. Ancak, bu eğiticide kullanılan yöntemleri ele alır ve genişletmek istiyorsanız sizi doğru yöne yönlendirmelidir. Sarmalayıcının tamamını tekrar yazabilirsiniz, ancak bunlar bu eğiticinin kapsamı dışındadır.

Artık denetleyicimizde $http ile istediğimiz gibi socket nesnesini isteyebiliriz:

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

Kumandanın içerisine ileti gönderme ve alma mantığını ekleyelim. js/public/controllers.js dosyasını açın ve içeriği şununla 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 uygulama yalnızca bir görünüm içerecek, dolayısıyla yönlendirmeyi public/js/app.js öğesinden kaldırabilir ve şu şekilde basitleştirebiliriz:

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

Sunucuya Yazma

routes/socket.js adlı kişiyi aç. Kullanıcı adlarının benzersiz olması için sunucunun durumunu korumak amacıyla 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 grup adı tanımlar ancak sohbet sunucusunun alanı için daha anlamlı olan API'ları 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);
  });
};

Böylece başvuru da tamamlanmış olur. node app.js uygulamasını çalıştırarak deneyin. Uygulama, Socket.IO sayesinde gerçek zamanlı olarak güncellenir.

Sonuç

Bu anlık mesajlaşma uygulamasına ekleyebileceğiniz daha pek çok şey var. Örneğin, boş iletiler gönderebilirsiniz. İstemci tarafında bu durumun önüne geçmek ve sunucu üzerinde bir denetim gerçekleştirmek için ng-valid kullanabilirsiniz. Belki de sunucu, uygulamaya katılan yeni kullanıcıların yararına iletilerin yakın zamandaki bir geçmişini saklayabilir.

Diğer kitaplıklardan yararlanan AngularJS uygulamaları yazmak için söz konusu uygulamaları bir hizmette nasıl sarmalayacağınızı anlayıp Angular'a bir modelin değiştiğini bildirmeniz yeterlidir. Bir sonraki derste, popüler görselleştirme kitaplığı olan D3.js ile AngularJS'yi kullanma konusunu ele almayı planlıyorum.

Referanslar

Angular Socket.IO Seed Tamamlanmış Anlık Mesajlaşma Uygulaması AngularJS Express Socket.IO"