用Horizon搭建可擴(kuò)展的Javascript移動(dòng)應(yīng)用后端方案
譯文【51CTO.com快譯】
簡介
Horizon是一個(gè)著名的跨平臺可擴(kuò)展的后端框架,適用于構(gòu)建跨平臺基于JavaScript的移動(dòng)應(yīng)用程序,尤其是那些需要實(shí)時(shí)功能的應(yīng)用。這個(gè)框架是由來自RethinkDB產(chǎn)品的程序員開發(fā)的,因此使用RethinkDB作為默認(rèn)數(shù)據(jù)庫。如果你還不熟悉RethinkDB,那么你只需知識它是一個(gè)開放源碼的支持實(shí)時(shí)功能的數(shù)據(jù)庫(https://www.rethinkdb.com)。
Horizon框架公開一組客戶端API來允許你與底層數(shù)據(jù)庫進(jìn)行交互。這意味著,你不必編寫任何后端代碼。你要做的就是,搭建一個(gè)新的服務(wù)器,運(yùn)行它,Horizon將會(huì)自動(dòng)管理其他內(nèi)容。借助于Horizon,你可以輕松地實(shí)現(xiàn)實(shí)時(shí)連接的客戶端和服務(wù)器之間的數(shù)據(jù)同步。
如果你想要了解更多關(guān)于Horizon的消息,請查閱其 faq頁面(http://horizon.io/faq/)。
在本教程中,你要使用Icon和Horizon來協(xié)同開發(fā)一個(gè)Tic-Tac-Toe井字游戲。因此,閱讀本文的前提是假定你已經(jīng)了解Icon和Horizon,所以我不打算解釋程序中Icon相關(guān)的特定代碼。當(dāng)然,如果你想要一點(diǎn)有關(guān)Icon的背景知識的話,我建議你去查閱這個(gè)網(wǎng)址http://ionicframework.com/getting-started/。如果你想繼續(xù)閱讀本文內(nèi)容,那么請你先下載文章的示例工程源碼(https://github.com/anchetaWern/ionic-horizon-tictactoe)。
下圖給出的是本文示例應(yīng)用程序最終的結(jié)果快照。
安裝Horizon
RethinkDB用作Horizon的數(shù)據(jù)庫。因此,在安裝Horizon之前你需要先安裝RethinkDB。有關(guān)安裝RethinkDB的具體信息,你可以從網(wǎng)址https://www.rethinkdb.com/docs/install/處找到答案。
一旦安裝了RethinkDB,你就可以在終端程序中執(zhí)行以下命令通過npm工具來安裝Horizon:
npm install -g horizon
Horizon服務(wù)器開發(fā)
Horizon服務(wù)器用作應(yīng)用程序的后端。每當(dāng)應(yīng)用程序執(zhí)行代碼時(shí),它要與數(shù)據(jù)庫進(jìn)行通信。
您可以通過在您的終端執(zhí)行以下命令來創(chuàng)建一個(gè)新的Horizon服務(wù)器:
hz init tictactoe-server
這個(gè)命令將創(chuàng)建RethinkDB數(shù)據(jù)庫并提供Horizon所使用的文件。
一旦創(chuàng)建了服務(wù)器,您可以通過執(zhí)行以下命令運(yùn)行它:
hz serve --dev
在上面的命令中,指定-dev作為一個(gè)選項(xiàng)。這意味著,你想要運(yùn)行一個(gè)開發(fā)服務(wù)器。在開發(fā)服務(wù)器中會(huì)設(shè)置以下選項(xiàng):
--secure no:這意味著websocket和文件不會(huì)通過加密連接提供服務(wù)。
--permissions no:禁用權(quán)限約束。這意味著,任何客戶端都可以在數(shù)據(jù)庫中執(zhí)行任何他們想執(zhí)行的操作。Horizon的權(quán)限系統(tǒng)基于白名單。這意味著,默認(rèn)情況下,所有用戶都沒有權(quán)限來做任何事情。你必須顯式地指定允許哪些操作。
--auto-create-collection yes:在首次使用時(shí)自動(dòng)創(chuàng)建一個(gè)集合。在Horizon中,集合相當(dāng)于關(guān)系數(shù)據(jù)庫中的表。此選項(xiàng)設(shè)置為true意味著,每次客戶端使用一個(gè)新的集合,它都會(huì)被自動(dòng)創(chuàng)建。
--auto-create-index yes:在首次使用中自動(dòng)創(chuàng)建索引。
--start-rethinkdb yes:在當(dāng)前目錄中自動(dòng)啟動(dòng)RethinkDB的一個(gè)新實(shí)例。
--allow-unauthenticated yes:允許未經(jīng)身份驗(yàn)證的用戶來執(zhí)行數(shù)據(jù)庫操作。
--allow-anonymous yes:允許匿名用戶執(zhí)行數(shù)據(jù)庫操作。
--serve-static ./dist:啟用靜態(tài)文件服務(wù)。如果你想要在瀏覽器中測試與Horizon API的交互時(shí),這是很有用的。Horizon服務(wù)器默認(rèn)運(yùn)行在端口8181,所以你可以通過訪問地址http://localhost:8181來訪問服務(wù)器。
【注意】--dev選項(xiàng)永遠(yuǎn)不要用于生產(chǎn)環(huán)境下,因?yàn)樗鼤?huì)打開大量的易于被攻擊者能夠利用的漏洞。
構(gòu)建Ionic應(yīng)用程序
現(xiàn)在,我們已經(jīng)作好了充分準(zhǔn)備。接下來,我們著手創(chuàng)建一個(gè)Ionic程序框架,命令如下:
ionic start tictactoe blank
安裝Chance.js
接下來,您需要安裝chance.js,這是一個(gè)JavaScript實(shí)用程序庫,用于生成隨機(jī)數(shù)據(jù)。在本應(yīng)用程序中,我們使用它來為玩家生成一個(gè)唯一的ID。你可以通過bower工具并使用下面的命令來安裝chance.js:
bower install chance
創(chuàng)建index.html
現(xiàn)在,打開文件www/index.html,并把其內(nèi)容修改為如下:
- <!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>
上面的代碼大部分來自于Icon空白向?qū)0迳傻臉影宕a。現(xiàn)在,我們來添加對chance.js腳本的引用:
- <script src="lib/chance/dist/chance.min.js"></script>
Horizon服務(wù)器將自動(dòng)提供Horizon腳本服務(wù),代碼如下:
- <script src="http://127.0.0.1:8181/horizon/horizon.js"></script>
【注意】如果你以后想部署這些內(nèi)容的話,你必須修改URL。
接下來,主應(yīng)用程序邏輯位于下面這個(gè)腳本文件中:
- <script src="js/controllers/HomeController.js"></script>
編寫主程序app.js
文件app.js是運(yùn)行初始化應(yīng)用程序代碼的地方。下面,需要打開文件www/js/app.js并把如下內(nèi)容添加到run函數(shù)的下面:
- .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');
- });
這將為默認(rèn)的應(yīng)用程序頁設(shè)置一個(gè)路由。此路由將指定頁面所使用的模板和可以訪問它的URL。
開發(fā)控制器程序HomeController.Js
現(xiàn)在,我們在路徑www/js/controllers下創(chuàng)建一個(gè)控制器文件HomeController.js,并修改其代碼如下:
- (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];
- });
- }
- }
- })();
現(xiàn)在,分析一下上面代碼。首先,設(shè)置默認(rèn)狀態(tài)。其中,has_joined變量用于是否玩家已經(jīng)進(jìn)入某個(gè)房間。其次,ready變量用于確定是否用戶已經(jīng)連接到Horizon服務(wù)器。當(dāng)這個(gè)變量值為false時(shí),還不能向用戶顯示應(yīng)用程序的界面。
- $scope.has_joined = false;
- $scope.ready = false;
連接到服務(wù)器的代碼如下:
- const horizon = Horizon({host: 'localhost:8181'});
- horizon.onReady(function(){
- $scope.$apply(function(){
- $scope.ready = true;
- });
- });
- horizon.connect(); //connect to the server
如我前面所提到的,Horizon服務(wù)器默認(rèn)使用的是8181端口。這正是我們?yōu)槭裁词褂胠ocal:8181作為端口的原因。如果你連接到一個(gè)遠(yuǎn)程服務(wù)器,這應(yīng)該對應(yīng)于分配給服務(wù)器的IP地址或者域名。當(dāng)用戶連接到服務(wù)器時(shí),onReady事件將會(huì)觸發(fā)。正是在此時(shí),我們把ready設(shè)置為true,這樣就可以向用戶顯示程序的UI部分了。
- horizon.onReady(function(){
- $scope.$apply(function(){
- $scope.ready = true;
- });
- });
進(jìn)入房間
每當(dāng)用戶點(diǎn)擊Join按鈕時(shí),將執(zhí)行join函數(shù):
- $scope.join = function(username, room){
- ...
- };
在此函數(shù)內(nèi)部,連接到一個(gè)稱為tictactoe的集合。
【注意】因?yàn)槲覀兲幱陂_發(fā)模式下;所以,如果集合不存在的話,系統(tǒng)將自動(dòng)為你創(chuàng)建。
- me.room = horizon('tictactoe');
接下來,生成一個(gè)ID,并把它設(shè)置為當(dāng)前用戶的ID:
- var id = chance.integer({min: 10000, max: 999999});
- me.id = id;
接下來,設(shè)置玩家用戶名和默認(rèn)的玩家得分。
- $scope.player = username;
- $scope.player_score = 0;
【注意】這兩個(gè)變量都被綁定到模板中;所以,你可以隨時(shí)顯示與更新它們。
接下來,進(jìn)行文檔查詢,查詢條件是:room屬性為當(dāng)前房間且type屬性為user。千萬不要把這種查詢與subscribe函數(shù)弄混,在這里我們并不監(jiān)聽數(shù)據(jù)變化的。而且,這里還要使用fetch函數(shù);這意味著,只有在用戶進(jìn)入一個(gè)房間時(shí)才執(zhí)行該操作。相關(guān)代碼如下:
- me.room.findAll({room: room, type: 'user'}).fetch().subscribe(function(row){
- ...
- });
一旦結(jié)果返回,即檢查用戶個(gè)數(shù)。當(dāng)然,井字游戲只能由兩個(gè)玩家玩,所以,如果用戶想加入一個(gè)已經(jīng)有兩名玩家的房間的話,系統(tǒng)會(huì)向他們發(fā)出警報(bào)。
- var user_count = row.length;
- if(user_count == 2){
- alert('Sorry, room is already full.');
- }else{
- ...
- }
上面代碼中的else語句將繼續(xù)處理接受用戶的邏輯,即根據(jù)當(dāng)前用戶數(shù)確定將被分配給用戶的卡片。第一個(gè)加入該房間的人得到"X"卡片,而第二個(gè)人得到"O"卡片。
- me.piece = 'X';
- if(user_count == 1){
- me.piece = 'O';
- }
一旦你選定了卡片,就把新用戶存儲到集合中,并把has_joined開關(guān)值取反,從而顯示井字棋盤。
- me.room.store({
- id: id,
- room: room,
- type: 'user',
- name: username,
- piece: me.piece
- });
- $scope.has_joined = true;
接下來,偵聽集合中的變化。這次,不是通過fetch方式,而是使用watch方式。具體地說,每當(dāng)添加新文檔或更新(或刪除)匹配查詢的現(xiàn)有文檔時(shí),都要執(zhí)行回調(diào)函數(shù)。回調(diào)函數(shù)執(zhí)行時(shí),循環(huán)遍歷所有的結(jié)果并設(shè)置對手的詳細(xì)信息——如果該文檔的用戶ID與當(dāng)前用戶的ID不匹配的話。本程序中正是通過這種方式來向當(dāng)前用戶顯示他們的對手是誰。
- 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事件,該事件每當(dāng)玩家把他們的卡片放到棋盤上從而這導(dǎo)致文檔變化時(shí)就觸發(fā)一次。如果發(fā)生這種情況,則遍歷所有的移動(dòng)動(dòng)作并將文本添加到相應(yīng)的格子。從現(xiàn)在開始,代碼中將使用“block”一詞來表示棋盤上的每一個(gè)格子。
添加的文本對應(yīng)于每個(gè)用戶所使用的卡片;此外,代碼中還將類名替換為“col done”。其中,col相應(yīng)于Ionic編程中網(wǎng)格實(shí)現(xiàn)類,而done是用于表示一個(gè)特定塊上已經(jīng)已經(jīng)有一個(gè)卡片的類。如果用戶還能將卡片放在格子上,我們就使用這種辦法來檢查。在更新棋盤用戶界面后,通過調(diào)用updateScores函數(shù)(將在以后添加這個(gè)函數(shù))來更新玩家的成績。
- 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);
- }
- );
放置卡片
每當(dāng)用戶點(diǎn)擊棋盤上的任何一格時(shí)都要調(diào)用placePiece函數(shù),同時(shí)要提供對應(yīng)格子的ID值作為此函數(shù)的參數(shù)。這允許我們隨心所欲地操縱游戲格子。在本程序中,使用此函數(shù)來檢查某個(gè)游戲格子是否屬于done類型。如果沒有done標(biāo)志,則創(chuàng)建一個(gè)新的移動(dòng),并顯示當(dāng)前房間、格子ID值及對應(yīng)的卡片。
- $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
- });
- }
- };
更新玩家得分
為了更新玩家得分,需要構(gòu)建一個(gè)包含可能的獲勝組合的數(shù)組,如下所示:
- 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]對應(yīng)于第一行,[1, 2, 3]對應(yīng)于第一列。只要相應(yīng)的數(shù)字存在,順序是無關(guān)緊要的。下面的圖形有助于你更直觀地了解這一點(diǎn)。
接下來,需要初始化每個(gè)單獨(dú)卡片的得分并循環(huán)遍歷每個(gè)可能的組合。對于每一次循環(huán)遍歷,初始化已經(jīng)放到棋盤上的卡片總數(shù)。然后針對每一種可能的組合進(jìn)行循環(huán)遍歷。使用id來檢查是否相應(yīng)的格子上已經(jīng)放了卡片。如果已經(jīng)有了卡片,則取得實(shí)際卡片并把卡片總數(shù)加1。在循環(huán)結(jié)束后,檢查是否卡片總數(shù)等于3。如果卡片總數(shù)等于3,則增加該卡片得分?jǐn)?shù),直到遍歷完所有可能的組合。一旦完成,更新當(dāng)前玩家和對手的得分值。
- 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];
- });
創(chuàng)建主模板文件
現(xiàn)在,在目錄www/templates下創(chuàng)建一個(gè)模板文件home.html,并添加如下代碼:
- <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>
現(xiàn)在,我們來分析一下上面的代碼。首先,創(chuàng)建了一個(gè)總的包裝器,在用戶連接到Horizon服務(wù)器前這部分內(nèi)容是不顯示的:
- <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>
井字棋棋盤的設(shè)計(jì)相關(guān)代碼如下:
- <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>
玩家得分部分對應(yīng)的代碼如下:
- <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>
編寫樣式文件
下面給出客戶端應(yīng)用程序的樣式定義:
- #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;
- }
運(yùn)行應(yīng)用程序
現(xiàn)在,你可以通過執(zhí)行應(yīng)用程序根目錄下的如下命令在你的瀏覽器中測試應(yīng)用程序:
- ionic serve
這樣啟動(dòng)的服務(wù)器端將服務(wù)于本地項(xiàng)目并在你的默認(rèn)瀏覽器中打開一個(gè)新的選項(xiàng)卡。
如果你想要和朋友一起測試的話,你可以使用Ngrok把Horizon服務(wù)器發(fā)布到互聯(lián)網(wǎng)上,命令如下:
- ngrok http 8181
這個(gè)命令將生成一個(gè)URL,當(dāng)你連接到Horizon服務(wù)器時(shí)可以用作主機(jī)地址:
- const horizon = Horizon({host: 'xxxx.ngrok.io'});
除此之外,還要在index.html文件中改變到horizon.js文件的引用:
- <script src="http://xxxx.ngrok.io/horizon/horizon.js"></script>
若要?jiǎng)?chuàng)建程序的移動(dòng)版本,需要在你的項(xiàng)目中添加一個(gè)平臺(例如,安卓系統(tǒng))。這假定你已經(jīng)在自己的計(jì)算機(jī)上安裝了Android SDK。
- ionic platform add android
接下來,我們生成.apk文件,命令如下:
- ionic build android
到此,你可以把這個(gè).apk文件發(fā)送給你的朋友來一起把玩這個(gè)游戲。當(dāng)然,你也可以自己玩這個(gè)游戲,這全是你自己的事了。
小結(jié)
在本教程中,你開發(fā)的僅僅是一個(gè)再簡單不過的應(yīng)用程序;因此,還有很多方面加以適當(dāng)改進(jìn)的話,效果會(huì)更好。下面列舉的是供你參考改進(jìn)的幾個(gè)內(nèi)容,把這些當(dāng)成你的技能作業(yè)好了。
再開發(fā)一個(gè)4×4或5×5的版本:目前你開發(fā)出的3×3版本幾乎總是會(huì)導(dǎo)致僵局,尤其是如果兩名玩井字游戲的玩家都是專家級的話。
得分邏輯:你不得不通過大量的循環(huán)來取得玩家得分。也許你可以想出更好的方案來實(shí)現(xiàn)。
美化游戲風(fēng)格:當(dāng)前游戲的風(fēng)格十分平實(shí),其實(shí)它模擬的是適用于在紙上玩的井字游戲。
添加動(dòng)畫:當(dāng)用戶加入一個(gè)房間時(shí),你可以嘗試為棋盤添加“滑落”動(dòng)畫效果;或者當(dāng)玩家把卡片放到棋盤上時(shí)實(shí)現(xiàn)“彈出”動(dòng)畫效果。您可以使用animate.css文件來實(shí)現(xiàn)這類動(dòng)畫。
添加SNS登錄支持:在這么簡單的一個(gè)應(yīng)用中添加SNS功能恐怕要求有點(diǎn)過了,但如果你想要了解如何在Horizon框架中實(shí)現(xiàn)身份驗(yàn)證工作原理,這倒是一個(gè)相當(dāng)不錯(cuò)的鍛煉機(jī)會(huì)。使用Horizon認(rèn)證,你可以讓用戶登錄其Facebook、Twitter或Github帳戶。
添加再玩一次功能:游戲完畢后你可以嘗試添加一個(gè)“Play Again”按鈕。按下這個(gè)按鈕時(shí),系統(tǒng)將清除排行榜和得分,以便玩家可以再玩一次。
添加實(shí)時(shí)排行榜功能:添加比賽排行榜來顯示誰贏了比賽。
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請注明原文譯者和出處為51CTO.com】