كتابة تطبيق AngularJS باستخدام Socket.IO

مقدمة

AngularJS هو إطار عمل رائع لرموز JavaScript يمنحك إمكانية ربط البيانات ثنائية الاتجاه سهلة الاستخدام وسريعة، وهو نظام توجيهي قوي يتيح لك استخدام إنشاء مكونات مخصّصة قابلة لإعادة الاستخدام، وغير ذلك الكثير. Socket.IO هو برنامج تضمين عبر المتصفحات وبرنامج polyfill لـ websockets، مما يجعل تطوير التطبيقات في الوقت الفعلي أمرًا سهلاً. العرضة، يعمل الاثنان بشكل جيد معًا!

لقد كتبت من قبل عن كتابة تطبيق AngularJS باستخدام Express، ولكن هذه المرة سأكتب عن كيفية دمج Socket.IO لإضافة ميزات في الوقت الفعلي إلى تطبيق AngularJS. سأتطرق في هذا البرنامج التعليمي إلى طريقة كتابة تطبيق للمراسلة الفورية. ويعتمد ذلك على البرنامج التعليمي السابق (باستخدام حزمة مقتطفات Node.js مماثلة على الخادم)، لذا أوصيك بالتحقّق من ذلك أولاً إذا لم تكن معتادًا على استخدام Node.js أو Express.

فتح العرض التوضيحي

كما هو الحال دائمًا، يمكنك الحصول على المنتج النهائي على جيت هب.

المتطلبات الأساسية

تم إنشاء Angular Socket.IO Seed بطريقة نموذجية لإعداد Socket.IO ودمجه مع Express.

للبدء، يمكنك إما استنساخ مستودع بذور العُقد الزاويّة من جيت هب:

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

أو تنزيله كملف zip.

بمجرد حصولك على البذور، تحتاج إلى الحصول على بعض التبعيات باستخدام npm. افتح وحدة طرفية للدليل الذي يتضمّن القيمة الأساسية وشغِّل:

npm install

مع تثبيت هذه التبعيات، يمكنك تشغيل التطبيق الهيكلي:

node app.js

يمكنك الاطّلاع عليه في متصفّحك على http://localhost:3000 للتأكّد من أنّ المحتوى الأساسي يعمل على النحو المتوقّع.

اتخاذ قرار بشأن ميزات التطبيق

هناك أكثر من بضع طرق مختلفة لكتابة تطبيق دردشة، لذلك لنصف الحد الأدنى من الميزات التي ستحتوي على ميزاتنا. ستكون هناك غرفة محادثة واحدة فقط سينتمي إليها جميع المستخدمين. يمكن للمستخدمين اختيار أسمائهم وتغييرها، ولكن يجب أن تكون الأسماء فريدة. سيفرض الخادم هذا التفرّد وسيعلن عندما يغير المستخدمون أسمائهم. يجب أن يعرض البرنامج قائمة بالرسائل، وقائمة بالمستخدمين الموجودين حاليًا في غرفة المحادثة.

واجهة أمامية بسيطة

باستخدام هذه المواصفات، يمكننا إنشاء واجهة أمامية بسيطة باستخدام Jade توفر عناصر واجهة المستخدم الضرورية. فتح views/index.jade وإضافة هذا داخل 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')

افتح public/css/app.css وأضِف خدمة CSS لتوفير الأعمدة والتجاوزات:

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

مع أنّ Socket.IO يعرض المتغيّر io في window، يُفضَّل أن يتم تضمينه في نظام IngularJS Injection Injection. إذًا، سنبدأ بكتابة خدمة لإحاطة الكائن socket الذي يعرضه Socket.IO. وهذا رائع، لأنه سيسهل عليك اختبار وحدة التحكم لاحقًا. يُرجى فتح "public/js/services.js" واستبدال المحتوى بما يلي:

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

يتم التفاف كل استدعاء لمقبس في $scope.$apply. وهذا يخبر AngularJS أنها بحاجة إلى التحقق من حالة التطبيق وتحديث النماذج إذا كان هناك تغيير بعد تشغيل معاودة الاتصال التي تم تمريرها إليه. وتعمل $http بالطريقة نفسها داخليًا. وبعد إرجاع بعض XHR، تستدعي $scope.$apply، حتى يتمكن AngularJS من تعديل المشاهدات وفقًا لذلك.

لاحظ أن هذه الخدمة لا تتضمن واجهة برمجة تطبيقات Socket.IO بالكامل (يُترك هذا كتمرين للقارئ ;P ). ومع ذلك، فإنها تغطي الأساليب المستخدمة في هذا البرنامج التعليمي، ويجب أن توجهك في الاتجاه الصحيح إذا كنت ترغب في توسيعه. قد أعيد النظر في كتابة برنامج تضمين كامل، لكن هذا خارج نطاق هذا البرنامج التعليمي.

الآن، ضمن وحدة التحكّم الخاصة بنا، يمكننا طلب الوصول إلى الكائن socket، تمامًا كما نفعل مع $http:

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

داخل وحدة التحكم، لنقم بإضافة منطق لإرسال الرسائل واستلامها. افتح "js/public/controllers.js" واستبدل المحتوى بما يلي:

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

سيعرِض هذا التطبيق عرضًا واحدًا فقط، لذا يمكننا إزالة التوجيه من public/js/app.js وتبسيطه إلى:

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

كتابة بيانات الخادم

فتح "routes/socket.js" ونحتاج إلى تعريف كائن للحفاظ على حالة الخادم، بحيث تكون أسماء المستخدمين فريدة.

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

ويحدد هذا بشكل أساسي مجموعة من الأسماء، ولكن باستخدام واجهات برمجة التطبيقات الأكثر منطقيةً لنطاق خادم الدردشة. لنربط هذا بمقبس الخادم للاستجابة إلى المكالمات التي يجريها العميل:

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

وبهذا، ينبغي أن يكون الطلب كاملاً. يمكنك تجربته من خلال تشغيل node app.js. من المفترض أن يتم تحديث التطبيق في الوقت الفعلي بفضل Socket.IO.

الخلاصة

هناك المزيد من الميزات التي يمكنك إضافتها إلى تطبيق المراسلة الفورية هذا. على سبيل المثال، يمكنك إرسال رسائل فارغة. ويمكنك استخدام ng-valid لمنع هذا من جهة العميل، والتحقق من الخادم. ربما يمكن للخادم الاحتفاظ بسجلّ حديث من الرسائل لصالح المستخدمين الجدد الذين ينضمون إلى التطبيق.

من السهل كتابة تطبيقات AngularJS التي تستفيد من المكتبات الأخرى بمجرد فهمك لكيفية ضمها إلى إحدى الخدمات وإرسال إشعار إلى Angular بأن أحد النماذج قد تغيّر. بعد ذلك، أخطط للتطرق إلى استخدام AngularJS مع D3.js، وهي مكتبة العروض المرئية الرائجة.

المراجع

Angular Socket.IO Seed إنهاء تطبيق المراسلة الفورية AngularJS Express Socket.IO`