نوشتن برنامه AngularJS با Socket.IO

معرفی

AngularJS یک چارچوب جاوا اسکریپت فوق‌العاده است که به شما اتصال دو طرفه داده را می‌دهد که استفاده آسان و سریع است، یک سیستم دستورالعمل قدرتمند که به شما امکان می‌دهد از ایجاد اجزای سفارشی قابل استفاده مجدد و بسیاری موارد دیگر استفاده کنید. Socket.IO یک بسته بندی بین مرورگر و polyfill برای وب سوکت ها است که توسعه برنامه های بلادرنگ را آسان می کند. اتفاقاً این دو با هم خیلی خوب کار می کنند!

قبلاً در مورد نوشتن یک برنامه AngularJS با Express نوشته‌ام، اما این بار در مورد نحوه ادغام Socket.IO برای افزودن ویژگی‌های بلادرنگ به یک برنامه AngularJS خواهم نوشت. در این آموزش، من قصد دارم در مورد نوشتن یک برنامه پیام‌رسانی فوری قدم بگذارم. این بر اساس آموزش قبلی من (استفاده از پشته node.js مشابه در سرور) است، بنابراین توصیه می کنم اگر با Node.js یا Express آشنایی ندارید، ابتدا آن را بررسی کنید.

دمو را باز کنید

مثل همیشه، می توانید محصول نهایی را در Github دریافت کنید .

پیش نیازها

راه اندازی و ادغام Socket.IO با Express کمی سخت است، بنابراین من Angular Socket.IO Seed را ایجاد کردم.

برای شروع، می توانید مخزن angular-node-seed را از Github شبیه سازی کنید:

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

یا به صورت فشرده دانلود کنید .

هنگامی که دانه را به دست آوردید، باید چند وابستگی را با npm بگیرید. یک ترمینال را به دایرکتوری با seed باز کنید و اجرا کنید:

npm install

با نصب این وابستگی ها، می توانید برنامه اسکلت را اجرا کنید:

node app.js

و آن را در مرورگر خود در http://localhost:3000 ببینید تا مطمئن شوید که seed همانطور که انتظار می رود کار می کند.

تصمیم گیری در مورد ویژگی های برنامه

بیش از چند روش مختلف برای نوشتن یک برنامه چت وجود دارد، بنابراین بیایید حداقل ویژگی هایی را که برنامه ما خواهد داشت را شرح دهیم. تنها یک اتاق گفتگو وجود خواهد داشت که همه کاربران به آن تعلق خواهند داشت. کاربران می توانند نام خود را انتخاب و تغییر دهند، اما نام ها باید منحصر به فرد باشند. سرور این منحصر به فرد بودن را اعمال می کند و زمانی که کاربران نام خود را تغییر می دهند، اعلام می کند. مشتری باید لیستی از پیام ها و لیستی از کاربرانی که در حال حاضر در اتاق چت هستند را در معرض دید قرار دهد.

یک جلوی ساده

با این مشخصات، می‌توانیم یک نمای ظاهری ساده با Jade ایجاد کنیم که عناصر UI لازم را ارائه می‌کند. 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 نمایش می دهد، بهتر است آن را در سیستم تزریق وابستگی AngularJS کپسوله کنید. بنابراین، ما با نوشتن یک سرویس برای بسته بندی شی 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 می‌گوید که باید وضعیت برنامه را بررسی کرده و در صورت ایجاد تغییر پس از اجرای callback ارسال شده به آن، الگوها را به‌روزرسانی کند. در داخل، $http به همین ترتیب کار می کند. پس از بازگرداندن مقداری XHR، $scope.$apply فراخوانی می‌کند تا AngularJS بتواند نماهای خود را مطابق با آن به‌روزرسانی کند.

توجه داشته باشید که این سرویس کل API Socket.IO را در بر نمی گیرد (که به عنوان تمرینی برای خواننده باقی می ماند ;P ). با این حال، روش‌های مورد استفاده در این آموزش را پوشش می‌دهد و اگر می‌خواهید آن را گسترش دهید، باید شما را در مسیر درست راهنمایی کند. ممکن است دوباره به نوشتن یک wrapper کامل مراجعه کنم، اما این فراتر از محدوده این آموزش است.

اکنون، در کنترلر خود، می‌توانیم شی 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
  };
}());

این اساساً مجموعه‌ای از نام‌ها را تعریف می‌کند، اما با APIهایی که برای دامنه یک سرور چت منطقی‌تر هستند. بیایید این را به سوکت سرور متصل کنیم تا به تماس هایی که مشتری ما می گیرد پاسخ دهیم:

// 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 Finished برنامه پیام‌رسانی فوری AngularJS Express Socket.IO `