Viết ứng dụng AngularJS bằng Socket.IO

Giới thiệu

AngularJS là một khung JavaScript tuyệt vời, cung cấp cho bạn tính năng liên kết dữ liệu hai chiều vừa dễ sử dụng vừa nhanh chóng, một hệ thống hướng dẫn mạnh mẽ cho phép bạn sử dụng để tạo các thành phần tuỳ chỉnh có thể sử dụng lại, cùng nhiều tính năng khác. Socket.IO là một trình bao bọc và polyfill đa trình duyệt cho websocket giúp bạn dễ dàng phát triển các ứng dụng theo thời gian thực. Tình cờ là hai công cụ này hoạt động khá tốt với nhau!

Trước đây, tôi đã viết về cách viết ứng dụng AngularJS bằng Express, nhưng lần này tôi sẽ viết về cách tích hợp Socket.IO để thêm các tính năng theo thời gian thực vào ứng dụng AngularJS. Trong hướng dẫn này, tôi sẽ hướng dẫn bạn cách viết một ứng dụng nhắn tin tức thì. Phần này dựa trên hướng dẫn trước của tôi (sử dụng ngăn xếp node.js tương tự trên máy chủ). Vì vậy, bạn nên xem hướng dẫn đó trước nếu chưa quen với Node.js hoặc Express.

Mở bản minh hoạ

Như thường lệ, bạn có thể tải sản phẩm hoàn chỉnh trên GitHub.

Điều kiện tiên quyết

Có một chút mã nguyên mẫu để thiết lập và tích hợp Socket.IO với Express, vì vậy, tôi đã tạo Angular Socket.IO Seed (Mã nguồn gốc Socket.IO của Angular).

Để bắt đầu, bạn có thể sao chép kho lưu trữ angular-node-seed từ GitHub:

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

hoặc tải xuống dưới dạng tệp zip.

Sau khi có hạt giống, bạn cần lấy một số phần phụ thuộc bằng npm. Mở một cửa sổ dòng lệnh đến thư mục có hạt giống rồi chạy:

npm install

Sau khi cài đặt các phần phụ thuộc này, bạn có thể chạy ứng dụng khung:

node app.js

và xem trong trình duyệt tại http://localhost:3000 để đảm bảo rằng hạt giống đang hoạt động như mong đợi.

Quyết định về tính năng của ứng dụng

Có nhiều cách để viết ứng dụng trò chuyện, vì vậy, hãy mô tả các tính năng tối thiểu mà ứng dụng của chúng ta sẽ có. Sẽ chỉ có một phòng trò chuyện mà tất cả người dùng đều thuộc về. Người dùng có thể chọn và thay đổi tên của họ, nhưng tên phải là duy nhất. Máy chủ sẽ thực thi tính duy nhất này và thông báo khi người dùng thay đổi tên. Ứng dụng sẽ hiển thị danh sách tin nhắn và danh sách người dùng hiện đang ở trong phòng trò chuyện.

Giao diện người dùng đơn giản

Với thông số kỹ thuật này, chúng ta có thể tạo một giao diện người dùng đơn giản bằng Jade để cung cấp các thành phần giao diện người dùng cần thiết. Mở views/index.jade và thêm nội dung này vào bên trong 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')

Mở public/css/app.css và thêm CSS để cung cấp các cột và phần tràn:

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

Tương tác với Socket.IO

Mặc dù Socket.IO hiển thị một biến io trên window, nhưng bạn nên đóng gói biến này trong hệ thống Chèn phần phụ thuộc của AngularJS. Vì vậy, chúng ta sẽ bắt đầu bằng cách viết một dịch vụ để gói đối tượng socket do Socket.IO trả về. Điều này thật tuyệt vời vì sau này, việc kiểm thử tay điều khiển sẽ dễ dàng hơn rất nhiều. Mở public/js/services.js và thay thế nội dung bằng:

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

Lưu ý rằng chúng ta gói từng lệnh gọi lại ổ cắm trong $scope.$apply. Thao tác này cho AngularJS biết rằng cần kiểm tra trạng thái của ứng dụng và cập nhật các mẫu nếu có thay đổi sau khi chạy lệnh gọi lại được truyền vào. Trong nội bộ, $http hoạt động theo cách tương tự; sau khi một số XHR trả về, phương thức này sẽ gọi $scope.$apply để AngularJS có thể cập nhật các thành phần hiển thị cho phù hợp.

Xin lưu ý rằng dịch vụ này không bao gồm toàn bộ API Socket.IO (đây là bài tập cho người đọc ;P ). Tuy nhiên, dịch vụ này bao gồm các phương thức được sử dụng trong hướng dẫn này và sẽ hướng bạn đi đúng hướng nếu bạn muốn mở rộng về dịch vụ này. Tôi có thể xem lại việc viết một trình bao bọc hoàn chỉnh, nhưng điều đó nằm ngoài phạm vi của hướng dẫn này.

Bây giờ, trong trình điều khiển, chúng ta có thể yêu cầu đối tượng socket, giống như chúng ta làm với $http:

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

Bên trong trình điều khiển, hãy thêm logic để gửi và nhận tin nhắn. Mở js/public/controllers.js và thay thế nội dung bằng nội dung sau:

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

Ứng dụng này sẽ chỉ có một thành phần hiển thị, vì vậy, chúng ta có thể xoá tuyến đường khỏi public/js/app.js và đơn giản hoá thành:

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

Viết máy chủ

Mở routes/socket.js. Chúng ta cần xác định một đối tượng để duy trì trạng thái của máy chủ, để tên người dùng là duy nhất.

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

Về cơ bản, việc này xác định một tập hợp tên, nhưng với các API phù hợp hơn với miền của máy chủ trò chuyện. Hãy kết nối socket này với máy chủ để phản hồi các lệnh gọi mà ứng dụng của chúng ta thực hiện:

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

Như vậy là bạn đã hoàn tất ứng dụng. Hãy thử bằng cách chạy node app.js. Ứng dụng sẽ cập nhật theo thời gian thực nhờ Socket.IO.

Kết luận

Bạn có thể thêm nhiều tính năng khác vào ứng dụng nhắn tin nhanh này. Ví dụ: bạn có thể gửi tin nhắn trống. Bạn có thể sử dụng ng-valid để ngăn chặn điều này ở phía máy khách và kiểm tra trên máy chủ. Có thể máy chủ có thể lưu giữ nhật ký gần đây của các tin nhắn để người dùng mới tham gia ứng dụng có thể sử dụng.

Bạn có thể dễ dàng viết các ứng dụng AngularJS sử dụng các thư viện khác sau khi hiểu cách gói các thư viện đó trong một dịch vụ và thông báo cho Angular rằng một mô hình đã thay đổi. Tiếp theo, tôi dự định sẽ trình bày cách sử dụng AngularJS với D3.js, một thư viện trực quan hoá phổ biến.

Tài liệu tham khảo

Angular Socket.IO Seed Ứng dụng nhắn tin tức thì đã hoàn tất AngularJS Express Socket.IO`