This article was originally published in Inside Angular 2 by Publishing House of Electronics Industry, and is based on chapter 5 of the Overview. About the author: The Internet Finance technology team of Gf Securities is an early firm practitioner of Angular. As a new generation of IT research and development organization in the securities industry, the team is committed to creating industry solutions with newer and better technologies and supporting business innovation. Please email chenqg#csdn.net or add wechat: Rachel_qg. For more cutting-edge technology information and in-depth technical articles, please follow CSDN r&d channel weibo.
Angular 2.0 was released last September.
While React/Vue is all the rage at the same time and the community is all the rage at the same time, their advantages are self-evident. However, this article is not about React/Vue, nor is it about competing with one another. This article is about Angular itself. Talk about Angular as a belated platform.
When the new Angular version is released, the official name AngularJS is no longer used. Instead, it is recommended to use Angular directly. AngularJS has changed to refer exclusively to the first-generation framework, so all Angular uses refer to Angular 2+.
We know that Angular has completely rewritten AngularJS, which is somewhat inconvenient to the community, but the Angular team has worked hard to make the upgrade seamless and, more importantly, the rewrite allows Angular to get rid of the legacy. Angular code designed with the new architecture is cleaner, easier to read, more efficient, and more in line with new trends such as component-based design and responsive programming. In addition, Angular is applicable to a wide range of scenarios, such as server-side rendering, better adaptation to Mobile Applications, and offline compilation. This article focuses on the core concepts of Angular itself to help you get started with the Angular platform, so the depth and breadth of the article has been carefully considered for beginners to Angular.
The core concept
The Official Angular documentation lists eight core concepts: modules, components, templates, metadata, data binding, directives, services, and dependency injection. If you’re familiar with React/Vue development, some of the concepts are quite similar. If you put these concepts together and give an overview of where each concept fits into the application, it looks something like this:
With the help of the figure above, let’s take a look at these core concepts:
- Directly interacting with the user is a template, which is not a stand-alone module, but one of the elements that make up a component. Another element is the component class, which maintains the data model and functional logic of the component.
- Templates are specified by metadata, which also contains other important information that tells Angular how to interpret a common class. As shown in the figure above, metadata combines common classes to form components.
- Directives are separate components of Angular that are closely associated with templates to enhance their features and indirectly extend their syntax.
- Services are also standalone components of Angular. They are units that encapsulate a single functional logic, usually providing functional extensions to components.
- In order for a service to be available to a component, it is introduced into a component through dependency injection. A service can be injected into a component alone or into a module. The two injection methods make the scope of the service different, which will be explained later.
Angular has many concepts that are easy to understand, and the most important of them is components. Throughout the Angular application, components are always at the entry and exit of this interaction as they receive user instructions, process them, and output the corresponding view. This is the embodiment of Angular component-based design. These core concepts are unveiled one by one, starting with components.
component
The Angular framework is based on component design, and applications are made up of a series of large and small components. The Angular component is similar to the React/Vue component. Here is an example of the address book in The Angular 2 book:
All of the boxed parts are rendered by the corresponding components, and these components are nested in layers to form the component tree from top to bottom. For example, the outermost box is the root component, which contains the Header, ContactList, and Footer child components. The ContactList has its own child components.
Focusing on individual components, each Angular component contains HTML (templates) and CSS code in addition to its own JavaScript logic. So each component not only has its own separate business logic, but also has its own view layer to render itself.
Here is a simple sample code for the Contact component:
import { Component } from '@angular/core'; @component ({selector: 'contact', template: '<p> constructor ') Class ContactComponent {constructor() {}}Copy the code
You can see that an Angular Component consists of two parts, @Component() and TS/ES6 classes. The TS/ES6 class is the business logic that handles components, while the @Component() part is called a decorator. Decorators are TypeScript language features that inject classes, functions, and so on with additional information that is essentially the core Angular concept of metadata. In Angular, metadata is specified as a decorator function parameter. In this example, we define two important metadata: selector and template. Template is a CSS3 selector, and the application runtime matches the DOM element on the template.
Decorator is actually a custom function, presents of all sorts of adornment processing logic in presents source: modules / @ presents/core/SRC/util/decorators ts.
The schematic diagram of the relationship is as follows:
The CSS style metadata is introduced as styles, and more component metadata can be viewed here.
If we only define a class, Angular doesn’t know how to interpret it. Angular doesn’t know how to interpret a class as a component until it injects component metadata into it. Similarly, instruction metadata interprets a common class as an instruction.
To see how metadata is injected into a class, dive into reflect-Metadata polyfill.
Although each component does its job, the organization of components as a tree means that components cannot exist in isolation and there is a two-way flow of data between parent and child components. Each component can define its own input and output properties, which become the component’s external interface to interact with the parent component.
Let’s improve the Contact component by adding input and output properties, pseudocode as follows:
// import Component, Input, Output, EventEmitter, etc @Component({ selector: 'contact', template: '<p> </p>'}) export class ContactComponent {@input () item: ContactModel; @output () Update: EventEmitter<ContactModel>; Constructor () {} modify() {//... this.update.emit(newValue); }}Copy the code
@input () and @Output() are also decorators that target class member attributes, while @Component() decorators target classes.
@Input() and @Output declare the Input and Output interfaces of the component Contact, the item variable is used to receive Input from the data source from the parent, and the Update event is used to send data to the parent. The input and output properties are separate, which is slightly different from the React props property.
With the Contact component input and output interfaces defined, we then interact with the parent component ContactList. Sample code for the ContactList parent component is as follows:
// import statement @Component({ selector: 'contact-list', template: ` <! Call contact component with <contact> tag --> <contact [item]="items[0]" (update)="doUpdate(newValue)"></contact> '}) export class ContactListComponent { items: ContactModel[] constructor() {} doUpdate(item: ContactModel) { // ... }}Copy the code
In order for the parent component ContactList template to use the tags defined by the child component Contact directly, there is an import process that depends on the “module” features.
The ContactList component’s template calls the Contact component, where [item] is called a property binding, and the data flows from the parent component to the child component. (Update) is called event binding, and data flows from the child to the parent.
The [] and event bindings () of the property binding cannot be omitted; they are important parts of the syntax.
Property binding and data binding are both called data binding, which is one of the core concepts Angular emphasizes. As the observant reader may have noticed, property bindings and data bindings are member properties that can refer directly to components, such as listItem and doUpdate(). Property binding and event binding are used for data transfer between component data model and template view, as well as data transfer between parent and child components. In the process of communication between parent and child components, template acts as a kind of bridge, connecting the functional logic of the two.
This mode of communication is suitable for components that are not far apart in hierarchy; components that are too deep in hierarchy or have different branches often communicate in other ways, such as using services as intermediaries.
This is Angular’s data flow mechanism. However, flow does not happen spontaneously. Flow needs a driving force, and this driving force is Angular’s change monitoring mechanism. Angular is a responsive system that processes every data change in near real time and updates the corresponding view. So how does Angular sense when a data object changes? ES5 provides a getter/setter language interface to catch object changes, which is the way VueJS does, but Angular does not. Angular checks if an object’s value has been changed at the appropriate time. The appropriate time is not at a fixed frequency, but usually after an asynchronous event such as a user action event (click), setTimeout, or XHR callback is triggered. Angular captures these asynchronous events through the Zones library, as shown in the change-monitoring event diagram:
As you can see from the figure above, each component maintains a separate change monitor behind it, which records the data change status of the owning component. Because applications are organized as component trees, each application also has a corresponding change monitoring tree. When Zones catch an asynchronous event, it usually notifies Angular to perform change-monitoring. Each change-monitoring operation starts with the root component and runs through the leaf component on a depth-first basis, and each component’s change-monitoring is optimized for its component’s data model.
Angular’s powerful data change monitoring mechanism enables developers not to care where or when data is changed, but to find the right time to trigger data detection. When data changes are detected, the combination of data binding drives the real-time update of the template view, which is what we see in the real-time update effect.
So let’s go ahead and say, what if we need to do some extra processing when we see changes in the input data? Angular provides sophisticated lifecycle hooks to solve this problem, such as ngOnChanges, which can catch changes in input data. It’s as simple as defining an instance method with the same name:
export class ContactComponent { @Input() item: ContactModel; @output () Update: EventEmitter<ContactModel>; Constructor () {} modify() {//... this.update.emit(newValue); } ngOnChanges(changes: SimpleChanges) {// Change detection hook, which is triggered when the item value changes // Changes contains state before and after the change //... }}Copy the code
Common lifecycle hooks are:
- Constructors are the first to fire, and you can do some component class initialization, such as class variable initialization, etc.
- The ngOnChanges hook is then fired. This is the first time the ngOnChanges hook is fired and is used to receive incoming data from the parent component for subsequent component initialization.
- Then there’s the ngOnInit hook, which is the actual component initialization phase. Angular doesn’t recommend doing business logic work during constructor initialization. It’s better to do it in ngOnInit.
- The component then enters a stationary phase, during which the ngOnChanges hook can be fired repeatedly. The ngOnChanges hook fires once whenever the data retrieved from the input property changes.
- Finally, the ngOnDestroy hook is triggered just before the component is destroyed, which can be used to do some cleanup at this stage, such as untying events, unsubscriping data, and so on.
That’s a brief introduction to components, along with a brief introduction to metadata and data binding. Templates are important components, and Angular provides powerful features for templates. Move on.
The template
Angular templates are BASED on HTML, and regular HTML can also be entered as templates:
@Component({template: '<p>' </p> '})Copy the code
But Angular templates are more than that. Angular customises a powerful syntax for templates, which is why they are listed separately. Data binding is the most basic function of templates. In addition to the property binding and event binding mentioned above, interpolation is also a common data binding syntax. Example code is as follows:
// import statement
@Component({
selector: 'contact',
template: '<p>{{ item.name }}</p>'
})
export class ContactComponent {
@Input() item: ContactModel;
// ...
}Copy the code
Interpolation syntax consists of a pair of curly braces {{}}. The variable context of interpolation is the component class itself, item in the example above. Interpolation is a one-way flow of data — from the data model to the template view.
The data flow of the three data binding (property binding, event binding, and interpolation) syntaxes mentioned above is one-way, and two-way data flow support is required in certain scenarios (such as forms). Angular templates combine property binding and event binding to enable bidirectional binding, such as:
<input [(ngModel)]="contact.name"></input>Copy the code
[()] is the syntactic sugar to implement bidirectional binding, and ngModel is the built-in instruction to assist in bidirectional binding. After the above code is executed, a two-way data association will be formed between the Input control and contact.name. When the value of the Input changes, it can be automatically assigned to contact.name, and when the value of contact.name is changed by the component class, the value of the Input can be updated in real time.
By the known, data binding is responsible for the transmission and display of data, and formatting of data show, presents provides a named pipe function, using the vertical bar |, the example code is as follows:
<span>{{ contact.telephone | phone }}</span>Copy the code
If the contact.telephone value above is 18612345678, this string of numbers is not very intuitive, the pipe command phone can beautify the output, such as “186-1234-5678”, without affecting the value of contact.name itself. The pipeline supports developer customization, and Phone is a custom pipeline. Angular also provides some basic built-in pipeline commands, such as number to format numbers, date to format dates, and so on.
These are the main syntactic features of Angular templates, and this overview is intended to help you get started. In addition to basic syntactic features, templates have a powerful set of “instructions” mechanisms to simplify specific interaction scenarios such as style processing, data traversal, and form processing.
instruction
Directives are closely related to templates, and they can interact with the DOM flexibly, either changing styles or changing layouts. Developers who are familiar with AngularJS may wonder: Are these directives the same as AngularJS directives? Although Angular directives are similar to AngularJS directives in function, they are not exactly the same concept. Angular directives are very broad, and components are actually directives. The difference between a component and a normal directive is that a component has a separate template, namely a DOM element, while a normal directive acts on an existing DOM element. General instructions are divided into two types: structural instructions and attribute instructions.
Structural directives can add, modify, or delete DOM to change the layout, as ngIf does:
<button *ngIf="canEdit"> </button>Copy the code
When canEdit is true, the button button is displayed on the view; If canEdit is false, the button button is removed from the DOM tree.
Note that the * sign of the structure directive cannot be dropped. This is a syntactic sugar that Angular implements for ease of use.
Attribute directives are used to change the appearance or behavior of an element. They are used much like normal HTML element attributes. For example, the ngStyle directive is used to dynamically evaluate style values.
<span [ngStyle]="setStyles()">{{ contact.name }}</span>Copy the code
The style of the tag is computed by the setStyles() function, which is a member of its component class. SetStyles () returns a computed style object in the following code:
class ContactComponent {
private isImportant: boolean;
setStyles() {
return {
'font-size': '14px',
'font-weight': this.isImportant ? 'bold' : 'normal'
}
}
}Copy the code
The ngIf and ngStyle directives listed above are Angular built-in directives, as are ngFor and ngClass, which provide powerful syntax support for templates. The more attractive aspect of directives is that they allow developers to customize them to maximize logic reuse at the UI level.
For built-in directives such as ngIf and ngStyle to be used directly in component templates, there is a declarative import process, which takes advantage of module features, as described below.
service
A service is a unit that encapsulates a single function. Similar to a tool library, it is often referenced inside a component as an extension of its function. What does the service include? It can be a simple string or JSON data, a function or even a class, and almost any object can be encapsulated as a service. Take the log service as an example. A simple log service is as follows:
// import statement
@Injectable()
export class LoggerService {
private level: string;
setLevel(level: string) {
this.level = level;
}
debug(msg: string) { }
warn(msg: string) { }
error(msg: string) { }
}Copy the code
@Injectable() is the service class decorator.
The service is simple and focused on logging. Every component in an Angular application can reuse this logging service to add new logging capabilities without having to implement them repeatedly. This is the main principle in designing the service. So how are services used by components? This requires the introduction of dependency injection.
Dependency injection
Dependency injection, a concept mentioned in the Services section, has always been Angular’s selling point. Through the dependency injection mechanism, modules such as services can be imported into any component (or module, or other service) without the developer having to care about how these modules are initialized. Because Angular has taken care of it for you, other modules, including those that the module itself depends on, are initialized as well. As shown in the figure below, when the component injects the logging service, the logging service and the underlying services on which it depends are initialized.
Dependency injection is a design pattern that helps developers manage module dependencies. In Angular, dependency injection combined with TypeScript provides a better development experience. In TypeScript, objects are usually explicitly typed, and by type matching, component classes know which type instance to use to assign variables. A simple dependency injection example looks like this:
import {LoggerService} from './logger-service';
// other import statement
@Component({
selector: 'contact',
template: '...'
providers: [LoggerService]
})
export class ContactListComponent {
constructor(logger: LoggerService) {
logger.debug('xxx');
}
}Copy the code
The providers metadata in the @Component decorator is the key to the dependency injection operation, which creates an injector object for the Component and stores a new LoggerService instance in the injector. When a component needs to import an instance of LoggerService, it simply declares a parameter of type LoggerService in the constructor. Angular automatically finds the pre-instantiated LoggerService object in the injector by type matching. Passed in as a parameter when the component is instantiated, the component gets an instance reference to LoggerService.
It is important to note that the injector object created on the component can be reused by the component. This means that we only need to inject the service once on the root component, in the providers declaration of the root component. Components in the whole tree can use the service, keeping the singleton. This feature is useful because it greatly reduces the memory footprint of services, and because services are singletons, when injected into components, they can act as a bridge for data transfer between those components.
It’s not uncommon to see a component branch where I don’t want to use this instance anymore and I want to use a new one. How does Angular handle this? The answer is layered injection. Each component in the component tree can inject a service individually. With each injection of a service (using providers declarations), a new instance of the service is created, which is then used by the component and all its children. Here’s an example:
I injected the LoggerService logging service into the root component and set the log level to warn.
// import statement // root @component ({selector: 'app', template: '... ', providers: [LoggerService] }) class AppComponent { constructor(logger: LoggerService) { logger.setLevel('warn'); }}Copy the code
All components in the component tree can use this warn level logging service:
// ContactList @component ({selector: 'ContactList ', template: '... }) class ContactListComponent {constructor(Logger: LoggerService) {}Copy the code
Next, as the business grows, I would like to have higher level logs (such as debug) on the ContactList branch. Obviously, changing the level in the ContactList component has some side effects:
class ContactListComponent { constructor(logger: LoggerService) { logger.setLevel('debug'); // Logger is a global instance}}Copy the code
The logger he gets is an instance injected by the root component, and calls to setLevel() on any child component are global, causing the root component to output debug messages as well. We need to inject the LoggerService instance into the ContactList component.
// ContactList @component ({selector: 'ContactList ', template: '... Providers: [LoggerService] // re-inject}) Class ContactListComponent {constructor(logger: LoggerService) { logger.setLevel('debug'); }}Copy the code
The ContactList branch uses the new Debug level logging service, while other components such as the root component and Header continue to use the WARN level logging service.
Components are organized as a tree, so that the injector object behind the component can also be abstracted into a tree, called an injection tree. Angular first looks for a matching service instance from the host component’s injector. If it doesn’t find a matching service instance, it looks through the parent component’s injector until it finds the topmost injector. If it doesn’t find a matching service instance, it throws an error. This flexible injection approach can be adapted to a variety of application scenarios, from configuring global singleton services (injected at the root of the application) to injecting different levels of services on demand without affecting each other’s data state.
As mentioned earlier, dependency injection can work on modules as well as components. To understand dependency injection for modules, first understand what modules are. Let’s move on.
The module
First of all, modules have two meanings:
- Framework code is organized as modules (physical modules)
- Functional units are organized as modules (logical modules)
The physical module is a file module feature provided by TS/ES6 and is not the focus of this article. The focus of this article is on the logical module. The following logical module is directly called a module.
A large application consists of a large number of components, instructions, pipelines, service, some of these artifacts are not overlap, and some are working together to accomplish a specific function, we want to put these packages to a piece of related components, formed a relatively independent unit, the unit is called a module in real sense. To put it simply, a module is a functional packaging of scattered components, instructions, and services within an application. The relationship is shown as follows:
In addition, modules have an important practical significance. By default, a component cannot directly reference other components, nor can it directly use the functions of other directives. To use it, you need to import it first. Other parent components have been mentioned earlier, and this import process is implemented by application modules. In summary, a component can use other components and directives of the same module at will. However, the inter-module component instructions cannot be directly used with each other. For example, the components of module A cannot directly use the instructions of module C. To cross-module access, it is necessary to combine the import and export functions of the module.
// import statement @NgModule({imports: [SomeModule], // Import other module declarations: [LoggerService], // dependency injection exports: [SomeComponent, SomeDirective, SomePipe] [SomeComponent, SomeDirective, SomePipe] [AppComponent] // Root component}) export class AppModule {}Copy the code
As you can see, the declaration module uses the @NgModule() decorator. Let’s look at the imports and exports properties. They are the import/export properties of modules. The import/export relationship between modules is shown below:
As can be seen from the figure, module A imports module B, which exposes component B1 and instruction B2 through exports property. Obviously, component B1 and instructions B2 can be used by component A1, but component B3 cannot. As you can see, Angular modules can expose some of the artifacts, but also have a degree of encapsulation that hides some of the implementation inside.
Having covered the components and directives within a module (pipes are accessed in the same way as component directives), let’s look at services, where we take up the water balloon thrown by dependency injection. Services can be injected into components or modules in much the same way, with the difference being scope. All modules share an application-level injector, which means that services injected into any module can be used globally (all modules), while services injected into a component can only be used by that component and its children.
The relationship between an application-level injector and a component-level injector is as follows:
In addition to component level injectors, the child nodes of application level injectors also contain lazy-loaded module level injectors. Lazy-loaded module injectors are generated independently as module-level injectors, which is beyond the scope of this article.
As you can see, the component-level injector is A child of the global injector, so looking back at the example above, service B4 in module B can be used in both module A and module C.
You might wonder if different modules inject services with the same identity, because modules share the same injector, conflicts are inevitable. For example, module A and module C both inject LoggerService, and module A imports module C. Since module C initializes first and then reaches module A, So the LoggerService injected by module A is applied globally. It is important to remember that even if LoggerService is injected into module C, the active instance in that module will be the same one injected into module A. Following this theory, the service injected in the root module is always the highest priority.
Now that we’ve covered the features of modules, let’s look at the best practices Angular recommends for using modules.
First, for Angular to run successfully, at least one module needs to be defined. There needs to be a module that acts as an entry point for the app to start. This module is called the root module.
And then, our apps are constantly adding new features. These additions can be packaged into a new module. These new modules are called feature modules in Angular. With feature modules, the logic that the root module used to carry can also be removed and placed in a feature module, keeping the root module concise.
As we add more and more feature modules, they can be separated into components or directives that have similar functions. These common parts can also be packaged into a separate module that cannot be logically called a feature module; Angular calls it a shared module.
Finally, there are core modules. We know that there are some global components or services in an application that need to be initialized only once when the application is started, such as services that maintain login information, or common header and tail components. Although we could put them in the root module, a better design would be to pull this logic out as well and put it in a separate module, which is the core module. Core modules are required to be imported only into the root module, and not into feature or shared modules, to avoid unexpected results when working together.
This is where Angular recommends best practices. As a result, the root module, which is in command, is very concise, with no tedious business details. , the application features are divided into various large and small modules, the logical structure is very clear.
Angular already encapsulates several common modules, such as:
- ApplicationModule: Encapsulates some startup related tools;
- CommonModule: encapsulates some common built-in directives and built-in pipes, etc.
- BrowserModule: encapsulates some of the runtime libraries in the browser platform. It also packages CommonModule and ApplicationModule, so BrowserModule is usually used.
- FormsModule and ReactiveFormsModule encapsulate form-related component directives, etc.
- RouterModule: Encapsulates routing related component directives.
- HttpModule: Encapsulates network request-related services, etc.
So, if you want to use built-in directives like ngIf and ngStyle, use CommonModule first.
Application startup
As mentioned, Angular boots the application by bootstrapping the root module. There are two ways to bootstrap: dynamically bootstrapping and statically bootstrapping. To understand the difference, start with a brief introduction to the Angular app startup process. Before an Angular app runs, the compiler compiles modules, components, etc., and then launches the app and renders the interface.
The difference between dynamic boot and static boot is the compilation time. Dynamic boot is to load all the code into the browser and compile it in the browser. Static boot, on the other hand, prefixes the compilation process to the project packaging phase at development time, and loads the compiled code into the browser.
Assuming our root module is AppModule, sample code for dynamic boot is as follows:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);Copy the code
Dynamic boot is started from the platformBrowserDynamic function, which is imported from the @angular/ platform-browser-Dynamic file module (about the Angular file module in the next summary). AppModule is the module we wrote for dynamic boot. Let’s look at the sample code for static boot:
import { platformBrowser } from '@angular/platform-browser';
import { AppModuleNgFactory } from './app.module.ngfactory';
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);Copy the code
Static boot starts with the platformBrowser function, which is imported from the @angular/platform-browser file module and is not the same as dynamic boot. Static bootstrapping launches the AppModuleNgFactory module, which is generated by the AppModule after the app.module file is compiled to generate the app.module. ngFactory file. Because the entire application is pre-compiled, the compiler is not packaged into project code, the code package is smaller, loads faster, and the browser compilation step is eliminated, so the application starts faster.
The dynamic boot development process is simple and suitable for small projects or large applications, while the static boot needs to add a precompiled process in the development stage, which is slightly complicated but improves performance significantly. It is recommended to use it at any time.
summary
This is an overview of Angular, which is more like a platform than just a framework. React, Vue, and Angular all solve our major problems. Angular offers a more comprehensive, one-stop solution. With careful architecture, a mature Angular ecosystem, an embrace of standards, and support from Google and Microsoft, developers are confident that Angular is a great platform to try!
Did this article help you? Welcome to join the front End learning Group wechat group: