Статьи

Horizon: масштабируемый бэкэнд, идеально подходящий для мобильных приложений JavaScript

Horizon — это масштабируемый бэкэнд для кроссплатформенных мобильных приложений на основе JavaScript, особенно тех, которым требуется функциональность в реальном времени. Он был создан замечательными людьми из RethinkDB и поэтому использует RethinkDB в качестве базы данных по умолчанию. Если вы не знакомы с RethinkDB, это база данных с открытым исходным кодом с возможностями реального времени.

Horizon предоставляет клиентский API, который позволяет вам взаимодействовать с базовой базой данных. Это означает, что вам не нужно писать какой-либо внутренний код. Все, что вам нужно сделать, — это запустить новый сервер, запустить его, а Horizon позаботится обо всем остальном. Данные синхронизируются между сервером и подключенными клиентами в режиме реального времени.

Если вы хотите узнать больше о Horizon, вы можете проверить их страницу часто задаваемых вопросов .

В этом уроке вы создадите приложение Tic-Tac-Toe с Ionic и Horizon. Я предполагаю, что вы не новичок в Ionic и Cordova, поэтому я не буду подробно объяснять код, специфичный для Ionic. Я рекомендую вам ознакомиться с Руководством по началу работы на веб-сайте Ionic, если вы хотите немного истории. Если вы хотите следовать, вы можете клонировать репозиторий приложений на Github . Вот как будет выглядеть финальное приложение:

Крестики-нолики

Установка Horizon

RethinkDB служит базой данных Horizon, поэтому вам необходимо сначала установить RethinkDB перед установкой Horizon. Вы можете узнать, как установить RethinkDB здесь .

После установки RethinkDB вы можете установить Horizon через npm , выполнив в своем терминале следующее:

npm install -g horizon 

Horizon Server

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

Вы можете создать новый сервер Horizon, выполнив в своем терминале следующее:

 hz init tictactoe-server 

Это создает базу данных RethinkDB и файлы сервера, которые использует Horizon.

Как только сервер создан, вы можете запустить его, выполнив:

 hz serve --dev 

В приведенной выше команде вы указали --dev в качестве опции. Это означает, что вы хотите запустить сервер разработки. Следующие параметры устанавливаются с сервером разработки:

  • --secure no : это означает, что веб-сокеты и файлы не обслуживаются по зашифрованному соединению.
  • --permissions no : отключает ограничения разрешений . Это означает, что любой клиент может выполнить любую операцию, которую он хочет в базе данных. Система разрешений Horizon основана на белом списке. Это означает, что по умолчанию все пользователи не имеют права что-либо делать. Вы должны явно указать, какие операции разрешены.
  • --auto-create-collection yes : автоматически создает коллекцию при первом использовании. В Horizon коллекции являются эквивалентом таблиц в реляционной базе данных. Если для этого параметра установлено значение yes , каждый раз, когда клиент использует новую коллекцию, она создается автоматически.
  • --auto-create-index yes : автоматически создает индекс при первом использовании.
  • --start-rethinkdb yes : автоматически запускает новый экземпляр RethinkDB в текущем каталоге.
  • --allow-unauthenticated yes : позволяет неаутентифицированным пользователям выполнять операции с базой данных.
  • --allow-anonymous yes : позволяет анонимным пользователям выполнять операции с базой данных.
  • --serve-static ./dist : включает обслуживание статических файлов. Это полезно, если вы хотите проверить взаимодействие с Horizon API в браузере. По умолчанию сервер horizon работает через порт 8181, поэтому вы можете получить доступ к серверу, посетив http: // localhost: 8181 .

Примечание . Опция --dev никогда не должна использоваться в --dev поскольку она открывает множество дыр для использования злоумышленниками.

Сборка приложения

Теперь вы готовы к созданию приложения. Начните с создания нового приложения Ionic:

 ionic start tictactoe blank 

Установка Chance.js

Затем вам нужно установить chance.js , библиотеку утилит JavaScript для генерации случайных данных. Для этого приложения вы используете его для генерации уникального идентификатора для игроков. Вы можете установить chance.js через bower с помощью следующей команды:

 bower install chance 

index.html

