AngularJS的基本架构与相关概念

Base Architecture of AngularJS

Official diagram of introduction for AngularJS:

其中包括了一些主要的部分,列表如下:

ConceptDescription
TemplateHTML with additional markup
Directivesextend HTML with custom attributes and elements
Modelthe data shown to the user in the view and with which the user interacts
Scopecontext where the model is stored so that controllers, directives and expressions can access it
Expressionsaccess variables and functions from the scope
Compilerparses the template and instantiates directives and expressions
Filterformats the value of an expression for display to the user
Viewwhat the user sees (the DOM)
Data Bindingsync data between the model and the view
Controllerthe business logic behind views
Dependency InjectionCreates and wires objects and functions
Injectordependency injection container
Modulea container for the different parts of an app including controllers, services, filters, directives which configures the Injector
Servicereusable business logic independent of views

1. Module

You can think of a module as a container for the different parts of your app – controllers, services, filters, directives, etc.

Most applications have a main method that instantiates and wires together the different parts of the application.

AngularJS apps don’t have a main method. Instead modules declaratively specify how an application should be bootstrapped. There are several advantages to this approach:

  • The declarative process is easier to understand.
  • You can package code as reusable modules.
  • The modules can be loaded in any order (or even in parallel) because modules delay execution.
  • Unit tests only have to load relevant modules, which keeps them fast.
  • End-to-end tests can use modules to override configuration.

2. Scope

2.1 Scope characteristics

  • Scopes provide APIs ($watch) to observe model mutations.

  • Scopes provide APIs ($apply) to propagate any model changes through the system into the view from outside of the “AngularJS realm” (controllers, services, AngularJS event handlers).

  • Scopes can be nested to limit access to the properties of application components while providing access to shared model properties. Nested scopes are either “child scopes” or “isolate scopes”. A “child scope” (prototypically) inherits properties from its parent scope. An “isolate scope” does not. See isolated scopes for more information.

  • Scopes provide context against which expressions are evaluated. For example expression is meaningless, unless it is evaluated against a specific scope which defines the username property.

2.2 Scope as Data-Model

Scope is the glue between application controller and the view. During the template linking phase the directives set up $watch expressions on the scope. The $watch allows the directives to be notified of property changes, which allows the directive to render the updated value to the DOM.

Both controllers and directives have reference to the scope, but not to each other. This arrangement isolates the controller from the directive as well as from the DOM. This is an important point since it makes the controllers view agnostic, which greatly improves the testing story of the applications.

1
2
3
4
5
6
7
8
angular.module('scopeExample', [])
.controller('MyController', ['$scope', function($scope) {
$scope.username = 'World';

$scope.sayHello = function() {
$scope.greeting = 'Hello ' + $scope.username + '!';
};
}]);
1
2
3
4
5
6
7
<div ng-controller="MyController">
Your name:
<input type="text" ng-model="username">
<button ng-click='sayHello()'>greet</button>
<hr>
{{greeting}}
</div>

In the above example notice that the MyController assigns World to the username property of the scope. The scope then notifies the input of the assignment, which then renders the input with username pre-filled. This demonstrates how a controller can write data into the scope.

Similarly the controller can assign behavior to scope as seen by the sayHello method, which is invoked when the user clicks on the ‘greet’ button. The sayHello method can read the username property and create a greeting property. This demonstrates that the properties on scope update automatically when they are bound to HTML input widgets.

Logically the rendering of involves:

  • retrieval of the scope associated with DOM node where is defined in template. In this example this is the same scope as the scope which was passed into MyController. (We will discuss scope hierarchies later.)
  • Evaluate the greeting expression against the scope retrieved above, and assign the result to the text of the enclosing DOM element.

You can think of the scope and its properties as the data which is used to render the view. The scope is the single source-of-truth for all things view related.

From a testability point of view, the separation of the controller and the view is desirable, because it allows us to test the behavior without being distracted by the rendering details.

1
2
3
4
5
6
7
8
9
10
11
12
it('should say hello', function() {
var scopeMock = {};
var cntl = new MyController(scopeMock);

// Assert that username is pre-filled
expect(scopeMock.username).toEqual('World');

// Assert that we read new username and greet
scopeMock.username = 'angular';
scopeMock.sayHello();
expect(scopeMock.greeting).toEqual('Hello angular!');
});

2.3 Scope Hierarchies

Each AngularJS application has exactly one root scope, but may have any number of child scopes.

