Статьи

Использование Socket.IO и Cordova для создания приложения для чата в реальном времени

В этом уроке мы собираемся создать приложение для чата, используя Cordova и Socket.io. Чтобы упростить задачу, мы используем Ionic Framework. Я предполагаю, что вы уже установили все соответствующие SDK на вашем компьютере. И установили Cordova и Ionic, так как мы не будем проходить эти шаги в этом уроке.

Настройка проекта

Сначала нам нужно создать новый проект Ionic, сделайте это с помощью следующей команды:

ionic start project_name 

Это создает пустой Ionic шаблон в текущем каталоге.

Добавление платформы

Затем добавьте платформы, на которых вы собираетесь развернуть. Если вы используете Windows или Ubuntu, вы можете использовать только платформу Android, если вы используете Mac, вы также можете использовать платформу iOS.

 ionic platform add android ionic platform add ios 

Установка внешних зависимостей

После создания проекта откройте файл bower.json в корневом каталоге проекта и добавьте следующее:

 { "name": "cordova-chatapp", "private": "true", "devDependencies": { "ionic": "driftyco/ionic-bower#1.1.1" }, "dependencies": { "angular-local-storage": "~0.2.3", "angular-socket-io": "~0.7.0", "sio-client": "~1.3.6", "angular-moment": "~0.9.2", "moment": "2.9.0" } } 

devDependencies автоматически добавляется в файл bower.json по умолчанию, поэтому все, что мы добавили, — это dependencies . В этом проекте используются следующие зависимости:

  • angular-local-storage : используется для хранения данных в локальном хранилище. В этом проекте он используется для хранения имени текущего пользователя и текущей комнаты.
  • sio-клиент : JavaScript-клиент socket.io.
  • angular-socket-io : позволяет использовать socket.io в Angular.
  • angular-moment : предоставляет угловые директивы для библиотеки moment.js, что позволяет нам использовать момент изнутри Angular.
  • момент : это зависимость углового момента, поэтому он автоматически устанавливается при установке углового момента. Если вы не знакомы с библиотекой moment.js, она в основном используется для манипулирования датами и временем.

Установите эти зависимости с помощью bower install .

Строим проект

Теперь мы готовы начать строить проект. Начните с открытия файла index.html внутри каталога www и связывания файлов сценариев ранее установленных зависимостей. Это включает в себя angular-local-stroage, moment.js, angular-moment и socket.io.

 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width"> <title></title> <link href="lib/ionic/css/ionic.css" rel="stylesheet"> <link href="css/style.css" rel="stylesheet"> <!-- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above <link href="css/ionic.app.css" rel="stylesheet"> --> <!-- ionic/angularjs js --> <script src="lib/ionic/js/ionic.bundle.js"></script> <script src="lib/angular-local-storage/dist/angular-local-storage.min.js"></script> <!-- cordova script (this will be a 404 during development) --> <script src="cordova.js"></script> <!-- your app's js --> <script src="js/app.js"></script> <script src="lib/moment/min/moment.min.js"></script> <script src="lib/angular-moment/angular-moment.min.js"></script> <script src="lib/sio-client/socket.io.js"></script> <script src="http://localhost:3000/socket.io/socket.io.js"></script> <script src="lib/angular-socket-io/socket.js"></script> <script src="js/services/SocketService.js"></script> <script src="js/controllers/HomeController.js"></script> <script src="js/controllers/RoomController.js"></script> </head> <body ng-app="starter"> <ion-nav-view></ion-nav-view> </body> </html> 

Есть другие сценарии, которые могут быть вам не знакомы. Это включает в себя скрипт socket.io для сервера.

 <script src="http://localhost:3000/socket.io/socket.io.js"></script> 

Сервис Socket, используемый для подключения к серверу socket.io.

 <script src="js/services/SocketService.js"></script> 

И домашний контроллер, который обрабатывает события, которые происходят на домашних страницах. Это включает в себя страницу входа и страницу комнат, где пользователь выбирает, в какую комнату войти.

 <script src="js/controllers/HomeController.js"></script> 

Наконец, есть комнатный контроллер, который обрабатывает события внутри комнаты чата, например, когда пользователь отправляет сообщение или покидает комнату.

 <script src="js/controllers/RoomController.js"></script> 

Позже мы рассмотрим код для каждого из этих файлов. А пока просто помни, для чего они.

Добавление зависимостей