Откройте файл www / index.html и добавьте следующий код:

 <!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"> --> <!-- chance.js --> <script src="lib/chance/dist/chance.min.js"></script> <!-- ionic/angularjs js --> <script src="lib/ionic/js/ionic.bundle.js"></script> <!-- cordova script (this will be a 404 during development) --> <script src="cordova.js"></script> <!-- horizon script --> <script src="http://127.0.0.1:8181/horizon/horizon.js"></script> <!-- your app's js --> <script src="js/app.js"></script> <!--main app logic --> <script src="js/controllers/HomeController.js"></script> </head> <body ng-app="starter"> <ion-nav-view></ion-nav-view> </body> </html> 

Большая часть приведенного выше кода представляет собой шаблонный код из пустого начального шаблона Ionic, добавляющий только сценарий chance.js:

 <script src="lib/chance/dist/chance.min.js"></script> 

Сценарий горизонта обслуживается сервером горизонта.

Примечание : вы должны изменить URL, если вы собираетесь развернуть это позже.

 <script src="http://127.0.0.1:8181/horizon/horizon.js"></script> 

Основная логика приложения находится в этом файле JavaScript:

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

app.js

В файле app.js вы запускаете код для инициализации приложения. Откройте www / js / app.js и добавьте следующее прямо под функцией run :

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

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

HomeController.Js

Создайте файл HomeController.js в каталоге www / js / controllers и добавьте следующее:

 (function(){ angular.module('starter') .controller('HomeController', ['$scope', HomeController]); function HomeController($scope){ var me = this; $scope.has_joined = false; $scope.ready = false; const horizon = Horizon({host: 'localhost:8181'}); horizon.onReady(function(){ $scope.$apply(function(){ $scope.ready = true; }); }); horizon.connect(); $scope.join = function(username, room){ me.room = horizon('tictactoe'); var id = chance.integer({min: 10000, max: 999999}); me.id = id; $scope.player = username; $scope.player_score = 0; me.room.findAll({room: room, type: 'user'}).fetch().subscribe(function(row){ var user_count = row.length; if(user_count == 2){ alert('Sorry, room is already full.'); }else{ me.piece = 'X'; if(user_count == 1){ me.piece = 'O'; } me.room.store({ id: id, room: room, type: 'user', name: username, piece: me.piece }); $scope.has_joined = true; me.room.findAll({room: room, type: 'user'}).watch().subscribe( function(users){ users.forEach(function(user){ if(user.id != me.id){ $scope.$apply(function(){ $scope.opponent = user.name; $scope.opponent_piece = user.piece; $scope.opponent_score = 0; }); } }); }, function(err){ console.log(err); } ); me.room.findAll({room: room, type: 'move'}).watch().subscribe( function(moves){ moves.forEach(function(item){ var block = document.getElementById(item.block); block.innerHTML = item.piece; block.className = "col done"; }); me.updateScores(); }, function(err){ console.log(err); } ); } }); } $scope.placePiece = function(id){ var block = document.getElementById(id); if(!angular.element(block).hasClass('done')){ me.room.store({ type: 'move', room: me.room_name, block: id, piece: me.piece }); } }; me.updateScores = function(){ const possible_combinations = [ [1, 4, 7], [2, 5, 8], [3, 2, 1], [4, 5, 6], [3, 6, 9], [7, 8, 9], [1, 5, 9], [3, 5, 7] ]; var scores = {'X': 0, 'O': 0}; possible_combinations.forEach(function(row, row_index){ var pieces = {'X' : 0, 'O': 0}; row.forEach(function(id, item_index){ var block = document.getElementById(id); if(angular.element(block).hasClass('done')){ var piece = block.innerHTML; pieces[piece] += 1; } }); if(pieces['X'] == 3){ scores['X'] += 1; }else if(pieces['O'] == 3){ scores['O'] += 1; } }); $scope.$apply(function(){ $scope.player_score = scores[me.piece]; $scope.opponent_score = scores[$scope.opponent_piece]; }); } } })(); 

Разбивая код выше, сначала установите состояние по умолчанию. has_joined определяет, присоединился ли пользователь к комнате. ready определяет, подключен ли пользователь к серверу Horizon. Вы не показываете пользовательский интерфейс приложения, пока для этого параметра установлено значение false .

 $scope.has_joined = false; $scope.ready = false; 

Подключитесь к серверу Horizon:

 const horizon = Horizon({host: 'localhost:8181'}); horizon.onReady(function(){ $scope.$apply(function(){ $scope.ready = true; }); }); horizon.connect(); //connect to the server 