The application can have multiple scopes, because directives can create new child scopes. When new scopes are created, they are added as children of their parent scope. This creates a tree structure which parallels the DOM where they’re attached.

The section Directives that Create Scopes has more info about which directives create scopes.

When AngularJS evaluates , it first looks at the scope associated with the given element for the name property. If no such property is found, it searches the parent scope and so on until the root scope is reached. In JavaScript this behavior is known as prototypical inheritance, and child scopes prototypically inherit from their parents.

This example illustrates scopes in application, and prototypical inheritance of properties. The example is followed by a diagram depicting the scope boundaries.

2.4 Scope Life Cycle

The normal flow of a browser receiving an event is that it executes a corresponding JavaScript callback. Once the callback completes the browser re-renders the DOM and returns to waiting for more events.

When the browser calls into JavaScript the code executes outside the AngularJS execution context, which means that AngularJS is unaware of model modifications. To properly process model modifications the execution has to enter the AngularJS execution context using the $apply method. Only model modifications which execute inside the $apply method will be properly accounted for by AngularJS. For example if a directive listens on DOM events, such as ng-click it must evaluate the expression inside the $applymethod.

After evaluating the expression, the $apply method performs a $digest. In the $digest phase the scope examines all of the $watchexpressions and compares them with the previous value. This dirty checking is done asynchronously. This means that assignment such as $scope.username="angular" will not immediately cause a $watch to be notified, instead the $watch notification is delayed until the $digest phase. This delay is desirable, since it coalesces multiple model updates into one $watch notification as well as guarantees that during the $watch notification no other $watches are running. If a $watch changes the value of the model, it will force additional$digest cycle.

  1. Creation

    The root scope is created during the application bootstrap by the $injector. During template linking, some directives create new child scopes.

  2. Watcher registration

    During template linking, directives register watches on the scope. These watches will be used to propagate model values to the DOM.

  3. Model mutation

    For mutations to be properly observed, you should make them only within the scope.$apply(). AngularJS APIs do this implicitly, so no extra $apply call is needed when doing synchronous work in controllers, or asynchronous work with $http, $timeout or $intervalservices.

  4. Mutation observation

    At the end of $apply, AngularJS performs a $digest cycle on the root scope, which then propagates throughout all child scopes. During the $digest cycle, all $watched expressions or functions are checked for model mutation and if a mutation is detected, the $watch listener is called.

  5. Scope destruction

    When child scopes are no longer needed, it is the responsibility of the child scope creator to destroy them via scope.$destroy() API. This will stop propagation of $digest calls into the child scope and allow for memory used by the child scope models to be reclaimed by the garbage collector.

3. Controller

In AngularJS, a Controller is defined by a JavaScript constructor function that is used to augment the AngularJS Scope.

Controllers can be attached to the DOM in different ways. For each of them, AngularJS will instantiate a new Controller object, using the specified Controller’s constructor function:

If the controller has been attached using the controller as syntax then the controller instance will be assigned to a property on the scope.

Use controllers to:

  • Set up the initial state of the $scope object.
  • Add behavior to the $scope object.

Do not use controllers to:

  • Manipulate DOM — Controllers should contain only business logic. Putting any presentation logic into Controllers significantly affects its testability. AngularJS has databinding for most cases and directives to encapsulate manual DOM manipulation.
  • Format input — Use AngularJS form controls instead.
  • Filter output — Use AngularJS filters instead.
  • Share code or state across controllers — Use AngularJS services instead.
  • Manage the life-cycle of other components (for example, to create service instances).

In general, a Controller shouldn’t try to do too much. It should contain only the business logic needed for a single view.

The most common way to keep Controllers slim is by encapsulating work that doesn’t belong to controllers into services and then using these services in Controllers via dependency injection. This is discussed in the Dependency Injection and Services sections of this guide.

4. Directive

At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS’s HTML compiler ($compile) to attach a specified behavior to that DOM element (e.g. via event listeners), or even to transform the DOM element and its children.

AngularJS comes with a set of these directives built-in, like ngBind, ngModel, and ngClass. Much like you create controllers and services, you can create your own directives for AngularJS to use. When AngularJS bootstraps your application, the HTML compilertraverses the DOM matching directives against the DOM elements.

4.1 Creating Directives

First let’s talk about the API for registering directives. Much like controllers, directives are registered on modules. To register a directive, you use the module.directive API. module.directive takes the normalized directive name followed by a factory function. This factory function should return an object with the different options to tell $compile how the directive should behave when matched.

