直到几年前,要核算您刚吃的三明治中含有多少卡路里,还需求依托猜想和查看包装。现在,相同的信息可从许多在线养分数据库中获取,这使得盯梢食物摄入量变得更简略。
本文将介绍怎么创立一个在线卡路里计数器,它使得用户能够:
- 按称号查找食物,经过一个 API 从在线养分数据库 Nutritionix 中检索成果。
- 运用一个 PHP/AngularJS 运用程序,将选定的食物分组在一同来创立用餐记载,并将这些记载与它们的卡路里计数一同保存在一个 MySQL 数据库中。
- 检索他们今日、最近七天和最近 30 天耗费的卡路里总量。
- 从平板电脑和智能电话等移动设备拜访该运用程序。
在客户端,我运用 jQuery Mobile 为该运用程序创立了一个移动友爱的用户界面,并运用 AngularJS 来启用该运用程序的一些交互式特性。在上,我运用 Slim(一个 PHP 微型结构)来操控与 Nutritionix API 的交互,并在 MySQL 中保存和检索数据。
最终一部分将会介绍怎么将运用程序布置到 Bluemix 云,该云供给了一种用于运用程序布置的、可扩展、健全的根底架构,可保证用户具有全天候的拜访才能。
听起来是否很风趣?让咱们开端吧!
完结该运用程序的先决条件
- 根本了解 jQuery Mobile、AngularJS、PHP、MySQL,以及 Apache 或 nginx
- 一个具有外寄邮件(outgoing mail server)的本地 PHP/MySQL 开发环境
- 一个 Bluemix 帐户
- 一个 Nutritionix API 帐户
- Composer(PHP 依靠项办理器)
- CloudFoundry 指令行东西
- 一个文本编辑器或 IDE
第 1 步. 设置运用程序数据库
运用下面这个代码清单(包括一个 MySQL 表界说和示例数据)来设置运用程序数据库。
- 假如仅在本地进行开发和布置,那么能够运用此代码初始化一个 MySQL 数据库表,让运用程序衔接到该表。
- 假如在 Bluemix 进步行布置,那么能够暂时越过此进程;在初始化 Bluemix 上的一个 MySQL 服务实例并绑定它之后,我会在 第 8 步 介绍怎么布置它。
CREATE TABLE meals ( id int(11) NOT NULL AUTO_INCREMENT, uid varchar(255) NOT NULL, calories decimal(10,2) NOT NULL, rdate timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ip varchar(20) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 CREATE TABLE users ( id int(11) NOT NULL AUTO_INCREMENT, email varchar(255) NOT NULL, `password` varchar(255) NOT NULL, code varchar(255) DEFAULT NULL, `status` int(11) NOT NULL, rdate timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ip varchar(20) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第 2 步. 装置 Slim
下载并设置 Slim 微型结构。为什么挑选 Slim?Slim 包括一个杂乱的 URL 路由器,并且支撑 Flash 音讯、加密 cookie 和中心件。它也简略了解和运用,并且具有优异的文档。
我运用 Composer(PHP 依靠项办理器)下载和设置 Slim。除了 Slim 之外,我还增加了针对 PHP 的 SendGrid 客户端库。(进一步了解 SendGrid 和为什么需求它。)下面这个代码清单是 Composer 配备文件。将此文件保存到 $APP_ROOT/composer.json(其间 $APP_ROOT 指您的作业目录)。
{ "require": { "slim/slim": "2.*", "sendgrid/sendgrid": "2.0.5" } }
现在能够运用 Composer 和以下指令装置 Slim:
shell> php composer.phar install
要使该运用程序更简略拜访,还能够在开发环境中界说一个新虚拟主机,并将它的文档根指向 $APP_ROOT。引荐履行这一步(可是可选的),由于这会为 Bluemix 上的方针布置环境创立一个很挨近的副本。
要在 Apache 中为该运用程序设置一个命名虚拟主机,能够翻开 Apache 配备文件(httpd.conf 或 httpd-vhosts.conf)并增加以下代码:
NameVirtualHost 127.0.0.1 <VirtualHost 127.0.0.1> DocumentRoot "/var/www/calories" ServerName calories.localhost </VirtualHost>
要在 nginx 中为该运用程序设置一个命名虚拟主机,能够翻开 nginx 配备文件 (nginx.conf) 并增加以下代码:
server { server_name calories.localhost; root /var/www/calories; try_files $uri /index.php; location ~ \.php$ { try_files $uri =404; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # assumes you are using php-fcgi fastcgi_pass 127.0.0.1:90; } }
这些代码界说了一个新的虚拟主机 http://calories.localhost/,该主机的文档根对应于 $APP_ROOT(请记住更新它来反映您自己的本地设置)。重新启动 Web ,以便激活这些新设置。您或许需求更新网络的本地 DNS 来指示运用这个新主机。
第 3 步. 了解 Nutritionix API
与其他许多 Web API 相同,Nutritionix API 经过 HTTP 进行作业,要求将 HTTP 恳求发送到指定的端点。收到这个恳求后,API 会运用包括一个恳求的数据的 JSON 源来回复查询。然后,它能够运用一种端编程言语(比方 PHP 或 Perl)或一个客户端东西包(比方 jQuery 或 AngularJS)来解析此数据,并从中提取内容,以便将它们集成到一个网页中。
注册一个 Nutritionix API 帐户并取得有用的 appId 和 appKey 后,能够运用该 API 作为测验渠道,查找与词汇 “chicken” 匹配的食物。这个免费的开发人员帐户仅支撑每天查找 500 次(但您能够向 API 团队发送电子邮件来恳求进步这一约束)。
看看下一幅图,其间显现了对一个针对 https://api.nutritionix.com/v1_1/search/chicken?fields=item_name,brand_name,nf_calories&item_type=3&appId=[APP-ID]&appKey=[APP-KEY](用于查找查询的 API 端点)的经过验证的 GET 恳求的呼应(在宣布恳求之前,请记住更新曾经的 URL,以反映您的 API 凭证)。
如该图所示,Nutritionix API 运用一个 JSON 文档呼应该恳求,该文档列出了与查找词汇 “chicken” 匹配的食物。查询字符串包括 item_type=3参数,该参数将查找规模约束到 USDA 数据库。关于每种食物,呼应包括食物称号、品牌称号和卡路里计数。也支撑其他字段;请查阅Nutritionix API 文档 了解有关的具体信息。
第 4 步. 启用查找界面
开发一个简略的查找界面,让用户能够查找食物并查看成果列表。成果页有必要包括一些控件,用户能够运用这些控件将所选的食物增加到其用餐记载中。
奢华此用户界面的根本结构并将其保存为 $APP_ROOT/templates/main.php。
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.css" /> <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script> <script src="http://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.6/angular.min.js"></script> </head> <body> <div data-role="page"> <div data-role="header"> <h1>Calorie Counter</h1> </div> <div data-role="content" ng-app="myApp"> <div data-role="tabs" ng-controller="myAppController"> <div data-role="navbar"> <ul> <li><a href="#search" data-theme="a" class="ui-btn-active">Search</a></li> <li><a href="#record" data-theme="a">Record</a></li> <li><a href="#report" data-theme="a">Report</a></li> </ul> </div> <div id="search"></div> <div id="record"></div> <div id="report"></div> </div> </div> </div> </body> </html>
前面的代码清单显现了一个契合规范 jQuery Mobile 约好的格局的页面。首要页面元素是一个 <div> 元素,它有一个 data-role="page" 特点。这个 <div> 元素包括针对页眉和内容的不同 <div> 元素。页面内容包括一系列选项卡。每个选项卡表明一个使命(“search”、“record” 和 “report”)。单击顶部导航栏中的选项卡称号将会显现它的内容。
接下来,向 search 选项卡增加元素,如下面这个代码清单所示:
<!DOCTYPE html> ... <div data-role="content" ng-app="myApp"> <div data-role="tabs" ng-controller="myAppController"> <div id="search"> <h2 class="ui-bar ui-bar-a">Food Item Search</h2> <div class="ui-body"> <input type="search" name="query" ng-model="foodItems.query" /> <button ng-click="search()">Search</button> </div> <h2 class="ui-bar ui-bar-a">Search Results</h2> <div class="ui-body"> <ul data-role="listview" data-split-theme="d"> <li ng-repeat="r in foodItems.results"> <a>{{r.fields.item_name}} / {{r.fields.nf_calories + ' calories'}}</a> <a href="#" data-inline="true" data-role="button" data-icon="plus" data-theme="a" ng-click="addToMeal(r)">Add</a> </li> </ul> </div> </div> </div> </div> ...
search 选项卡现在包括两个区域:顶部的查找输入字段和底部的查找成果列表。两个区域由一个 AngularJS 操控器操控,二者都运用了一个名为 foodItems 的 AngularJS 模型。下面这个代码清单显现了该操控器的代码。
<script> var myApp = angular.module('myApp', []); function myAppController($scope, $http) { // related to search functionality $scope.mealItems = []; $scope.foodItems = {}; $scope.foodItems.results = []; $scope.foodItems.query = ''; $scope.search = function() { if ($scope.foodItems.query != '') { $http({ method: 'GET', url: '/search/' + $scope.foodItems.query, }). success(function(data) { $scope.foodItems.results = data.hits; }); }; }; $scope.addToMeal = function(foodItem) { $scope.mealItems.push(foodItem); }; } </script>
这是该查找界面的实践外观。
它的作业原理是什么?用户输入一个查找词汇并单击 Search 时,AngularJS search() 函数会经过 foodItems 模型检索输入,并向 /search 运用程序端点生成一个 Ajax 恳求。此恳求不是 Nutritionix API 端点,而是一个由运用程序自身办理的中心 API 端点(稍后将会具体介绍)。
Ajax 恳求的呼应是一个 JSON 包,与之前显现的内容相似。这段响被附加到 foodItems.results 特点中,AngularJS 数据绑定担任迭代此调集,解析它,并将它显现为一个查找成果列表。
请细心看看这个查找界面。请注意,每个查找成果周围都有一个按钮,该按钮链接到 addToMeal() 函数。单击此按钮时,相应的食物会增加到该规模内的一个 mealItems 数组中。稍后,体系会运用这个数组构建 record 选项卡的视图。
在端,需求一个处理函数来处理对 /search 端点的 Ajax 恳求,这时就需求运用 Slim。Slim 运用 Ajax 恳求数据衔接到 Nutritionix API 并运转一次查找,相似于之前显现的查找。下面这个代码清单给出了完结此功用的代码。
<?php // use Composer autoloader require 'vendor/autoload.php'; \Slim\Slim::registerAutoloader(); // configure Slim application instance $app = new \Slim\Slim(); $app->config(array( 'debug' => true, 'templates.path' => './templates' )); // configure credentials // ... for Nutritionix $config["nutritionix"]["appId"] = 'APP-ID'; $config["nutritionix"]["appKey"] = 'APP-KEY'; // index page handlers $app->get('/', function () use ($app) { $app->redirect('/index'); }); $app->get('/index', function () use ($app) { $app->render('main.php'); }); // search handler $app->get('/search/:query', function ($query) use ($app, $config) { try { // execute search on Nutritionix API // specify search scope and required response fields // replace with your API credentials $qs = http_build_query(array('appId' => $config["nutritionix"]["appId"], 'appKey' => $config["nutritionix"]["appKey"], 'item_type' => '3', 'fields' => 'item_name,brand_name,nf_calories')); $url = 'https://api.nutritionix.com/v1_1/search/' . str_replace(' ', '+', $query) . '?' . $qs; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_exec($ch); curl_close($ch); } catch (Exception $e) { $app->response()->status(400); $app->response()->header('X-Status-Reason', $e->getMessage()); } }); $app->run();
此脚本(需求保存为 $APP_ROOT/index.php)首要加载 Slim 库,初始化并配备一个新的 Slim 运用程序目标。具体来讲,Slim 运用程序目标有必要配备 jQuery Mobile 页面模板的途径,以便能够根据需求出现这些模板。
Slim 界说了对 HTTP 办法和端点的路由器回调。为了履行此操作,它调用了相应的办法(例如 get() 来处理 GET 恳求,或许调用 post() 来处理 POST 恳求),并将 URL 路由,使之与该办法的第一个参数匹配。该办法的第二个参数是一个函数,它指定了在路由与一个传入的恳求匹配时应采纳的操作。前面的代码清单设置了两个这样的路由器回调:/index 和 /search。
- /index 徽标:出现首要运用程序页面模板。此回调包括各种选项卡、jQuery Mobile 页面元素和 AngularJS 操控器代码。
- /search 回调:处理 AngularJS 操控器发送的 Ajax 查找恳求。它承受一个查找词汇,然后运用 PHP http_build_query() 办法结构一个对 Nutritionix API 的恳求的 URL。该恳求经过 cURL 发送到该 API,呼应作为一个 JSON 文档回来给运用程序前端。然后,AngularJS 担任解析呼应数据并将它们绑定到该规模。
也能够直接从运用程序前端运用 AngularJS 运转 Ajax 恳求。可是,履行此操作会向用户露出您的私有 Nutritionix API 运用程序密钥,关于可揭露拜访的运用程序,不引荐这样做。运用一段端脚原本履行恳求会增加一些开支,但具有更高的安全性。
第 5 步. 核算和存储用餐记载
完结查找界面后,下一步是构建用户界面中的第二个选项卡。此选项卡显现了用户选定的食物的列表,以及这些食物中的卡路里总量。它包括一些将用餐记载保存到数据库中的控件。运用以下代码。
<!DOCTYPE html> … <head> <script> var myApp = angular.module('myApp', []); function myAppController($scope, $http) { // related to record functionality $scope.removeFromMeal = function(index) { $scope.mealItems.splice(index, 1); }; $scope.clearMeal = function() { $scope.mealItems.length = 0; }; $scope.getTotalCalories = function() { var sum = 0; for(i=0; i<$scope.mealItems.length; i++) { sum += $scope.mealItems[i].fields.nf_calories; } return sum.toFixed(2); }; $scope.record = function() { if ($scope.getTotalCalories() > 0) { $http({ method: 'POST', url: '/record', data: {'totalCalories': $scope.getTotalCalories()} }). success(function(data) { $scope.clearMeal(); }); }; }; } </script> </head> <body> ... <div data-role="content" ng-app="myApp"> <div data-role="tabs" ng-controller="myAppController"> <div data-role="navbar"> <ul> <li><a href="#search" data-theme="a" class="ui-btn-active">Search</a></li> <li><a href="#record" data-theme="a">Record <span class="ui-li-count"> {{ getTotalCalories() }} / {{ mealItems.length }}</span></a></li> <li><a href="#report" data-theme="a">Report</a></li> </ul> </div> <div id="record"> <h2 class="ui-bar ui-bar-a">Meal Record</h2> <div class="ui-body"> <ul data-role="listview" data-split-theme="d"> <li ng-repeat="item in mealItems track by $index"> <a>{{item.fields.item_name}} / {{item.fields.nf_calories + ' calories'}}</a> <a href="#" data-inline="true" data-role="button" data-icon="minus" data-theme="a" ng-click="removeFromMeal($index)">Add</a> </li> </ul> </div> <div class="ui-body"> <button ng-click="record()">Save</button> </div> </div> </div> </div> … </body> </html>
下一个屏幕截图显现了该界面的实践外观。
经过迭代 第 4 步 中的 mealItems 数组来生成选定食物列表十分简略。在用户挑选查找界面中的新食物时,AngularJS 数据绑定机制可保证该列表会即时更新。
完结用餐记载后,用户单击 Save 将该记载保存到数据库中。record() 函数创立了一个对 /record 端点的 Ajax POST 恳求,将它传递给用餐记载的卡路里总量中。假如该 Ajax 恳求被成功处理,那么 mealItems 数组将会被铲除,预备存储下一次用餐的记载。
在上一个清单中,请注意别的两点:
- 导航栏包括两个计数器:所选食物的卡路里总量和食物总数。当用户在用餐记载中增加和删去食物时,该计数会主动更新。此更新相同是运用数据绑定来完结的。这两个值可在 mealItems 数组长度和 getTotalCalories() 操控器办法中进行动态更新。
- 用户可单击每个食物周围的按钮,从用餐记载中删去选定的食物。此操作会调用 removeFromMeal() 操控器办法,该办法运用所选食物的索引来从 mealItems 数组中删去它。数据绑定担任更新食物和导航栏计数器。
在端,需求增加一个对 /record 端点的 Slim 回调。您或许现已猜到,此回调将会读取运用程序前端发送的卡路里总量,并将它耐久保存在 第 1 步 中创立的 MySQL 数据库中。该回调的代码为:
<?php // use Composer autoloader require 'vendor/autoload.php'; \Slim\Slim::registerAutoloader(); // configure credentials // ... for Nutritionix $config["nutritionix"]["appId"] = 'APP-ID'; $config["nutritionix"]["appKey"] = 'APP-KEY'; // ... for MySQL $config["db"]["name"] = 'test'; $config["db"]["host"] = 'localhost'; $config["db"]["port"] = '3306'; $config["db"]["user"] = 'root'; $config["db"]["password"] = 'guessme'; // if Bluemix VCAP_SERVICES environment available // overwrite with credentials from Bluemix if ($services = getenv("VCAP_SERVICES")) { $services_json = json_decode($services, true); $config["db"] = $services_json["mysql-5.5"][0]["credentials"]; } // configure Slim application instance $app = new \Slim\Slim(); $app->config(array( 'debug' => true, 'templates.path' => './templates' )); // initialize PDO object $db = $config["db"]["name"]; $host = $config["db"]["host"]; $port = $config["db"]["port"]; $username = $config["db"]["user"]; $password = $config["db"]["password"]; $dbh = new PDO("mysql:host=$host;dbname=$db;port=$port;charset=utf8", $username, $password); // start session session_start(); // record handler $app->post('/record', function () use ($app, $dbh) { try { // get and decode JSON request body $request = $app->request(); $body = $request->getBody(); $input = json_decode($body); // insert meal record $stmt = $dbh->prepare('INSERT INTO meals (uid, calories, rdate, ip) VALUES(?, ?, ?, ?)'); $stmt->execute(array($_SESSION['uid'], $input->totalCalories, date('Y-m-d h:i:s', time()), $_SERVER['SERVER_ADDR'])); $input->id = $dbh->lastInsertId(); // return JSON-encoded response body $app->response()->header('Content-Type', 'application/json'); echo json_encode($input); } catch (Exception $e) { $app->response()->status(400); $app->response()->header('X-Status-Reason', $e->getMessage()); } }); // snip: other handlers $app->run();
此进程首要配备本地运用程序数据库的凭证。然后在 PHP 环境中查看特别的 VCAP_SERVICES 环境变量。在 Bluemix 上,此变量具有绑定的服务实例的凭证。假如找到此变量,该脚本会假定它在 Bluemix 上运转,运用这些凭证来初始化一个与绑定的 MySQL 实例的 PDO 衔接。假如未找到此变量,该脚本会假定它在一个本地开发实例上运转,并运用本地数据库的凭证。
接下来,该清单为 /record 路由界说了一个 POST 回调处理函数。此处理函数收到包括卡路里总量的 Ajax POST 恳求,并创立 SQL INSERT 句子来将这些计数保存到数据库中。除了卡路里计数之外,该处理函数还会在句子中主动增加时刻戳、客户端的 IP 地址和登录用户的专一标识符。假如 INSERT 成功完结,该处理函数会向恳求的 Ajax 脚本回来一个包括记载标识符的 JSON 包。
您或许想知道用户标识符来自何处。这将在 第 7 步 中具体介绍,但简略地讲,每个运用程序用户都有一个在注册时生成的专一标识符。用户登录时,此标识符会被增加到 $_SESSION['uid'] 变量中的会话中,并在您保存和检索特定于用户的信息时刺进到各种 SQL 句子中。