Как я уже говорил ранее, Horizon по умолчанию работает на порту 8181, поэтому вы указали localhost:8181 в качестве порта. Если вы подключаетесь к удаленному серверу, это должен быть IP или доменное имя, назначенное серверу. Когда пользователь подключен к серверу, onReady событие onReady . Здесь вы можете установить true чтобы вы могли показать пользовательский интерфейс.

 horizon.onReady(function(){ $scope.$apply(function(){ $scope.ready = true; }); }); 

Присоединение к комнате

Затем выполняется ли функция join когда пользователь нажимает кнопку « Присоединиться» :

 $scope.join = function(username, room){ ... }; 

Внутри функции подключитесь к коллекции под названием tictactoe .

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

 me.room = horizon('tictactoe'); 

Сгенерируйте идентификатор и установите его в качестве идентификатора для текущего пользователя:

 var id = chance.integer({min: 10000, max: 999999}); me.id = id; 

Установите имя пользователя и счет игрока по умолчанию.

Примечание . Эти переменные связаны с шаблоном, поэтому вы можете отображать и обновлять их в любое время.

 $scope.player = username; $scope.player_score = 0; 

Запросите таблицу для документов, в которых для room задана текущая комната и тип user . Не смущайтесь при использовании функции subscribe , вы на самом деле не слушаете изменения. Вы использовали функцию fetch которая означает, что она будет выполняться только после входа пользователя в комнату.

 me.room.findAll({room: room, type: 'user'}).fetch().subscribe(function(row){ ... }); 

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

 var user_count = row.length; if(user_count == 2){ alert('Sorry, room is already full.'); }else{ ... } 

В противном случае перейдите к логике принятия пользователя, определив кусок, который будет назначен пользователю в зависимости от текущего количества пользователей. Первый человек, пришедший в комнату, получает фигуру «Х», второй — фигуру «О».

 me.piece = 'X'; if(user_count == 1){ me.piece = 'O'; } 

После того, как вы определили часть, сохраните нового пользователя в коллекции и переключите переключатель has_joined чтобы появилась has_joined для крестики-нолики.

 me.room.store({ id: id, room: room, type: 'user', name: username, piece: me.piece }); $scope.has_joined = true; 

Далее прислушайтесь к изменениям в коллекции. На этот раз вместо fetch используйте watch . Это выполняет функцию обратного вызова каждый раз, когда добавляется новый документ или обновляется (или удаляется) существующий документ, который соответствует предоставленному запросу. Когда функция обратного вызова выполнена, просмотрите все результаты и задайте данные оппонента, если идентификатор пользователя документа не совпадает с идентификатором текущего пользователя. Вот как вы показываете текущему пользователю, кто является его противником.

 me.room.findAll({room: room, type: 'user'}).watch().subscribe( function(users){ users.forEach(function(user){ if(user.id != me.id){ $scope.$apply(function(){ $scope.opponent = user.name; $scope.opponent_piece = user.piece; $scope.opponent_score = 0; }); } }); }, function(err){ console.log(err); } ); 

Затем подпишитесь на изменения, внесенные в документы типа move выполняемые каждый раз, когда один из игроков размещает свою фигуру на доске. Если это произойдет, переберите все ходы и добавьте текст в соответствующий блок. Теперь я буду использовать слово «блок», чтобы ссылаться на каждую ячейку на доске .

Добавленный текст является частью, используемой каждым из пользователей, также заменяя имя класса на col done . col — это класс, используемый для реализации сетки Ionic, а done — это класс, который показывает, что в конкретном блоке уже есть фрагмент. Вы используете это для проверки, может ли пользователь все еще поместить свою часть в блок. После обновления пользовательского интерфейса для доски обновите оценки, вызвав функцию updateScores (которую вы добавите позже).

 me.room.findAll({room: room, type: 'move'}).watch().subscribe( function(moves){ moves.forEach(function(item){ var block = document.getElementById(item.block); block.innerHTML = item.piece; block.className = "col done"; }); me.updateScores(); }, function(err){ console.log(err); } ); 

Размещение части

Вы вызываете функцию placePiece каждый раз, когда пользователь нажимает на любой из блоков на плате, предоставляя идентификатор блока в качестве аргумента этой функции. Это позволяет вам манипулировать им любым удобным для вас способом. В этом случае вы используете его, чтобы проверить, не имеет ли класс класса done . Если этого не произойдет, создайте новый ход с указанием комнаты, ID блока и части.

 $scope.placePiece = function(id){ var block = document.getElementById(id); if(!angular.element(block).hasClass('done')){ me.room.store({ type: 'move', room: me.room_name, block: id, piece: me.piece }); } }; 

