May 08, 2021 AngularJS
This article focuses on AngularJS XHR and dependency injection, where details and sample code are organized
There are three phones in a hard-coded data set, and building an app is enough!
Let's use one of
Angel's built-in services, $http to
get a larger data set from the server, and we'll use Angular's Dependency Injection
(DI)
to
PhoneListCtrl
controller.
Reset the workspace to step five
git checkout -f step-5
Refresh your browser or check this step online: Step 5 Live Demo
The most important differences between step four and step five are listed below. You can see the full difference in GitHub.
In your project,
app/phones/phones.json
is a dataset that contains a larger list of phones stored in JSON format.
Follow the following file example:
[
{
"age": 13,
"id": "motorola-defy-with-motoblur",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
"snippet": "Are you ready for everything life throws your way?"
...
},
...
]
We will use
Angular's $http service
in the controller to make HTTP requests to your Web server to get back the data from the
app/phones/phones.json
file.
$http
one of several built-in
Angular
services that handle common operations in Web applications.
Angular injects these services where you need them.
Angular's DI subsysys system is responsible for managing these services. Dependency injection is useful for your web application to be both well-structured (for example, separating the performance layer, data, and control) and loose coupling (dependency issues between components that cannot be solved by the components themselves, solved by the DI subsyscies).
app/js/controllers.js:
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
});
$http
to send an HTTP
GET
request to your Web server that
phones/phones.json
(the url is a
index.html
T
he server provides this data in the jason file in response to the request. (
The response may be dynamically generated by the back-end server.) B
ut in the eyes of browsers and our apps, they're no different. F
or simplicity, we used a johnson file in this tutorial.
)
The
$http
the service returns a
promise object,
with the
success
method. W
e call this method to handle asynchronous responses and assume that the scope of the phone data is controlled by the controller, as a module called
phones
Note that Angular detected the johnson response and parsed it for us.
To use a service in Angular, you simply declare the name of the dependency you need as an argument to the controller's constructor, as follows:
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {...}
When you construct a controller, Angular's dependency injector injects these services into your controller. These dependency controllers are also responsible for creating any delivery dependencies that the service might require (one service typically depends on other services).
Note that the names of the parameters are important because the injector uses those names to look up dependencies.
$
The prefix name convention
You can create your own service, and
we'll actually do this in Step 11 AngularJS REST and
Custom Services.
As a naming convention, Angular's built-in services, scope methods, and some other Angular APIs use a $ prefix
$
naming.
The namespace of the service provided by Angular has
$
To avoid conflicts, it's a good idea to avoid naming your services and modules as prefixes.
$
If you examine a scope, you may also notice that some properties start
$$
These properties are considered private and cannot be accessed or modified.
Because Angel calls the controller's dependencies from the name of the argument to
the controller constructor's function, if you plan to shrink the JavaScript code for
PhoneListCtrl
all function parameters are compressed, and the dependency injector will not correctly recognize the service.
We can overcome this problem by commenting this function with a dependency name, which is provided as a string and is not compressed. There are two ways to provide this injection comment:
Create a property in the
$inject
function that can carry an array of strings. E
ach string in the array is the name of the service to be injected into the corresponding parameter.
We can write this in our own example:
function PhoneListCtrl($scope, $http) {...}
PhoneListCtrl.$inject = ['$scope', '$http'];
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);
Using an inline comment there is not just for this function, you also provide an array. This array contains a series of service names, followed by the function itself.
function PhoneListCtrl($scope, $http) {...}
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]);
Both methods work perfectly with any function that Angular injects, so which method to choose depends entirely on the programming style of your project.
If you use the second method, when registering a controller, you typically provide an inline constructor function in the form of an anonymous function.
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) {...}]);
From this point on, we'll use the inline method in this tutorial.
With that in in place, let's add the
PhoneListCtrl
app/js/controllers.js:
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http',
function ($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
}]);
test/unit/controllersSpec.js
:
Since we started using dependency injection, and our controllers contain dependencies, constructing controllers in our tests became a bit complicated. W
e can
new
operator and provide a constructor with some
$http
implemented by the computer. H
owever, Angular provides an analog
$http
that we can use in unit tests.
We configured a
$httpBackend
server request by calling a method called the server service.
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
var scope, ctrl, $httpBackend;
// 在每次测试之前载入我们的应用模块定义
beforeEach(module('phonecatApp'));
// 注入器会忽略前面和后面的下划线(例如_$httpBackend_)。
// 这允许我们注入一个服务,然后把它附加到同名变量上,以避免名称冲突
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/phones.json').
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
scope = $rootScope.$new();
ctrl = $controller('PhoneListCtrl', {$scope: scope});
}));
Note: Because we loaded Jasmine and
angular-mocks.js
we got
two secondary methods, module
and
inject,
to access and configure the injector.
We create controllers in a test environment, as follows:
inject
assist method to inject instances of
$rootScope,
$controller,
and $httpBackend
services into
Jasmine's
beforeEach
functions, which come from an injector that is recreated within each test.
This ensures that each test starts from a well-known starting point, and that each test is independent of the others.
$rootScope.$new()
by calling the called .$new().
$controller
to pass in
PhoneListCtrl
controller as an argument.
Because our code now
$http
get back the phone list data from our controller, we need to tell the test suite to wait for a later request from the controller before we create the
PhoneListCtrl
sub-scope.
We can do this:
Request to
$httpBackend
service into our
beforeEach
function. T
his is a simulated version of the service in the product environment that can respond to a variety of XHR and JSONP requests.
The simulated version of the service allows you to write tests without having to deal with the native API and the global state associated with it -- both of which would have made testing a nightmare.
$httpBackend.expectGET
$httpBackend
request after the service waits and tells it how to respond to it.
Note that the response is
$httpBackend.flush
the .flush method.
Now we make an assertion to verify that the phone module does not exist on the scope until the response arrives:
it('should create "phones" model with 2 phones fetched from xhr', function() {
expect(scope.phones).toBeUndefined();
$httpBackend.flush();
expect(scope.phones).toEqual([{name: 'Nexus S'},
{name: 'Motorola DROID'}]);
});
By calling
$httpBackend.flush()
we empty the request queue in the browser. T
his causes
$http
returned by the service to be handled by the canon standard answer.
You
can find a full explanation of
why httpBackend documentation must be "emptyed".
Finally, we verify that the default value for
orderProp
set correctly.
it('should set the default value of orderProp model', function() {
expect(scope.orderProp).toBe('age');
});
Now in the Karma tag card, you should see the following output:
Chrome 22.0: Executed 2 of 2 SUCCESS (0.028 secs / 0.007 secs)
index.html
add a binding to view | | | a list of
<pre>{{phones | filter:query | orderBy:orderProp | json}}</pre>
displayed in json format.
PhoneListCtrl
the http response is preprocessed by limiting the number of phones to the first five of the list.
Use
$http
in the callback:
$scope.phones = data.splice(0, 5);
Now that you know how easy it is to use the Angular service (thanks to Angel's dependency injection), go to Step 6 template connections and images, where you'll add some thumbnails of your phone as well as some links.