用Horizon搭建可扩展的Javascript移动应用后端方案

简介

Horizon是一个著名的跨平台可扩展的后端框架,适用于构建跨平台基于JavaScript的移动应用程序,尤其是那些需要实时功能的应用。这个框架是由来自RethinkDB产品的程序员开发的,因此使用RethinkDB作为默认数据库。如果你还不熟悉RethinkDB,那么你只需知识它是一个开放源码的支持实时功能的数据库。

Horizon框架公开一组客户端API来允许你与底层数据库进行交互。这意味着,你不必编写任何后端代码。你要做的就是,搭建一个新的服务器,运行它,Horizon将会自动管理其他内容。借助于Horizon,你可以轻松地实现实时连接的客户端和服务器之间的数据同步。

如果你想要了解更多关于Horizon的消息,请查阅其 faq页面(http://horizon.io/faq/)。

在本教程中,你要使用Icon和Horizon来协同开发一个Tic-Tac-Toe井字游戏。因此,阅读本文的前提是假定你已经了解Icon和Horizon,所以我不打算解释程序中Icon相关的特定代码。当然,如果你想要一点有关Icon的背景知识的话,我建议你去查阅这个网址http://ionicframework.com/getting-started/。如果你想继续阅读本文内容,那么请你先下载文章的示例工程源码。

下图给出的是本文示例应用程序最终的结果快照。

安装Horizon

RethinkDB用作Horizon的数据库。因此,在安装Horizon之前你需要先安装RethinkDB。有关安装RethinkDB的具体信息,处找到答案。

一旦安装了RethinkDB,你就可以在终端程序中执行以下命令通过npm工具来安装Horizon:

npm install -g horizon

Horizon服务器开发

Horizon服务器用作应用程序的后端。每当应用程序执行代码时,它要与数据库进行通信。

您可以通过在您的终端执行以下命令来创建一个新的Horizon服务器:

hz init tictactoe-server

这个命令将创建RethinkDB数据库并提供Horizon所使用的文件。

一旦创建了服务器,您可以通过执行以下命令运行它:

hz serve --dev

在上面的命令中,指定-dev作为一个选项。这意味着,你想要运行一个开发服务器。在开发服务器中会设置以下选项:

--secure no:这意味着websocket和文件不会通过加密连接提供服务。

--permissions no:禁用权限约束。这意味着,任何客户端都可以在数据库中执行任何他们想执行的操作。Horizon的权限系统基于白名单。这意味着,默认情况下,所有用户都没有权限来做任何事情。你必须显式地指定允许哪些操作。

--auto-create-collection yes:在首次使用时自动创建一个集合。在Horizon中,集合相当于关系数据库中的表。此选项设置为true意味着,每次客户端使用一个新的集合,它都会被自动创建。

--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选项永远不要用于生产环境下,因为它会打开大量的易于被攻击者能够利用的漏洞。

构建Ionic应用程序

现在,我们已经作好了充分准备。接下来,我们着手创建一个Ionic程序框架,命令如下:

ionic start tictactoe blank

安装Chance.js

接下来,您需要安装chance.js,这是一个JavaScript实用程序库,用于生成随机数据。在本应用程序中,我们使用它来为玩家生成一个唯一的ID。你可以通过bower工具并使用下面的命令来安装chance.js:

bower install chance

创建index.html

现在,打开文件www/index.html,并把其内容修改为如下:

 
 
 
 

上面的代码大部分来自于Icon空白向导模板生成的样板代码。现在,我们来添加对chance.js脚本的引用:

 
 
 
 

Horizon服务器将自动提供Horizon脚本服务,代码如下:

 
 
 
 

【注意】如果你以后想部署这些内容的话,你必须修改URL。

接下来,主应用程序逻辑位于下面这个脚本文件中:

 
 
 
 

编写主程序app.js

文件app.js是运行初始化应用程序代码的地方。下面,需要打开文件www/js/app.js并把如下内容添加到run函数的下面:

 
 
 
 
  1. .config(function($stateProvider, $urlRouterProvider) {
  2. $stateProvider
  3. .state('home', {
  4. cache: false,
  5. url: '/home',
  6. templateUrl: 'templates/home.html'
  7. });
  8. // if none of the above states are matched, use this as the fallback
  9. $urlRouterProvider.otherwise('/home');
  10. });

这将为默认的应用程序页设置一个路由。此路由将指定页面所使用的模板和可以访问它的URL。

开发控制器程序HomeController.Js

现在,我们在路径www/js/controllers下创建一个控制器文件HomeController.js,并修改其代码如下:

 
 
 
 
  1. (function(){
  2. angular.module('starter')
  3. .controller('HomeController', ['$scope', HomeController]);
  4. function HomeController($scope){
  5. var me = this;
  6. $scope.has_joined = false;
  7. $scope.ready = false;
  8. const horizon = Horizon({host: 'localhost:8181'});
  9. horizon.onReady(function(){
  10. $scope.$apply(function(){
  11. $scope.ready = true;
  12. });
  13. });
  14. horizon.connect();
  15. $scope.join = function(username, room){
  16. me.room = horizon('tictactoe');
  17. var id = chance.integer({min: 10000, max: 999999});
  18. me.id = id;
  19. $scope.player = username;
  20. $scope.player_score = 0;
  21. me.room.findAll({room: room, type: 'user'}).fetch().subscribe(function(row){
  22. var user_count = row.length;
  23. if(user_count == 2){
  24. alert('Sorry, room is already full.');
  25. }else{
  26. me.piece = 'X';
  27. if(user_count == 1){
  28. me.piece = 'O';
  29. }
  30. me.room.store({
  31. id: id,
  32. room: room,
  33. type: 'user',
  34. name: username,
  35. piece: me.piece
  36. });
  37. $scope.has_joined = true;
  38. me.room.findAll({room: room, type: 'user'}).watch().subscribe(
  39. function(users){
  40. users.forEach(function(user){
  41. if(user.id != me.id){
  42. $scope.$apply(function(){
  43. $scope.opponent = user.name;
  44. $scope.opponent_piece = user.piece;
  45. $scope.opponent_score = 0;
  46. });
  47. }
  48. });
  49. },
  50. function(err){
  51. console.log(err);
  52. }
  53. );
  54. me.room.findAll({room: room, type: 'move'}).watch().subscribe(
  55. function(moves){
  56. moves.forEach(function(item){
  57. var block = document.getElementById(item.block);
  58. block.innerHTML = item.piece;
  59. block.className = "col done";
  60. });
  61. me.updateScores();
  62. },
  63. function(err){
  64. console.log(err);
  65. }
  66. );
  67. }
  68. });
  69. }
  70. $scope.placePiece = function(id){
  71. var block = document.getElementById(id);
  72. if(!angular.element(block).hasClass('done')){
  73. me.room.store({
  74. type: 'move',
  75. room: me.room_name,
  76. block: id,
  77. piece: me.piece
  78. });
  79. }
  80. };
  81. me.updateScores = function(){
  82. const possible_combinations = [
  83. [1, 4, 7],
  84. [2, 5, 8],
  85. [3, 2, 1],
  86. [4, 5, 6],
  87. [3, 6, 9],
  88. [7, 8, 9],
  89. [1, 5, 9],
  90. [3, 5, 7]
  91. ];
  92. var scores = {'X': 0, 'O': 0};
  93. possible_combinations.forEach(function(row, row_index){
  94. var pieces = {'X' : 0, 'O': 0};
  95. row.forEach(function(id, item_index){
  96. var block = document.getElementById(id);
  97. if(angular.element(block).hasClass('done')){
  98. var piece = block.innerHTML;
  99. pieces[piece] += 1;
  100. }
  101. });
  102. if(pieces['X'] == 3){
  103. scores['X'] += 1;
  104. }else if(pieces['O'] == 3){
  105. scores['O'] += 1;
  106. }
  107. });
  108. $scope.$apply(function(){
  109. $scope.player_score = scores[me.piece];
  110. $scope.opponent_score = scores[$scope.opponent_piece];
  111. });
  112. }
  113. }
  114. })();

现在,分析一下上面代码。首先,设置默认状态。其中,has_joined变量用于是否玩家已经进入某个房间。其次,ready变量用于确定是否用户已经连接到Horizon服务器。当这个变量值为false时,还不能向用户显示应用程序的界面。

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

连接到服务器的代码如下:

 
 
 
 
  1. const horizon = Horizon({host: 'localhost:8181'});
  2. horizon.onReady(function(){
  3. $scope.$apply(function(){
  4. $scope.ready = true;
  5. });
  6. });
  7. horizon.connect(); //connect to the server

如我前面所提到的,Horizon服务器默认使用的是8181端口。这正是我们为什么使用local:8181作为端口的原因。如果你连接到一个远程服务器,这应该对应于分配给服务器的IP地址或者域名。当用户连接到服务器时,onReady事件将会触发。正是在此时,我们把ready设置为true,这样就可以向用户显示程序的UI部分了。

 
 
 
 
  1. horizon.onReady(function(){
  2. $scope.$apply(function(){
  3. $scope.ready = true;
  4. });
  5. });

进入房间

每当用户点击Join按钮时,将执行join函数:

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

在此函数内部,连接到一个称为tictactoe的集合。

【注意】因为我们处于开发模式下;所以,如果集合不存在的话,系统将自动为你创建。

 
 
 
 
  1. me.room = horizon('tictactoe');

接下来,生成一个ID,并把它设置为当前用户的ID:

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

接下来,设置玩家用户名和默认的玩家得分。

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

【注意】这两个变量都被绑定到模板中;所以,你可以随时显示与更新它们。

接下来,进行文档查询,查询条件是:room属性为当前房间且type属性为user。千万不要把这种查询与subscribe函数弄混,在这里我们并不监听数据变化的。而且,这里还要使用fetch函数;这意味着,只有在用户进入一个房间时才执行该操作。相关代码如下:

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

一旦结果返回,即检查用户个数。当然,井字游戏只能由两个玩家玩,所以,如果用户想加入一个已经有两名玩家的房间的话,系统会向他们发出警报。

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

上面代码中的else语句将继续处理接受用户的逻辑,即根据当前用户数确定将被分配给用户的卡片。第一个加入该房间的人得到"X"卡片,而第二个人得到"O"卡片。

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

一旦你选定了卡片,就把新用户存储到集合中,并把has_joined开关值取反,从而显示井字棋盘。

 
 
 
 
  1. me.room.store({
  2. id: id,
  3. room: room,
  4. type: 'user',
  5. name: username,
  6. piece: me.piece
  7. });
  8. $scope.has_joined = true;

接下来,侦听集合中的变化。这次,不是通过fetch方式,而是使用watch方式。具体地说,每当添加新文档或更新(或删除)匹配查询的现有文档时,都要执行回调函数。回调函数执行时,循环遍历所有的结果并设置对手的详细信息——如果该文档的用户ID与当前用户的ID不匹配的话。本程序中正是通过这种方式来向当前用户显示他们的对手是谁。

 
 
 
 
  1. me.room.findAll({room: room, type: 'user'}).watch().subscribe(
  2. function(users){
  3. users.forEach(function(user){
  4. if(user.id != me.id){
  5. $scope.$apply(function(){
  6. $scope.opponent = user.name;
  7. $scope.opponent_piece = user.piece;
  8. $scope.opponent_score = 0;
  9. });
  10. }
  11. });
  12. },
  13. function(err){
  14. console.log(err);
  15. }
  16. );

接下来要订阅move事件,该事件每当玩家把他们的卡片放到棋盘上从而这导致文档变化时就触发一次。如果发生这种情况,则遍历所有的移动动作并将文本添加到相应的格子。从现在开始,代码中将使用“block”一词来表示棋盘上的每一个格子。

添加的文本对应于每个用户所使用的卡片;此外,代码中还将类名替换为“col done”。其中,col相应于Ionic编程中网格实现类,而done是用于表示一个特定块上已经已经有一个卡片的类。如果用户还能将卡片放在格子上,我们就使用这种办法来检查。在更新棋盘用户界面后,通过调用updateScores函数(将在以后添加这个函数)来更新玩家的成绩。

 
 
 
 
  1. me.room.findAll({room: room, type: 'move'}).watch().subscribe(
  2. function(moves){
  3. moves.forEach(function(item){
  4. var block = document.getElementById(item.block);
  5. block.innerHTML = item.piece;
  6. block.className = "col done";
  7. });
  8. me.updateScores();
  9. },
  10. function(err){
  11. console.log(err);
  12. }
  13. );

放置卡片

每当用户点击棋盘上的任何一格时都要调用placePiece函数,同时要提供对应格子的ID值作为此函数的参数。这允许我们随心所欲地操纵游戏格子。在本程序中,使用此函数来检查某个游戏格子是否属于done类型。如果没有done标志,则创建一个新的移动,并显示当前房间、格子ID值及对应的卡片。

 
 
 
 
  1. $scope.placePiece = function(id){
  2. var block = document.getElementById(id);
  3. if(!angular.element(block).hasClass('done')){
  4. me.room.store({
  5. type: 'move',
  6. room: me.room_name,
  7. block: id,
  8. piece: me.piece
  9. });
  10. }
  11. };

更新玩家得分

为了更新玩家得分,需要构建一个包含可能的获胜组合的数组,如下所示:

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

在这段代码中,[1, 4, 7]对应于第一行,[1, 2, 3]对应于第一列。只要相应的数字存在,顺序是无关紧要的。下面的图形有助于你更直观地了解这一点。

接下来,需要初始化每个单独卡片的得分并循环遍历每个可能的组合。对于每一次循环遍历,初始化已经放到棋盘上的卡片总数。然后针对每一种可能的组合进行循环遍历。使用id来检查是否相应的格子上已经放了卡片。如果已经有了卡片,则取得实际卡片并把卡片总数加1。在循环结束后,检查是否卡片总数等于3。如果卡片总数等于3,则增加该卡片得分数,直到遍历完所有可能的组合。一旦完成,更新当前玩家和对手的得分值。

 
 
 
 
  1. var scores = {'X': 0, 'O': 0};
  2. possible_combinations.forEach(function(row, row_index){
  3. var pieces = {'X' : 0, 'O': 0};
  4. row.forEach(function(id, item_index){
  5. var block = document.getElementById(id);
  6. if(angular.element(block).hasClass('done')){ //check if there's already a piece
  7. var piece = block.innerHTML;
  8. pieces[piece] += 1;
  9. }
  10. });
  11. if(pieces['X'] == 3){
  12. scores['X'] += 1;
  13. }else if(pieces['O'] == 3){
  14. scores['O'] += 1;
  15. }
  16. });
  17. //update current player and opponent score
  18. $scope.$apply(function(){
  19. $scope.player_score = scores[me.piece];
  20. $scope.opponent_score = scores[$scope.opponent_piece];
  21. });

创建主模板文件

现在,在目录www/templates下创建一个模板文件home.html,并添加如下代码:

 
 
 
 
  1. Ionic Horizon Tic Tac Toe
  • join
  • 现在,我们来分析一下上面的代码。首先,创建了一个总的包装器,在用户连接到Horizon服务器前这部分内容是不显示的:

     
     
     
     
    1. ...

    加入游戏房间的表单代码如下:

     
     
     
     
    1. join

    井字棋棋盘的设计相关代码如下:

     
     
     
     

    玩家得分部分对应的代码如下:

     
     
     
     

    编写样式文件

    下面给出客户端应用程序的样式定义:

     
     
     
     
    1. #board .col {
    2. text-align: center;
    3. height: 100px;
    4. line-height: 100px;
    5. font-size: 30px;
    6. padding: 0;
    7. }
    8. #board .col:nth-child(2) {
    9. border-right: 1px solid;
    10. border-left: 1px solid;
    11. }
    12. #board .row:nth-child(2) .col {
    13. border-top: 1px solid;
    14. border-bottom: 1px solid;
    15. }
    16. .player {
    17. font-weight: bold;
    18. text-align: center;
    19. }
    20. .player-name {
    21. font-size: 18px;
    22. }
    23. .player-score {
    24. margin-top: 15px;
    25. font-size: 30px;
    26. }
    27. #scores {
    28. margin-top: 30px;
    29. }

    运行应用程序

    现在,你可以通过执行应用程序根目录下的如下命令在你的浏览器中测试应用程序:

     
     
     
     
    1. ionic serve

    这样启动的服务器端将服务于本地项目并在你的默认浏览器中打开一个新的选项卡。

    如果你想要和朋友一起测试的话,你可以使用Ngrok把Horizon服务器发布到互联网上,命令如下:

     
     
     
     
    1. ngrok http 8181

    这个命令将生成一个URL,当你连接到Horizon服务器时可以用作主机地址:

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

    除此之外,还要在index.html文件中改变到horizon.js文件的引用:

     
     
     
     

    若要创建程序的移动版本,需要在你的项目中添加一个平台(例如,安卓系统)。这假定你已经在自己的计算机

    名称栏目:用Horizon搭建可扩展的Javascript移动应用后端方案
    分享路径:http://www.shufengxianlan.com/qtweb/news17/319367.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

    猜你还喜欢下面的内容

    App开发知识

    同城分类信息