Обновление результатов

Чтобы обновить оценки, создайте массив, содержащий возможные выигрышные комбинации.

 const possible_combinations = [ [1, 4, 7], [2, 5, 8], [3, 2, 1], [4, 5, 6], [3, 6, 9], [7, 8, 9], [1, 5, 9], [3, 5, 7] ]; 

[1, 4, 7] — первая строка, [1, 2, 3] — первая колонка и так далее. Порядок на самом деле не имеет значения, если есть соответствующие цифры. Вот изображение, которое поможет вам лучше понять:

крестики-нолики

Вы также можете сделать по диагонали ( [1, 5, 9] и [3, 5, 7] ), но инструмент редактирования, который я использовал, не позволяет мне, так что терпите меня.

Затем инициализируйте оценки для каждого отдельного произведения и просматривайте каждую возможную комбинацию. Для каждой итерации цикла инициализируйте общее количество фигур для каждого элемента, уже размещенного на доске. Затем переберите пункты для возможных комбинаций. Используя id , проверьте, не прикреплен ли к соответствующему блоку кусок. Если есть, возьмите фактическую часть и увеличьте общее количество частей. Как только цикл завершен, проверьте, равны ли итоговые значения для каждой части 3 . Если они это сделают, увеличивайте счет для части, пока вы не пройдете все возможные комбинации. После завершения обновите счет текущего игрока и противника.

 var scores = {'X': 0, 'O': 0}; possible_combinations.forEach(function(row, row_index){ var pieces = {'X' : 0, 'O': 0}; row.forEach(function(id, item_index){ var block = document.getElementById(id); if(angular.element(block).hasClass('done')){ //check if there's already a piece var piece = block.innerHTML; pieces[piece] += 1; } }); if(pieces['X'] == 3){ scores['X'] += 1; }else if(pieces['O'] == 3){ scores['O'] += 1; } }); //update current player and opponent score $scope.$apply(function(){ $scope.player_score = scores[me.piece]; $scope.opponent_score = scores[$scope.opponent_piece]; }); 

Основной шаблон

Создайте файл home.html в каталоге www / templates и добавьте следующее:

 <ion-view title="Home" ng-controller="HomeController as home_ctrl" ng-init="connect()"> <header class="bar bar-header bar-stable"> <h1 class="title">Ionic Horizon Tic Tac Toe</h1> </header> <ion-content class="has-header" ng-show="home_ctrl.ready"> <div id="join" class="padding" ng-hide="home_ctrl.has_joined"> <div class="list"> <label class="item item-input"> <input type="text" ng-model="home_ctrl.room" placeholder="Room Name"> </label> <label class="item item-input"> <input type="text" ng-model="home_ctrl.username" placeholder="User Name"> </label> </div> <button class="button button-positive button-block" ng-click="join(home_ctrl.username, home_ctrl.room)"> join </button> </div> <div id="game" ng-show="home_ctrl.has_joined"> <div id="board"> <div class="row"> <div class="col" ng-click="placePiece(1)" id="1"></div> <div class="col" ng-click="placePiece(2)" id="2"></div> <div class="col" ng-click="placePiece(3)" id="3"></div> </div> <div class="row"> <div class="col" ng-click="placePiece(4)" id="4"></div> <div class="col" ng-click="placePiece(5)" id="5"></div> <div class="col" ng-click="placePiece(6)" id="6"></div> </div> <div class="row"> <div class="col" ng-click="placePiece(7)" id="7"></div> <div class="col" ng-click="placePiece(8)" id="8"></div> <div class="col" ng-click="placePiece(9)" id="9"></div> </div> </div> <div id="scores"> <div class="row"> <div class="col col-50 player"> <div class="player-name" ng-bind="player"></div> <div class="player-score" ng-bind="player_score"></div> </div> <div class="col col-50 player"> <div class="player-name" ng-bind="opponent"></div> <div class="player-score" ng-bind="opponent_score"></div> </div> </div> </div> </div> </ion-content> </ion-view> 

Если разбить приведенный выше код, у вас есть основная оболочка, которую вы не увидите, пока пользователь не подключится к серверу Horizon.

 <ion-content class="has-header" ng-show="home_ctrl.ready"> ... </ion-content> 

Форма для присоединения к комнате:

 <div id="join" class="padding" ng-hide="home_ctrl.has_joined"> <div class="list"> <label class="item item-input"> <input type="text" ng-model="home_ctrl.room" placeholder="Room Name"> </label> <label class="item item-input"> <input type="text" ng-model="home_ctrl.username" placeholder="User Name"> </label> </div> <button class="button button-positive button-block" ng-click="join(home_ctrl.username, home_ctrl.room)"> join </button> </div> 

Крестики-нолики:

 <div id="board"> <div class="row"> <div class="col" ng-click="placePiece(1)" id="1"></div> <div class="col" ng-click="placePiece(2)" id="2"></div> <div class="col" ng-click="placePiece(3)" id="3"></div> </div> <div class="row"> <div class="col" ng-click="placePiece(4)" id="4"></div> <div class="col" ng-click="placePiece(5)" id="5"></div> <div class="col" ng-click="placePiece(6)" id="6"></div> </div> <div class="row"> <div class="col" ng-click="placePiece(7)" id="7"></div> <div class="col" ng-click="placePiece(8)" id="8"></div> <div class="col" ng-click="placePiece(9)" id="9"></div> </div> </div> 

И игрок забивает:

 <div id="scores"> <div class="row"> <div class="col col-50 player"> <div class="player-name" ng-bind="player"></div> <div class="player-score" ng-bind="player_score"></div> </div> <div class="col col-50 player"> <div class="player-name" ng-bind="opponent"></div> <div class="player-score" ng-bind="opponent_score"></div> </div> </div> </div> 

стайлинг

Вот стили для приложения:

 #board .col { text-align: center; height: 100px; line-height: 100px; font-size: 30px; padding: 0; } #board .col:nth-child(2) { border-right: 1px solid; border-left: 1px solid; } #board .row:nth-child(2) .col { border-top: 1px solid; border-bottom: 1px solid; } .player { font-weight: bold; text-align: center; } .player-name { font-size: 18px; } .player-score { margin-top: 15px; font-size: 30px; } #scores { margin-top: 30px; } 

Запуск приложения

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

 ionic serve 

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

Если вы хотите проверить с другом, вы можете использовать Ngrok, чтобы выставить сервер Horizon в Интернет:

 ngrok http 8181 

Это создает URL-адрес, который можно использовать в качестве значения для host при подключении к серверу Horizon:

 const horizon = Horizon({host: 'xxxx.ngrok.io'}); 

Также измените ссылку на файл horizon.js в вашем файле index.html :

 <script src="http://xxxx.ngrok.io/horizon/horizon.js"></script> 

Чтобы создать мобильную версию, добавьте платформу в свой проект (для этого примера Android). Это предполагает, что вы уже установили Android SDK на свой компьютер.

 ionic platform add android 

Затем сгенерируйте файл .apk :

 ionic build android 

Затем вы можете отправить сгенерированный файл .apk своему другу, чтобы вы оба могли наслаждаться игрой. Или вы также можете играть в одиночку, если это ваша вещь.

Куда пойти отсюда

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

  • Создайте версию 4 × 4 или 5 × 5 : созданная вами версия 3 × 3 почти всегда приводит к тупику, особенно если оба игрока являются экспертами в игре в крестики-нолики.
  • Логика подсчета очков : Вы должны были сделать много циклов, чтобы получить очки для каждого игрока. Может быть, вы можете придумать лучший способ реализовать это.
  • Улучшение стилей . Текущий стиль прост и имитирует крестики-нолики, используемые для игры на бумаге.
  • Добавить анимацию : Вы можете добавить анимацию «скольжения вниз» для доски, когда пользователь присоединяется к комнате, или анимацию «отскока», когда игрок кладет свою фигуру на доску. Вы можете реализовать такие виды анимации, используя animate.css .
  • Добавьте социальный логин : для такого простого приложения это может оказаться излишним, но если вы хотите узнать, как работает аутентификация в Horizon, то это хороший пример. Аутентификация Horizon позволяет пользователям входить в систему через свои учетные записи Facebook, Twitter или Github.
  • Добавить функцию воспроизведения снова : Показать кнопку «Играть снова» после завершения игры. При нажатии он очищает игровое поле и счет, чтобы игроки могли снова играть.
  • Добавьте таблицу лидеров в реальном времени : добавьте таблицу лидеров, показывающую, кто выиграл больше игр, таблицу лидеров для комнат с наибольшим количеством перезапусков (если вы включили функцию воспроизведения снова).

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