新聞中心
簡介

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


咨詢
建站咨詢