The factory function is invoked only once when the compiler matches the directive for the first time. You can perform any initialization work here. The function is invoked using $injector.invoke which makes it injectable just like a controller.

We’ll go over a few common examples of directives, then dive deep into the different options and compilation process.

Best Practice: In order to avoid collisions with some future standard, it’s best to prefix your own directive names. For instance, if you created a <carousel> directive, it would be problematic if HTML7 introduced the same element. A two or three letter prefix (e.g. btfCarousel) works well. Similarly, do not prefix your own directives with ng or they might conflict with directives included in a future version of AngularJS.

For the following examples, we’ll use the prefix my (e.g. myCustomer).

For the following examples, we’ll use the prefix my (e.g. myCustomer).

Template-expanding directive

Let’s say you have a chunk of your template that represents a customer’s information. This template is repeated many times in your code. When you change it in one place, you have to change it in several others. This is a good opportunity to use a directive to simplify your template.

Let’s create a directive that simply replaces its contents with a static template:

1
2
3
4
5
6
7
8
9
10
11
12
angular.module('docsSimpleDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.directive('myCustomer', function() {
return {
template: 'Name: {{customer.name}} Address: {{customer.address}}'
};
});
1
2
3
<div ng-controller="Controller">
<div my-customer></div>
</div>

Notice that we have bindings in this directive. After $compile compiles and links <div my-customer></div>, it will try to match directives on the element’s children. This means you can compose directives of other directives. We’ll see how to do that in an examplebelow.

In the example above we in-lined the value of the template option, but this will become annoying as the size of your template grows.

Best Practice: Unless your template is very small, it’s typically better to break it apart into its own HTML file and load it with the templateUrl option.

If you are familiar with ngInclude, templateUrl works just like it. Here’s the same example using templateUrl instead:

1
2
3
4
5
6
7
8
9
10
11
12
angular.module('docsTemplateUrlDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.directive('myCustomer', function() {
return {
templateUrl: 'my-customer.html'
};
});
1
2
3
<div ng-controller="Controller">
<div my-customer></div>
</div>
1
Name: {{customer.name}} Address: {{customer.address}}

templateUrl can also be a function which returns the URL of an HTML template to be loaded and used for the directive. AngularJS will call the templateUrl function with two parameters: the element that the directive was called on, and an attr object associated with that element.

The restrict option is typically set to:

  • 'A' - only matches attribute name
  • 'E' - only matches element name
  • 'C' - only matches class name
  • 'M' - only matches comment

These restrictions can all be combined as needed:

  • 'AEC' - matches either attribute or element or class name

Let’s change our directive to use restrict: 'E':

6. Filter

Filters format the value of an expression for display to the user. They can be used in view templates, controllers or services. AngularJS comes with a collection of built-in filters, but it is easy to define your own as well.

The underlying API is the $filterProvider.

Using filters in view templates

Filters can be applied to expressions in view templates using the following syntax:

1
{{ expression | filter }}

E.g. the markup

1
{{ 12 | currency }}

formats the number 12 as a currency using the currency filter. The resulting value is $12.00.

Filters can be applied to the result of another filter. This is called “chaining” and uses the following syntax:

1
{{ expression | filter1 | filter2 | ... }}

Filters may have arguments. The syntax for this is

1
{{ expression | filter:argument1:argument2:... }}

E.g. the markup

1
{{ 1234 | number:2 }}

formats the number 1234 with 2 decimal points using the number filter. The resulting value is 1,234.00.


When filters are executed

In templates, filters are only executed when their inputs have changed. This is more performant than executing a filter on each $digest as is the case with expressions.

There are two exceptions to this rule:

  1. In general, this applies only to filters that take primitive values as inputs. Filters that receive Objects as input are executed on each $digest, as it would be too costly to track if the inputs have changed.
  2. Filters that are marked as $stateful are also executed on each \$digest. See Stateful filters for more information. Note that no AngularJS core filters are $stateful.

Filter components in ng

