您的第一个 AngularJS 应用程序的分步教程
已发表: 2022-03-11什么是 AngularJS?
AngularJS 是由 Google 开发的 JavaScript MVC 框架,可让您构建结构良好、易于测试和可维护的前端应用程序。
我为什么要使用它?
如果你还没有尝试过 AngularJS,那你就错过了。 该框架由一个紧密集成的工具集组成,它将帮助您以模块化的方式构建结构良好、丰富的客户端应用程序——使用更少的代码和更大的灵活性。
AngularJS 通过提供向标记添加功能并允许您创建强大的动态模板的指令来扩展 HTML。 您还可以创建自己的指令,制作满足您需求的可重用组件并抽象出所有 DOM 操作逻辑。
它还实现了双向数据绑定,将您的 HTML(视图)无缝连接到您的 JavaScript 对象(模型)。 简单来说,这意味着模型上的任何更新都将立即反映在您的视图中,而无需任何 DOM 操作或事件处理(例如,使用 jQuery)。
最后,我喜欢 Angular,因为它在服务器通信方面的灵活性。 与大多数 JavaScript MVC 框架一样,它允许您使用任何服务器端技术,只要它可以通过 RESTful Web API 为您的应用程序提供服务。 但是 Angular 还在 XHR之上提供了服务,这些服务极大地简化了您的代码,并允许您将 API 调用抽象为可重用的服务。 因此,您可以将模型和业务逻辑移至前端并构建后端不可知的 Web 应用程序。 在这篇文章中,我们将一步一步地做到这一点。
那么,我从哪里开始?
首先,让我们决定我们要构建的应用程序的性质。 在本指南中,我们不希望在后端花费太多时间,因此我们将根据可在 Internet 上轻松获得的数据编写一些内容,例如体育动态应用程序!
由于我碰巧是赛车和一级方程式赛车的忠实粉丝,因此我将使用汽车运动 API 服务作为我们的后端。 幸运的是,Ergast 的人很友善地提供了一个免费的赛车 API,这对我们来说是完美的。
如需了解我们将要构建的内容,请查看现场演示。 为了美化演示并展示一些 Angular 模板,我应用了 WrapBootstrap 的 Bootstrap 主题,但鉴于本文不是关于 CSS 的,我将把它从示例中抽象出来并省略掉。
入门教程
让我们用一些样板来启动我们的示例应用程序。 我推荐 angular-seed 项目,因为它不仅为您提供了一个很好的引导框架,而且还为使用 Karma 和 Jasmine 进行单元测试奠定了基础(我们不会在这个演示中进行任何测试,所以我们只是暂时把这些东西放在一边;有关设置项目以进行单元和端到端测试的更多信息,请参阅本教程的第 2 部分)。
编辑(2014 年 5 月):自从我编写本教程以来,angular-seed 项目经历了一些重大变化(包括将 Bower 添加为包管理器)。 如果您对如何部署项目有任何疑问,请快速查看其参考指南的第一部分。 在本教程的第 2 部分中,更详细地介绍了 Bower 以及其他工具。
好的,现在我们已经克隆了存储库并安装了依赖项,我们的应用程序的骨架将如下所示:
现在我们可以开始编码了。 当我们尝试为赛车锦标赛构建体育提要时,让我们从最相关的视图开始:锦标赛表。
鉴于我们已经在我们的范围内定义了一个驱动程序列表(挂我 - 我们会到达那里),并忽略任何 CSS(为了可读性),我们的 HTML 可能如下所示:
<body ng-app="F1FeederApp" ng-controller="driversController"> <table> <thead> <tr><th colspan="4">Drivers Championship Standings</th></tr> </thead> <tbody> <tr ng-repeat="driver in driversList"> <td>{{$index + 1}}</td> <td> <img src="img/flags/{{driver.Driver.nationality}}.png" /> {{driver.Driver.givenName}} {{driver.Driver.familyName}} </td> <td>{{driver.Constructors[0].name}}</td> <td>{{driver.points}}</td> </tr> </tbody> </table> </body>
在这个模板中你会注意到的第一件事是使用表达式(“{{”和“}}”)来返回变量值。 在 AngularJS 开发中,表达式允许您执行一些计算以返回所需的值。 一些有效的表达式是:
-
{{ 1 + 1 }}
-
{{ 946757880 | date }}
-
{{ user.name }}
实际上,表达式是类似 JavaScript 的片段。 但是尽管非常强大,但您不应该使用表达式来实现任何更高级别的逻辑。 为此,我们使用指令。
了解基本指令
您会注意到的第二件事是ng-attributes
的存在,这在典型的标记中是看不到的。 这些是指令。
在高层次上,指令是指示 AngularJS 将给定行为附加到 DOM 元素(或对其进行转换、替换等)的标记(例如属性、标签和类名)。 让我们来看看我们已经看过的那些:
ng-app
指令负责引导您的应用程序定义其范围。 在 AngularJS 中,您可以在同一页面中拥有多个应用程序,因此该指令定义了每个不同应用程序的开始和结束位置。ng-controller
指令定义了哪个控制器将负责您的视图。 在这种情况下,我们表示driversController
,它将提供我们的驱动程序列表(driversList
)。ng-repeat
指令是最常用的指令之一,用于在遍历集合时定义模板范围。 在上面的示例中,它为driversList
中的每个驱动程序复制了表中的一行。
添加控制器
当然,没有控制器我们的视图是没有用的。 让我们将driversController
添加到我们的 controllers.js 中:
angular.module('F1FeederApp.controllers', []). controller('driversController', function($scope) { $scope.driversList = [ { Driver: { givenName: 'Sebastian', familyName: 'Vettel' }, points: 322, nationality: "German", Constructors: [ {name: "Red Bull"} ] }, { Driver: { givenName: 'Fernando', familyName: 'Alonso' }, points: 207, nationality: "Spanish", Constructors: [ {name: "Ferrari"} ] } ]; });
您可能已经注意到我们作为参数传递给控制器的$scope
变量。 $scope
变量应该链接你的控制器和视图。 特别是,它包含将在您的模板中使用的所有数据。 您添加到其中的任何内容(如上例中的driversList
)都可以在您的视图中直接访问。 现在,让我们只使用一个虚拟(静态)数据数组,稍后我们将用我们的 API 服务替换它。
现在,将其添加到 app.js:
angular.module('F1FeederApp', [ 'F1FeederApp.controllers' ]);
通过这行代码,我们实际上初始化了我们的应用程序并注册了它所依赖的模块。 稍后我们将回到该文件 ( app.js
)。
现在,让我们将所有内容放在index.html
中:
<!DOCTYPE html> <html> <head> <title>F-1 Feeder</title> </head> <body ng-app="F1FeederApp" ng-controller="driversController"> <table> <thead> <tr><th colspan="4">Drivers Championship Standings</th></tr> </thead> <tbody> <tr ng-repeat="driver in driversList"> <td>{{$index + 1}}</td> <td> <img src="img/flags/{{driver.Driver.nationality}}.png" /> {{driver.Driver.givenName}} {{driver.Driver.familyName}} </td> <td>{{driver.Constructors[0].name}}</td> <td>{{driver.points}}</td> </tr> </tbody> </table> <script src="bower_components/angular/angular.js"></script> <script src="bower_components/angular-route/angular-route.js"></script> <script src="js/app.js"></script> <script src="js/services.js"></script> <script src="js/controllers.js"></script> </body> </html>
模数小错误,您现在可以启动您的应用程序并检查您的(静态)驱动程序列表。
注意:如果您需要帮助调试您的应用程序并在浏览器中可视化您的模型和范围,我建议您查看用于 Chrome 的很棒的 Batarang 插件。
从服务器加载数据
由于我们已经知道如何在视图中显示控制器的数据,是时候从 RESTful 服务器实际获取实时数据了。
为了促进与 HTTP 服务器的通信,AngularJS 提供了$http
和$resource
服务。 前者只是 XMLHttpRequest 或 JSONP 之上的一层,而后者提供了更高级别的抽象。 我们将使用$http
。
为了从控制器中抽象出我们的服务器 API 调用,让我们创建我们自己的自定义服务,它将获取我们的数据并通过将其添加到我们的services.js
来充当$http
的包装器:
angular.module('F1FeederApp.services', []). factory('ergastAPIservice', function($http) { var ergastAPI = {}; ergastAPI.getDrivers = function() { return $http({ method: 'JSONP', url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK' }); } return ergastAPI; });
在前两行中,我们创建了一个新模块( F1FeederApp.services
)并在该模块中注册了一个服务( ergastAPIservice
)。 请注意,我们将$http
作为参数传递给该服务。 这告诉 Angular 的依赖注入引擎,我们的新服务需要(或依赖于) $http
服务。
以类似的方式,我们需要告诉 Angular 将我们的新模块包含到我们的应用程序中。 让我们用app.js
注册它,将我们现有的代码替换为:
angular.module('F1FeederApp', [ 'F1FeederApp.controllers', 'F1FeederApp.services' ]);
现在,我们需要做的就是稍微调整一下我们的controller.js
,将ergastAPIservice
包含为依赖项,然后我们就可以开始了:
angular.module('F1FeederApp.controllers', []). controller('driversController', function($scope, ergastAPIservice) { $scope.nameFilter = null; $scope.driversList = []; ergastAPIservice.getDrivers().success(function (response) { //Dig into the responde to get the relevant data $scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings; }); });
现在重新加载应用程序并查看结果。 请注意,我们没有对模板进行任何更改,但我们在作用域中添加了一个nameFilter
变量。 让我们使用该变量。
过滤器
伟大的! 我们有一个功能控制器。 但它只显示驱动程序列表。 让我们通过实现一个简单的文本搜索输入来添加一些功能,该输入将过滤我们的列表。 让我们将以下行添加到我们的index.html
中,就在<body>
标签的正下方:
<input type="text" ng-model="nameFilter" placeholder="Search..."/>
我们现在正在使用ng-model
指令。 该指令将我们的文本字段绑定到$scope.nameFilter
变量,并确保其值始终与输入值保持同步。 现在,让我们再次访问 index.html 并对包含ng-repeat
指令的行做一个小的调整:
<tr ng-repeat="driver in driversList | filter: nameFilter">
这一行告诉ng-repeat
,在输出数据之前, driversList
数组必须通过nameFilter
中存储的值进行过滤。
此时,双向数据绑定开始发挥作用:每次在搜索字段中输入值时,Angular 都会立即确保我们与之关联的$scope.nameFilter
使用新值进行更新。 由于绑定是双向的,当nameFilter
值更新时,与之关联的第二个指令(即ng-repeat
)也获取新值并且视图会立即更新。

重新加载应用程序并查看搜索栏。
请注意,此过滤器将在模型的所有属性(包括未使用的属性)上查找关键字。 假设我们只想通过Driver.givenName
和Driver.familyName
进行过滤:首先,我们添加到driversController
中,就在$scope.driversList = [];
线:
$scope.searchFilter = function (driver) { var keyword = new RegExp($scope.nameFilter, 'i'); return !$scope.nameFilter || keyword.test(driver.Driver.givenName) || keyword.test(driver.Driver.familyName); };
现在,回到index.html
,我们更新包含ng-repeat
指令的行:
<tr ng-repeat="driver in driversList | filter: searchFilter">
再次重新加载应用程序,现在我们可以按名称搜索。
路线
我们的下一个目标是创建一个司机详细信息页面,让我们点击每个司机并查看他/她的职业详细信息。
首先,让我们包含$routeProvider
服务(在app.js
中),它将帮助我们处理这些不同的应用程序路由。 然后,我们将添加两条这样的路线:一条用于冠军桌,另一条用于车手详细信息。 这是我们的新app.js
:
angular.module('F1FeederApp', [ 'F1FeederApp.services', 'F1FeederApp.controllers', 'ngRoute' ]). config(['$routeProvider', function($routeProvider) { $routeProvider. when("/drivers", {templateUrl: "partials/drivers.html", controller: "driversController"}). when("/drivers/:id", {templateUrl: "partials/driver.html", controller: "driverController"}). otherwise({redirectTo: '/drivers'}); }]);
通过该更改,导航到http://domain/#/drivers
将加载driversController
并在partials/drivers.html
中查找要呈现的部分视图。 可是等等! 我们还没有任何局部视图,对吧? 我们也需要创建这些。
部分视图
AngularJS 将允许你将你的路由绑定到特定的控制器和视图。
但首先,我们需要告诉 Angular 在哪里渲染这些局部视图。 为此,我们将使用ng-view
指令,修改我们的index.html
以反映以下内容:
<!DOCTYPE html> <html> <head> <title>F-1 Feeder</title> </head> <body ng-app="F1FeederApp"> <ng-view></ng-view> <script src="bower_components/angular/angular.js"></script> <script src="bower_components/angular-route/angular-route.js"></script> <script src="js/app.js"></script> <script src="js/services.js"></script> <script src="js/controllers.js"></script> </body> </html>
现在,每当我们浏览我们的应用程序路由时,Angular 都会加载关联的视图并将其呈现在<ng-view>
标记的位置。 我们需要做的就是创建一个名为partials/drivers.html
的文件并将我们的冠军表 HTML 放在那里。 我们还将利用这个机会将驱动程序名称链接到我们的驱动程序详细信息路由:
<input type="text" ng-model="nameFilter" placeholder="Search..."/> <table> <thead> <tr><th colspan="4">Drivers Championship Standings</th></tr> </thead> <tbody> <tr ng-repeat="driver in driversList | filter: searchFilter"> <td>{{$index + 1}}</td> <td> <img src="img/flags/{{driver.Driver.nationality}}.png" /> <a href="#/drivers/{{driver.Driver.driverId}}"> {{driver.Driver.givenName}} {{driver.Driver.familyName}} </a> </td> <td>{{driver.Constructors[0].name}}</td> <td>{{driver.points}}</td> </tr> </tbody> </table>
最后,让我们决定要在详细信息页面中显示的内容。 对司机的所有相关事实(例如,出生、国籍)的总结以及包含他/她最近结果的表格怎么样? 为此,我们添加到services.js
:
angular.module('F1FeederApp.services', []) .factory('ergastAPIservice', function($http) { var ergastAPI = {}; ergastAPI.getDrivers = function() { return $http({ method: 'JSONP', url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK' }); } ergastAPI.getDriverDetails = function(id) { return $http({ method: 'JSONP', url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/driverStandings.json?callback=JSON_CALLBACK' }); } ergastAPI.getDriverRaces = function(id) { return $http({ method: 'JSONP', url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/results.json?callback=JSON_CALLBACK' }); } return ergastAPI; });
这一次,我们将司机的 ID 提供给服务,以便我们检索仅与特定司机相关的信息。 现在,我们修改controllers.js
:
angular.module('F1FeederApp.controllers', []). /* Drivers controller */ controller('driversController', function($scope, ergastAPIservice) { $scope.nameFilter = null; $scope.driversList = []; $scope.searchFilter = function (driver) { var re = new RegExp($scope.nameFilter, 'i'); return !$scope.nameFilter || re.test(driver.Driver.givenName) || re.test(driver.Driver.familyName); }; ergastAPIservice.getDrivers().success(function (response) { //Digging into the response to get the relevant data $scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings; }); }). /* Driver controller */ controller('driverController', function($scope, $routeParams, ergastAPIservice) { $scope.id = $routeParams.id; $scope.races = []; $scope.driver = null; ergastAPIservice.getDriverDetails($scope.id).success(function (response) { $scope.driver = response.MRData.StandingsTable.StandingsLists[0].DriverStandings[0]; }); ergastAPIservice.getDriverRaces($scope.id).success(function (response) { $scope.races = response.MRData.RaceTable.Races; }); });
这里要注意的重要一点是,我们只是将$routeParams
服务注入到驱动程序控制器中。 该服务将允许我们使用$routeParams.id
访问我们的 URL 参数(在这种情况下是:id
)。
现在我们的数据在范围内,我们只需要剩余的部分视图。 让我们创建一个名为partials/driver.html
的文件并添加:
<section> <a href="./#/drivers"><- Back to drivers list</a> <nav class="main-nav"> <div class="driver-picture"> <div class="avatar"> <img ng-show="driver" src="img/drivers/{{driver.Driver.driverId}}.png" /> <img ng-show="driver" src="img/flags/{{driver.Driver.nationality}}.png" /><br/> {{driver.Driver.givenName}} {{driver.Driver.familyName}} </div> </div> <div class="driver-status"> Country: {{driver.Driver.nationality}} <br/> Team: {{driver.Constructors[0].name}}<br/> Birth: {{driver.Driver.dateOfBirth}}<br/> <a href="{{driver.Driver.url}}" target="_blank">Biography</a> </div> </nav> <div class="main-content"> <table class="result-table"> <thead> <tr><th colspan="5">Formula 1 2013 Results</th></tr> </thead> <tbody> <tr> <td>Round</td> <td>Grand Prix</td> <td>Team</td> <td>Grid</td> <td>Race</td> </tr> <tr ng-repeat="race in races"> <td>{{race.round}}</td> <td><img src="img/flags/{{race.Circuit.Location.country}}.png" />{{race.raceName}}</td> <td>{{race.Results[0].Constructor.name}}</td> <td>{{race.Results[0].grid}}</td> <td>{{race.Results[0].position}}</td> </tr> </tbody> </table> </div> </section>
请注意,我们现在正在充分利用ng-show
指令。 如果提供的表达式为true
(即既不是false
也不是null
),该指令只会显示 HTML 元素。 在这种情况下,只有当控制器将驱动程序对象加载到作用域中时,头像才会显示。
收尾工作
添加一堆 CSS 并渲染您的页面。 你应该得到这样的结果:
您现在已准备好启动您的应用程序并确保两条路线都按预期工作。 您还可以将静态菜单添加到index.html
以改进用户的导航功能。 可能性是无止境。
编辑(2014 年 5 月):我收到了很多关于我们在本教程中构建的代码的可下载版本的请求。 因此,我决定在这里发布它(剥离任何 CSS)。 但是,我真的不建议下载它,因为本指南包含了您自己动手构建相同应用程序所需的每一个步骤,这将是一个更加有用和有效的学习练习。
结论
在本教程的这一点上,我们已经涵盖了编写简单应用程序(如一级方程式喂食器)所需的一切。 现场演示中的每个剩余页面(例如,车队冠军表、团队详细信息、日历)都共享我们在此处查看过的相同基本结构和概念。
最后,请记住,Angular 是一个非常强大的框架,就它所提供的一切而言,我们几乎没有触及到皮毛。 在本教程的第 2 部分中,我们将举例说明为什么 Angular 在其同行前端 MVC 框架中脱颖而出:可测试性。 我们将回顾使用 Karma 编写和运行单元测试的过程,实现与 Yeomen、Grunt 和 Bower 的持续集成,以及这个出色的前端框架的其他优势。