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.
- Добавить функцию воспроизведения снова : Показать кнопку «Играть снова» после завершения игры. При нажатии он очищает игровое поле и счет, чтобы игроки могли снова играть.
- Добавьте таблицу лидеров в реальном времени : добавьте таблицу лидеров, показывающую, кто выиграл больше игр, таблицу лидеров для комнат с наибольшим количеством перезапусков (если вы включили функцию воспроизведения снова).
Если у вас есть какие-либо вопросы, комментарии или отличные идеи по улучшению приложения, пожалуйста, дайте мне знать в комментариях ниже .