NameDescription
filterSelects a subset of items from array and returns it as a new array.
currencyFormats a number as a currency (ie $1,234.56). When no currency symbol is provided, default symbol for current locale is used.
numberFormats a number as text.
dateFormats date to a string based on the requested format.
jsonAllows you to convert a JavaScript object into JSON string.
lowercaseConverts string to lowercase.
uppercaseConverts string to uppercase.
limitToCreates a new array or string containing only a specified number of elements. The elements are taken from either the beginning or the end of the source array, string or number, as specified by the value and sign (positive or negative) of limit. Other array-like objects are also supported (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input, it is converted to a string.
orderByReturns an array containing the items from the specified collection, ordered by a comparator function based on the values computed using the expression predicate.

7. Service

Service components in ng

NameDescription
$anchorScrollWhen called, it scrolls to the element related to the specified hash or (if omitted) to the current value of $location.hash(), according to the rules specified in the HTML5 spec.
$animateThe $animate service exposes a series of DOM utility methods that provide support for animation hooks. The default behavior is the application of DOM operations, however, when an animation is detected (and animations are enabled), $animate will do the heavy lifting to ensure that animation runs with the triggered DOM operation.
$animateCssThis is the core version of $animateCss. By default, only when the ngAnimate is included, then the $animateCss service will actually perform animations.
$cacheFactoryFactory that constructs Cache objects and gives access to them.
$templateCache$templateCache is a Cache object created by the $cacheFactory.
$compileCompiles an HTML string or DOM into a template and produces a template function, which can then be used to link scope and the template together.
$controller$controller service is responsible for instantiating controllers.
$documentA jQuery or jqLite wrapper for the browser’s window.document object.
$exceptionHandlerAny uncaught exception in AngularJS expressions is delegated to this service. The default implementation simply delegates to $log.error which logs it into the browser console.
$filterFilters are used for formatting data displayed to the user.
$httpParamSerializerDefault $http params serializer that converts objects to strings according to the following rules:
$httpParamSerializerJQLikeAlternative $http params serializer that follows jQuery’s param() method logic. The serializer will also sort the params alphabetically.
$httpThe $http service is a core AngularJS service that facilitates communication with the remote HTTP servers via the browser’s XMLHttpRequest object or via JSONP.
$xhrFactoryFactory function used to create XMLHttpRequest objects.
$httpBackendHTTP backend used by the service that delegates to XMLHttpRequest object or JSONP and deals with browser incompatibilities.
$interpolateCompiles a string with markup into an interpolation function. This service is used by the HTML $compileservice for data binding. See $interpolateProvider for configuring the interpolation markup.
$intervalAngularJS’s wrapper for window.setInterval. The fn function is executed every delaymilliseconds.
$jsonpCallbacksThis service handles the lifecycle of callbacks to handle JSONP requests. Override this service if you wish to customise where the callbacks are stored and how they vary compared to the requested url.
$locale$locale service provides localization rules for various AngularJS components. As of right now the only public api is:
$locationThe $location service parses the URL in the browser address bar (based on the window.location) and makes the URL available to your application. Changes to the URL in the address bar are reflected into $location service and changes to $location are reflected into the browser address bar.
$logSimple service for logging. Default implementation safely writes the message into the browser’s console (if present).
$parseConverts AngularJS expression into a function.
$qA service that helps you run functions asynchronously, and use their return values (or exceptions) when they are done processing.
$rootElementThe root element of AngularJS application. This is either the element where ngApp was declared or the element passed into angular.bootstrap. The element represents the root element of application. It is also the location where the application’s $injector service gets published, and can be retrieved using $rootElement.injector().
$rootScopeEvery application has a single root scope. All other scopes are descendant scopes of the root scope. Scopes provide separation between the model and the view, via a mechanism for watching the model for changes. They also provide event emission/broadcast and subscription facility. See the developer guide on scopes.
$sceDelegate$sceDelegate is a service that is used by the $sce service to provide Strict Contextual Escaping (SCE) services to AngularJS.
$sce$sce is a service that provides Strict Contextual Escaping services to AngularJS.
$templateRequestThe $templateRequest service runs security checks then downloads the provided template using$http and, upon success, stores the contents inside of $templateCache. If the HTTP request fails or the response data of the HTTP request is empty, a $compile error will be thrown (the exception can be thwarted by setting the 2nd parameter of the function to true). Note that the contents of $templateCache are trusted, so the call to $sce.getTrustedUrl(tpl) is omitted when tpl is of type string and $templateCache has the matching entry.
$timeoutAngularJS’s wrapper for window.setTimeout. The fn function is wrapped into a try/catch block and delegates any exceptions to $exceptionHandler service.
$windowA reference to the browser’s window object. While window is globally available in JavaScript, it causes testability problems, because it is a global variable. In AngularJS we always refer to it through the$window service, so it may be overridden, removed or mocked for testing.

More

Guide to AngularJS Documentation