เกริ่นนำ
AngularJS เป็นเฟรมเวิร์ก JavaScript ที่ยอดเยี่ยมซึ่งให้คุณการเชื่อมโยงข้อมูลแบบ 2 ทางทั้งที่ใช้งานง่ายและรวดเร็ว ระบบคำสั่งที่มีประสิทธิภาพซึ่งให้คุณสร้างคอมโพเนนต์ที่กำหนดเองแบบนำกลับมาใช้ใหม่ได้ และอีกมากมาย Socket.IO เป็น Wrapper และ Polyfill ที่ทำงานข้ามเบราว์เซอร์สำหรับ WebSocket ที่ทำให้การพัฒนาแอปพลิเคชันแบบเรียลไทม์กลายเป็นเรื่องง่าย แน่นอนว่าทั้ง 2 อย่างทำงานด้วยกันได้ดี
เราเคยเขียนเรื่องการเขียนแอป 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
หลังจากที่ได้ Seed แล้ว คุณต้องมีทรัพยากร Dependency เล็กน้อยด้วย npm เปิดเทอร์มินัลไปยังไดเรกทอรีที่มี Seed แล้วเรียกใช้
npm install
เมื่อติดตั้งทรัพยากร Dependency เหล่านี้แล้ว คุณจะเรียกใช้แอป Skeleton ได้
node app.js
แล้วดูในเบราว์เซอร์ที่ http://localhost:3000
เพื่อให้มั่นใจว่า Seed ทำงานได้ตามที่คาดไว้
การตัดสินใจเกี่ยวกับฟีเจอร์ของแอป
การเขียนแอปพลิเคชันแชทมีมากกว่า 2-3 วิธี ดังนั้นเราจะอธิบายถึงคุณสมบัติเล็กๆ น้อยๆ ของเรา ผู้ใช้ทั้งหมดจะมีห้องแชทเพียงห้องเดียว ผู้ใช้เลือกและเปลี่ยนชื่อได้ แต่ชื่อต้องไม่ซ้ำกัน เซิร์ฟเวอร์จะบังคับใช้ความไม่ซ้ำกันนี้และประกาศเมื่อผู้ใช้เปลี่ยนชื่อ ไคลเอ็นต์ควรแสดงรายการข้อความและรายชื่อผู้ใช้ที่อยู่ในห้องแชทในปัจจุบัน
ส่วนหน้าแบบง่าย
ด้วยข้อกำหนดนี้ เราสามารถสร้างส่วนหน้าที่เรียบง่ายด้วย 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
แต่ก็ควรห่อหุ้มตัวแปรในระบบ Dependency Injection ของ 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 อัปเดตมุมมองให้สอดคล้องกันได้
โปรดทราบว่าบริการนี้ไม่ได้รวม Socket.IO API ทั้งหมด (ซึ่งเป็นแบบฝึกหัดสำหรับผู้อ่าน ;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 แอปการรับส่งข้อความโต้ตอบแบบทันทีเสร็จสิ้นแล้ว AngularJS Express Socket.IO