Откройте файл js / app.js и включите следующие сервисы:

  • LocalStorageModule : позволяет нам использовать локальное хранилище.
  • btford.socket-io : позволяет нам использовать socket.io.
  • angularMoment : позволяет нам использовать moment.js.
 angular.module('starter', ['ionic', 'LocalStorageModule', 'btford.socket-io', 'angularMoment']) 

Добавление маршрутов

Еще в js / app.js добавьте конфигурацию для различных состояний с помощью метода config . Это позволяет нам указать различные страницы, URL-адрес, по которому они могут быть доступны, и шаблон HTML, который будет отображаться при доступе к этим страницам.

Состояние login возвращает страницу входа, состояние rooms возвращает список комнат, в которые пользователь может присоединиться, а состояние room возвращает страницу чата. Страница login — это страница по умолчанию, которая будет отображаться, если ни одно из других состояний не активно.

 .config(function($stateProvider, $urlRouterProvider) { $stateProvider .state('login', { url: '/login', templateUrl: 'templates/login.html' }) .state('rooms', { url: '/rooms', templateUrl: 'templates/rooms.html' }) .state('room', { url: '/room', templateUrl: 'templates/room.html' }); // if none of the above states are matched, use this as the fallback $urlRouterProvider.otherwise('/login'); }) 

Домашний контроллер

Затем создайте Home Controller в js / controllers / HomeController.js . Он обрабатывает все взаимодействия пользовательского интерфейса и данные, используемые на странице входа в систему и комнаты.

Внутри контроллера мы внедряем следующие сервисы:

  • $scope : используется для присоединения данных или функций к текущей странице.
  • $state : используется для перенаправления в другое состояние.
  • localStorageService : используется для сохранения и получения данных из локального хранилища.
  • SocketService : используется для отправки данных через веб-сокеты.
 (function(){ angular.module('starter') .controller('HomeController', ['$scope', '$state', 'localStorageService', 'SocketService', HomeController]); function HomeController($scope, $state, localStorageService, SocketService){ var me = this; me.current_room = localStorageService.get('room'); me.rooms = ['Coding', 'Art', 'Writing', 'Travel', 'Business', 'Photography']; $scope.login = function(username){ localStorageService.set('username', username); $state.go('rooms'); }; $scope.enterRoom = function(room_name){ me.current_room = room_name; localStorageService.set('room', room_name); var room = { 'room_name': room_name }; SocketService.emit('join:room', room); $state.go('room'); }; } })(); 

Разбивая этот код, сначала мы получим название текущей комнаты, если она есть.

 me.current_room = localStorageService.get('room'); 

Комнаты жестко запрограммированы в массиве. Это будет использовано в шаблоне комнат позже.

 me.rooms = ['Coding', 'Art', 'Writing', 'Travel', 'Business', 'Photography']; 

Присоедините функцию login в login к текущей области. Эта функция будет вызываться, когда пользователь нажимает кнопку входа в систему. Имя username будет передано этой функции и сохранено в локальном хранилище. Затем перенаправляем в состояние room .

 $scope.login = function(username){ localStorageService.set('username', username); $state.go('rooms'); }; 

Как только пользователь будет перенаправлен в состояние rooms , он увидит список комнат, в каждом из которых enterRoom функция enterRoom . Функция выполняется всякий раз, когда пользователь нажимает на комнату, имя комнаты передается в качестве аргумента, используемого для установки current_room и сохраняется в локальном хранилище. Событие join:room отправляется через сокет, который содержит объект, представляющий текущую комнату. Наконец, мы перенаправляем в состояние room .

 $scope.enterRoom = function(room_name){ me.current_room = room_name; localStorageService.set('room', room_name); var room = { 'room_name': room_name }; SocketService.emit('join:room', room); $state.go('room'); }; 

SocketService.emit() позволяют нам отправлять данные через сокет. Это означает, что данные отправляются на сервер socket.io (созданный позже) в режиме реального времени.

Шаблон входа

Создайте шаблон входа ( templates / login.html ), который является страницей приложения по умолчанию. Это позволяет пользователю вводить свое имя пользователя и нажимать кнопку входа для входа. При этом используется HomeController , добавляя его в качестве значения атрибута ng-controller и используя home_ctrl в качестве псевдонима для HomeController . Когда нажимается кнопка входа в систему, мы вызываем функцию login присоединенную к области ранее в контроллере. Это принимает текущее значение, введенное в имени пользователя в качестве аргумента.

 <ion-view title="Login" ng-controller="HomeController as home_ctrl"> <header class="bar bar-header bar-positive"> <h1 class="title">Login</h1> </header> <ion-content class="has-header padding"> <div class="list"> <label class="item item-input"> <input type="text" ng-model="home_ctrl.username" placeholder="User name"> </label> <div class="padding"> <button class="button button-positive button-block" ng-click="login(home_ctrl.username)">Enter</button> </div> </div> </ion-content> </ion-view> 

Шаблон номера

Затем создайте шаблон комнаты в templates / rooms.html . Это страница, на которую пользователь перенаправляется после входа в систему. Здесь перечислены все комнаты, предоставленные HomeController ранее. Директива ng-repeat позволяет нам перебирать все комнаты, а директива ng-click позволяет нам выполнять функцию enterRoom по щелчку пользователя.

 <ion-view title="Rooms" ng-controller="HomeController as home_ctrl"> <header class="bar bar-header bar-positive"> <h1 class="title">Rooms</h1> </header> <ion-content class="has-header padding"> <div class="card" ng-repeat="room in home_ctrl.rooms"> <div class="item item-text-wrap text-center" ng-click="enterRoom(room)"> <strong>{{room}}</strong> </div> </div> </ion-content> </ion-view> 

SocketService

Затем создайте js / services / SocketService.js , который мы используем для подключения к серверу socket.io с помощью SocketService() . Этот сервис использует socketFactory предоставляемый angular-socket-io. Здесь вы можете видеть, что мы подключаемся к порту 3000 localhost. Если вы планируете развернуть приложение чата позже, вы должны изменить http: // localhost: 3000 на доступный в Интернете URL. В целях разработки вы можете использовать ngrok для предоставления локального URL-адреса в Интернете.

 (function(){ angular.module('starter') .service('SocketService', ['socketFactory', SocketService]); function SocketService(socketFactory){ return socketFactory({ ioSocket: io.connect('http://localhost:3000') }); } })(); 

Комнатный контроллер

Создайте комнатный контроллер в js / services / RoomController.js, который обрабатывает все события, происходящие в комнате чата. В этом контроллере мы $ionicScrollDelegate два новых сервиса, moment и $ionicScrollDelegate . moment позволяет нам использовать библиотеку moment.js, чтобы получить текущую метку времени при отправке сообщения. Это позволяет нам форматировать отметку времени в удобном для человека формате (например, 4 секунды назад). $ionicScrollDelegate автоматически прокручивает приложение каждый раз, когда новое сообщение $ionicScrollDelegate в массив. Таким образом, пользователь всегда видит самое последнее сообщение.

 (function(){ angular.module('starter') .controller('RoomController', ['$scope', '$state', 'localStorageService', 'SocketService', 'moment', '$ionicScrollDelegate', RoomController]); function RoomController($scope, $state, localStorageService, SocketService, moment, $ionicScrollDelegate){ var me = this; me.messages = []; $scope.humanize = function(timestamp){ return moment(timestamp).fromNow(); }; me.current_room = localStorageService.get('room'); var current_user = localStorageService.get('username'); $scope.isNotCurrentUser = function(user){ if(current_user != user){ return 'not-current-user'; } return 'current-user'; }; $scope.sendTextMessage = function(){ var msg = { 'room': me.current_room, 'user': current_user, 'text': me.message, 'time': moment() }; me.messages.push(msg); $ionicScrollDelegate.scrollBottom(); me.message = ''; SocketService.emit('send:message', msg); }; $scope.leaveRoom = function(){ var msg = { 'user': current_user, 'room': me.current_room, 'time': moment() }; SocketService.emit('leave:room', msg); $state.go('rooms'); }; SocketService.on('message', function(msg){ me.messages.push(msg); $ionicScrollDelegate.scrollBottom(); }); } })(); 

Разбить этот код. Сначала у нас есть массив, в котором хранятся сообщения, отправленные в текущей комнате. Всякий раз, когда сообщение отправляется через сокет, мы просто помещаем его в этот массив, а затем можем использовать его из представления для вывода всех сообщений.

 me.messages = []; 

Прикрепите функцию humanize в поле зрения. Это использует библиотеку moment.js для стандартного форматирования метки времени.

 $scope.humanize = function(timestamp){ return moment(timestamp).fromNow(); }; 

Получить имя текущей комнаты из локального хранилища и назначить его контроллеру. Мы будем использовать эти данные позже при отправке сообщений и выходе из комнаты.

 me.current_room = localStorageService.get('room'); 

Получить имя текущего пользователя из локального хранилища.

 var current_user = localStorageService.get('username'); 

Присоедините функцию isNotCurrentUser к текущей области, которая проверяет, isNotCurrentUser ли пользователь, указанный в качестве аргумента, с текущим пользователем. Он возвращает другую строку на основе результата, используемого в представлении, так что контейнер сообщений для текущего пользователя оформляется по-разному.

 $scope.isNotCurrentUser = function(user){ if(current_user != user){ return 'not-current-user'; } return 'current-user'; }; 

Функция sendTextMessage выполняется, когда пользователь нажимает кнопку для отправки сообщения. Это создает объект, содержащий имя текущей комнаты, текущего пользователя и фактическое сообщение. Затем мы помещаем его в массив messages чтобы пользователь мог его сразу увидеть. А затем вызовите функцию scrollBottom в $ionicScrollDelegate чтобы прокрутить страницу вниз. Затем мы присваиваем сообщение пустой строке, чтобы содержимое текстового поля было удалено. Наконец, мы отправляем объект.

 $scope.sendTextMessage = function(){ var msg = { 'room': me.current_room, 'user': current_user, 'text': me.message }; me.messages.push(msg); $ionicScrollDelegate.scrollBottom(); me.message = ''; SocketService.emit('send:message', msg); }; 

Функция leaveRoom покидает комнату, отправляя на сервер сообщение leave:room чтобы текущий пользователь был удален из текущей комнаты, отправляя имя пользователя, покидающего комнату. Это используется сервером для отправки сообщения всем другим пользователям в комнате, что конкретный пользователь покинул комнату.

 $scope.leaveRoom = function(){ var msg = { user: current_user, room: me.current_room }; SocketService.emit('leave:room', msg); }; 

Это слушает сообщения, отправленные другими пользователями в комнате. Когда сообщение получено, мы помещаем его в массив messages чтобы оно отображалось в представлении.

 SocketService.on('message', function(msg){ me.messages.push(msg); $ionicScrollDelegate.scrollBottom(); }); 

Шаблон комнаты

Создайте шаблон комнаты в templates / room.html , который является самой комнатой чата. Это где сообщения отправляются людьми в комнате. Это зависит от RoomController для его данных и функций, которые будут выполняться (отправка сообщений и выход из комнаты).

 <ion-view title="{{ room_ctrl.current_room }}" ng-controller="RoomController as room_ctrl"> <header class="bar bar-header bar-positive"> <h1 class="title">{{ room_ctrl.current_room }}</h1> <button class="button button-assertive" ng-click="leaveRoom()">Leave</button> </header> <ion-content class="has-header padding"> <div class="list" ng-if="room_ctrl.messages.length > 0"> <li class="item item-text-wrap no-border {{ isNotCurrentUser(msg.user) }}" ng-repeat="msg in room_ctrl.messages"> <div class="msg"> <div class="details padding"> <p> <div class="user">{{ msg.user }}</div> <div class="message">{{ msg.text }}</div> </p> <small>{{ humanize(msg.time) }}</small> </div> </div> </li> </div> <div class="card" ng-if="!room_ctrl.messages.length"> <div class="item item-text-wrap"> No messages yet. </div> </div> </ion-content> <footer class="bar bar-footer bar-positive item-input-inset"> <label class="item-input-wrapper"> <input type="text" id="message" name="message" ng-model="room_ctrl.message" placeholder="Type message"> </label> <a class="button button-icon icon ion-android-send" ng-click="sendTextMessage()"></a> </footer> </ion-view> 

Разбить этот код. Внутри основного контента мы проверяем наличие сообщений, проверяя длину элемента. Если есть, мы используем ng-repeat чтобы перебрать все сообщения. Обратите внимание, что для этого используются живые данные, хранящиеся в массиве messages . Это означает, что каждый раз, когда новое сообщение помещается в этот массив, оно автоматически отображается. Для каждой итерации мы используем функцию isNotCurrentUser для вывода дополнительного класса для текущего элемента. Затем мы показываем имя пользователя, сообщение и время отправки.

 <div class="list" ng-if="room_ctrl.messages.length > 0"> <li class="item item-text-wrap no-border {{ isNotCurrentUser(msg.user) }}" ng-repeat="msg in room_ctrl.messages"> <div class="msg"> <div class="details padding"> <p> <div class="user">{{ msg.user }}</div> <div class="message">{{ msg.text }}</div> </p> <small>{{ humanize(msg.time) }}</small> </div> </div> </li> </div> 

Если сообщений нет, мы выводим карточку, в которой еще нет сообщений.

 <div class="card" ng-if="!room_ctrl.messages.length"> <div class="item item-text-wrap"> No messages yet. </div> </div> 

Нижний колонтитул, где у нас есть форма, которая позволяет пользователю отправлять сообщения.

 <footer class="bar bar-footer bar-positive item-input-inset"> <label class="item-input-wrapper"> <input type="text" id="message" name="message" ng-model="room_ctrl.message" placeholder="Type message"> </label> <a class="button button-icon icon ion-android-send" ng-click="sendTextMessage()"></a> </footer> 

стайлинг

Добавьте следующее в css / style.css , в основном для чата.

 ion-content { margin-bottom: 50px !important; } .no-border { border: none; } .user { font-weight: bold; } .current-user { float: left; clear: both; } .not-current-user { float: right; clear: both; } .current-user .details { background-color: #72CBFF; } .not-current-user .details { background-color: #DCDCDC; } 

Сервер Socket.io

Теперь мы готовы работать на стороне сервера. Создайте папку сервера внутри корневого каталога приложения и внутри папки, создайте файл package.json , добавив следующее:

 { "name": "cordova-chatapp", "version": "0.0.1", "dependencies": { "socket.io": "^1.3.7" } } 

Сохраните файл и выполните npm install . Это устанавливает версию сервера socket.io, ранее мы установили версию клиента.

Создайте файл chat-server.js и добавьте следующее:

 var io = require('socket.io')(3000); io.on('connection', function(socket){ socket.on('join:room', function(data){ var room_name = data.room_name; socket.join(room_name); }); socket.on('leave:room', function(msg){ msg.text = msg.user + " has left the room"; socket.in(msg.room).emit('exit', msg); socket.leave(msg.room); }); socket.on('send:message', function(msg){ socket.in(msg.room).emit('message', msg); }); }); 

Вызов require('socket.io') возвращает функцию. Эта функция принимает порт, на котором будет работать сервер. В этом случае мы используем порт 3000, поэтому в SocketService ранее мы подключались к http: // localhost: 3000 . В файле index.html скрипт socket.io работает на порте 3000. Если вы измените значение для порта, вы также должны изменить его в этих двух других местах.

 var io = require('socket.io')(3000); 

Каждый раз, когда клиент подключается к socket.io, выполняется приведенный ниже код. Итак, мы обертываем все вызовы функции socket.io внутри этого. Клиент socket.io передает объект, содержащий информацию о сокете. Это в основном идентификатор, назначаемый socket.io каждому пользователю.

 io.on('connection', function(socket){ ... }); 

Ранее в приложении были обращения к SocketService.emit('join:room', msg) . Это событие вызывается на сервере каждый раз, когда выполняется. Как вы видели ранее, мы добавили имя room_name к объекту, который мы отправляли. Мы просто получаем его отсюда и используем, чтобы добавить пользователя в комнату.

 socket.on('join:room', function(data){ var room_name = data.room_name; socket.join(room_name); console.log('someone joined room ' + room_name + ' ' + socket.id); }); 

Этот код выполняется всякий раз, когда пользователь покидает комнату. Сначала всем другим пользователям отправляется сообщение о том, что пользователь покинул комнату. Затем пользователь удаляется из комнаты.

 socket.on('leave:room', function(msg){ msg.text = msg.user + " has left the room"; socket.in(msg.room).emit('exit', msg); socket.leave(msg.room); }); 

Нам нужно прослушивать сообщения, отправленные с клиента. Все, что нужно, это отправить сообщение всем пользователям в комнате.

 socket.on('send:message', function(msg){ socket.in(msg.room).emit('message', msg); }); 

Тестирование приложения

Чтобы протестировать приложение, сначала вам нужно запустить сервер socket.io:

 node chat-server.js 

Затем в корневой директории проекта используйте ionic serve для проверки в вашем браузере.

 ionic serve 

Вот как должен выглядеть окончательный результат:

страница чата

Вывод

Это оно! В этом уроке мы узнали, как создать приложение для чата с Cordova. Приложение чата простое, и вот несколько идей по улучшению приложения:

  • Добавить базу данных для хранения сообщений. Это позволяет пользователям, которые вошли в комнату в конце обсуждения, понять, о чем говорят другие.
  • Добавить функцию обмена фотографиями.
  • Добавить функциональность голосовой почты.
  • Добавьте смайлики.

Список бесконечен. Основная идея заключается в том, что вы всегда можете улучшить вещи. Если вы хотите увидеть код, используемый в этом руководстве, взгляните на Github .