<article class=“article fmt article-content”><p><em>作者:徐海峰</em><br/>就在前几天(2022-11-07) Angular 正式公布了 v15 版本,自己第一工夫用我那不业余的英文翻译了一下 [译] Angular 15 正式公布! 文章一出就受到社区局部人的质疑,什么 “Angular 落寞很久了,劝咱们换框架”,还有什么 “这玩意竟然能够活到 2022 年,远古生物忽然复活”。 对此呢我也不想过多的评估,我只在乎通过工具是否扭转我司前端的开发效率,让每位共事早点上班,高质量实现指标,让 PingCode 远超竞品。<br/>略微相熟 Angular 框架的人应该都晓得, Angular 是一个齐全遵循语义化版本的框架,每年会固定公布2个主版本,目前尽管曾经是 v15,但根本和 v2 版本保持一致的主旋律,那么 v15 能够说是 Angular 框架在尝试扭转迈出的一大步,独立组件 APIs 正式稳固,指令的组合 API 以及很多个性的简化。<br/>尽管很多 Angular 开发者曾经习惯了 NgModules,然而 Angular 模块对于老手来说确实会带来一些学习老本,对于很多小我的项目而言带来的收益比并不高,所以反对独立组件(无 Modules) 也算是笼罩了更多的场景,同时也简化了学习老本,那么明天我想通过这篇文章以最小化的示例重新学习一下 Angular 框架,让不理解 Angular 的人重新认识一下这个 “<del>远古的生物</del>"<br/></p><h2>创立一个独立组件的我的项目</h2><p>首先通过如下<code>ng new</code> 命令创立一个 <code>ng-relearning</code> 的示例我的项目:</p><pre><code class=“JavaScript”>ng new ng-relearning –style scss –routing false</code></pre><blockquote>ng 命令须要通过 npm install @angular/cli -g 全局装置 @angular/cli 模块才能够应用。</blockquote><p>创立后的我的项目目录如下:</p><pre><code class=“JavaScript”>.├── README.md├── angular.json├── package.json├── src│ ├── app│ │ ├── app.component.html│ │ ├── app.component.scss│ │ ├── app.component.ts│ │ ├── app.component.spec.ts│ │ └── app.module.ts│ ├── assets│ ├── favicon.ico│ ├── index.html│ ├── main.ts│ └── styles.scss├── tsconfig.app.json├── tsconfig.json└── tsconfig.spec.json</code></pre><p>默认生成文件的介绍见下方表格,曾经相熟 Angular 的开发者能够跳过,相比拟其余框架 CLI 生成的目录构造而言,我集体感觉 Angular 的最正当也是最优雅的(纯个人见解)。<br/><br/>目前 <code>ng new</code> 命令初始化的我的项目还是带 Modules 的,反对 <code>–standalone</code> 参数创立独立组件的我的项目个性正在开发中,能够关注 此 Issue 。<br/>把默认的我的项目改为独立组件须要做如下几件事: <br/>手动删除 <code>app.module.ts</code> <br/>启动组件 AppComponent 中 @Component 元数据增加 <code>standalone: true</code> 并增加 <code>imports: [CommonModule]</code> <br/>批改 main.ts 代码为:</p><pre><code class=“JavaScript”>import { bootstrapApplication } from ‘@angular/platform-browser’;import { AppComponent } from ‘./app/app.component’;bootstrapApplication(AppComponent).catch((err) => console.error(err));</code></pre><p>这样执行 <code>npm start</code> ,会在本地启动一个 4200 端口的服务,拜访 http://localhost:4200/ 会展现 Angular 默认的站点:<br/><br/><code>bootstrapApplication</code> 函数启动利用,第一个参数 AppComponent 组件是启动组件,一个利用至多须要一个启动组件,也就是根组件,这个根组件选择器为 <code>app-root</code> ,那么生成的 index.html 会有对应的 <code><app-root></code> 占位元素,Angular 启动时会把它替换为 AppComponent 渲染的 DOM 元素。</p><pre><code class=“JavaScript”><!doctype html><html lang=“en”><head> <meta charset=“utf-8”> <title>NgRelearning</title> <base href=”/"> <meta name=“viewport” content=“width=device-width, initial-scale=1”> <link rel=“icon” type=“image/x-icon” href=“favicon.ico”></head><body> <app-root></app-root></body></html></code></pre><p>为了让 Angular 更容易学习和上手,Angular CLI 在 v15 初始化的我的项目做了一些简化(大家能够疏忽):</p><ul><li>去掉了独立的 <code>polyfills.ts</code> 文件,应用 angular.json <code>build</code> 选项 <code>“polyfills”: [ “zone.js”]</code> 代替</li><li>去掉了 <code>environments</code> 环境变量, 这个性能还在,当你须要的时候独自配置 <code>fileReplacements</code> 即可</li><li>去掉了 main.ts 中的 <code>enableProdMode</code> </li><li>去掉了 .browserslistrc</li><li>去掉了 karma.conf.js</li><li><p>去掉了 test.ts</p><h2>Hello Angular Relearning</h2><p>因为 <code>app.component.html</code> 的示例太简单,为了简化学习,咱们尝试删除 html 所有内容,批改为绑定 title 到 h2 模板元素中,应用双花括号 <code>{{</code> 和 <code>}}</code> 将组件的变量 <code>title</code> 动静插入到 HTML 模板中,这种叫 <code>文本插值</code> ,花括号两头的局部叫插值表达式。<br/><code><h2>{{title}}</h2></code><br/>同时批改组件类的代码,增加 <code>title</code> 属性,初始化值为 <code>‘Hello Angular Relearning!’</code> </p></li></ul><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;@Component({ selector: ‘app-root’, templateUrl: ‘./app.component.html’, standalone: true, styleUrls: [’./app.component.scss’],})export class AppComponent { title = ‘Hello Angular Relearning!’;}</code></pre><p>这样关上浏览器,发现 title 被渲染在界面上:<br/><br/>Angular 的组件就是一个一般的 class 类,通过 <code>@Component</code> 装璜器装璜后就变成了组件,通过装璜器参数能够设置选择器(selector)、模板(templateUrl 或者 template)、款式(styleUrls 或者 styles),组件模板中能够间接绑定组件类的公开属性。<br/>默认模板和款式是独立的一个 html 文件,如果是一个很简略的组件,也能够设置内联模板,上述示例能够简化为:</p><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;@Component({ selector: ‘app-root’, template: <h2>{{title}}</h2>, standalone: true, styleUrls: [’./app.component.scss’],})export class AppComponent { title = ‘Hello Angular Relearning!’;}</code></pre><h2>条件判断</h2><p>在理论的利用中会常常用到的就是条件判断,咱们批改一下 <code>app.component.ts</code> 为:</p><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;@Component({ selector: ‘app-root’, templateUrl: ‘./app.component.html’, standalone: true, styleUrls: [’./app.component.scss’], imports: [ CommonModule, ],})export class AppComponent { relearned = false; constructor() { setTimeout(() => { this.relearned = true; }, 2000); }}</code></pre><p>组件类中新增了 <code>relearned</code> 属性,默认为 false,setTimeout 2s 后设置 <code>relearned</code> 值为 true。<br/> <code>app.component.html</code> 批改为:</p><pre><code class=“JavaScript”><p *ngIf=“relearned else default”>很快乐看到你重新学习 Angular 这款优良的框架!</p><ng-template #default> <p>我还没有接触过 Angular 框架</p></ng-template></code></pre><ul><li><code>*ngIf</code> 为 Angular 内置的条件判断结构型指令,当 ngIf 的表达式为 true 时渲染此模板,否则不渲染,那么示例中的表达式为 <code>“relearned”</code> ,也就是 AppComponent 组件中的 relearned 属性</li><li>else 示意表达式为 false 后的模板,通过 ng-template 定义了一个默认模板,并通过 #default 申明这个模板变量为 default,这样 <code>ngIf</code> else 就能够应用这个变量 default</li><li>ng-template 是 Angular 定义的一个模板,模板默认不会渲染,ngIf 指令会在表达式为 else 的时候通过 createEmbeddedView 创立内嵌视图渲染这个模板,同时也能够通过 <code>NgTemplateOutlet</code> 指令渲染模板 <br/>展现成果为:<br/><br/>在 AppComponent 中咱们设置了 <code>imports: [CommonModule]</code> ,如果去掉这行代码会报错:<br/><br/>这是因为独立组件的模板中应用其余指令或者组件的时候须要显示的通过 <code>imports</code> 申明, <code>ngIf</code> 结构性指令是在 <code>@angular/common</code> 模块中提供的,如需应用须要导入:</li></ul><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;import { NgIf } from ‘@angular/common’;@Component({ … imports: [NgIf]})export class AppComponent {}</code></pre><p> <code>@angular/common</code> 模块除了提供 <code>NgIf</code> 内置指令外还有大家罕用的 <code>NgClass</code> 、 <code>NgFor</code> 、 <code>NgSwitch</code> 、 <code>NgStyle</code> 等等,所以间接把整个 CommonModule 都导入进来,这样在组件模板中就能够应用 CommonModule 模块的任意指令。</p><h2>事件绑定</h2><p>咱们接下来通过如下 ng 命令创立一个新组件 <code>event-binding</code> 改善了一下上述的示例:</p><pre><code class=“JavaScript”>ng generate component event-binding –standalone // 简写 ng g c event-binding –standalone</code></pre><p>执行后会在 src/app 文件夹下创立一个 event-binding 文件夹寄存新增的 event-binding 组件</p><pre><code class=“JavaScript”>├── src│ ├── app│ │ ├── app.component.html│ │ ├── app.component.scss│ │ ├── app.component.spec.ts│ │ ├── app.component.ts│ │ └── event-binding│ │ ├── event-binding.component.html│ │ ├── event-binding.component.scss│ │ └── event-binding.component.ts│ ├── …</code></pre><p>批改 <code>event-binding.component.html</code> 增加一个按钮,绑定一个点击事件,同时在模板中通过 <code>*ngIf=“relearned”</code> 语法增加一个条件判断。</p><pre><code class=“JavaScript”><p *ngIf=“relearned”>很快乐看到你重新学习 Angular 这款优良的框架!</p><button (click)=“startRelearn()">开始重学 Angular</button></code></pre><p>EventBindingComponent 组件中增加一个 <code>relearned</code> 属性和 <code>startRelearn</code> 函数:</p><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;@Component({ selector: ‘app-event-binding’, standalone: true, imports: [CommonModule], templateUrl: ‘./event-binding.component.html’, styleUrls: [’./event-binding.component.scss’]})export class EventBindingComponent { relearned = false; startRelearn() { this.relearned = true; }}</code></pre><p>最初在 AppComponent 组件中导入 EventBindingComponent,并在模板中插入 <code><app-event-binding></app-event-binding></code> ,运行的成果如下:<br/><br/>当用户点击按钮时会调用 <code>startRelearn</code> 函数,设置 <code>relearned</code> 属性为 true,模板中的 ngIf 构造指令检测到数据变动,渲染 p 标签。<br/> <code>(click)=“startRelearn()"</code> 为 Angular 事件绑定语法, <code>()</code> 内为绑定的事件名称,<code> =</code> 号右侧为模板语句,此处的模板语句是调用组件内的 <code>startRelearn()</code> 函数,当然此处的模板语句能够间接改为 <code> (click)=“relearned = true”</code> ,浏览器所有反对的事件都能够通过 () 包裹应用。</p><h2>循环渲染列表</h2><p>除了条件判断与事件绑定外,利用中最罕用的就是循环渲染元素,咱们通过如下命令创立一个 ng-for 组件</p><pre><code class=“JavaScript”>ng generate component ng-for –standalone</code></pre><p>同时在组件中新增 <code>items</code> 属性,设置为数组。</p><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;@Component({ selector: ‘app-ng-for’, standalone: true, imports: [CommonModule], templateUrl: ‘./ng-for.component.html’, styleUrls: [’./ng-for.component.scss’],})export class NgForComponent { items = [ { id: 1, title: ‘Angular 怎么不火呢?’, }, { id: 2, title: ‘Angular 太牛逼了!’, }, { id: 3, title: ‘优良的前端工程师和框架无关,然而 Angular 会让你更快的成为优良前端工程师!’, }, ];}</code></pre><p>最初在 AppComponent 中导入 <code>NgForComponent</code> 后在模板中通过 <code><app-ng-for></app-ng-for></code> 渲染 NgForComponent 组件。</p><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;import { NgForComponent } from ‘./ng-for/ng-for.component’;@Component({ selector: ‘app-root’, templateUrl: ‘./app.component.html’, standalone: true, imports: [CommonModule, NgForComponent], styleUrls: [’./app.component.scss’],})export class AppComponent { message = ‘Hello Angular Relearning!’; relearned = false; startRelearn() { this.relearned = true; }}</code></pre><p>渲染后的成果为:<br/></p><h2>路由</h2><p>以上咱们简略增加了三个示例组件:</p><ul><li> <code>event-binding</code> 展现事件绑定</li><li> <code>ng-for</code> 展现循环渲染一个列表</li><li>咱们再把条件判断的示例从 AppComponent 中挪动到独立的示例 <code>ng-if</code> 组件中</li></ul><p>接下来通过 router 路由模块别离展现这三个示例,首先须要批改 main.ts,bootstrapApplication 启动利用的第二个参数,通过 <code>provideRouter(routes)</code> 函数提供路由赋值给 providers ,routes 设置三个示例组件的路由,输出根路由的时候跳转到 ng-if 路由中,代码如下:</p><pre><code class=“JavaScript”>import { bootstrapApplication } from ‘@angular/platform-browser’;import { provideRouter, Routes } from ‘@angular/router’;import { AppComponent } from ‘./app/app.component’;import { NgForComponent } from ‘./app/ng-for/ng-for.component’;import { EventBindingComponent } from ‘./app/event-binding/event-binding.component’;import { NgIfComponent } from ‘./app/ng-if/ng-if.component’;const routes: Routes = [ { path: ‘’, redirectTo: ’ng-if’, pathMatch: ‘full’, }, { path: ’ng-if’, component: NgIfComponent, }, { path: ’event-binding’, component: EventBindingComponent, }, { path: ’ng-for’, component: NgForComponent, },];bootstrapApplication(AppComponent, { providers: [provideRouter(routes)],}).catch((err) => console.error(err));</code></pre><p>在 AppComponent 根组件中导入 <code>RouterModule</code> 模块。</p><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;import { RouterModule } from ‘@angular/router’;@Component({ selector: ‘app-root’, templateUrl: ‘./app.component.html’, standalone: true, styleUrls: [’./app.component.scss’], imports: [CommonModule, RouterModule],})export class AppComponent { title = ‘Hello Angular Relearning!’; constructor() {}}</code></pre><p>这样在根组件的模板中就能够应用 <code>router-outlet</code> 和 <code>routerLink</code> 组件或者指令。</p><pre><code class=“JavaScript”><h2>{{ title }}</h2><div> <a [routerLink]=”[’./ng-if’]">NgIf</a> | <a [routerLink]=”[’./event-binding’]">EventBinding</a> | <a [routerLink]="[’./ng-for’]">NgFor</a></div><router-outlet></router-outlet></code></pre><ul><li>router-outlet 为路由占位器,Angular 会依据以后的路由器状态动静渲染对应的组件并填充它的地位</li><li><p>routerLink 让 a 标签元素成为开始导航到某个路由的链接,关上链接会在页面上的 router-outlet 地位上渲染对应的路由组件<br/>运行成果如下:<br/><br/>示例代码: ng-relearning v0.4 分支</p><h2>HttpClient 近程调用</h2><p>在 Angular 中内置了近程调用的 <code>HttpClient</code> 模块,能够间接通过此模块调用 API。<br/>批改 ng-for 的示例,在 NgForComponent 组件中通过构造函数注入 <code>HttpClient</code> 服务,并调用 HttpClient 的 get 函数获取 todos 列表。</p></li></ul><pre><code class=“JavaScript”>import { Component, OnInit } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;import { HttpClient } from ‘@angular/common/http’;export interface Todo { id?: string; title: string; created_by?: string;}@Component({ selector: ‘app-ng-for’, standalone: true, imports: [CommonModule], templateUrl: ‘./ng-for.component.html’, styleUrls: [’./ng-for.component.scss’],})export class NgForComponent implements OnInit { todos!: Todo[]; constructor(private http: HttpClient) {} ngOnInit(): void { this.http .get<Todo[]>(‘https://62f70d4273b79d015352b5e5.mockapi.io/items') .subscribe((items) => { this.todos = items; }); }}</code></pre><p>个别组件初始化的工作举荐放在 ngOnInit 生命周期函数中,比方初始化数据等。Angular 会在组件所有的 Input 属性第一次赋值后调用 ngOnInit 函数,生命周期更多理解参考: lifecycle-hooks 。<br/>而后在模板中通过 *ngIf 判断数据是否有值显示加载状态。</p><pre><code class=“JavaScript”><ol *ngIf=“todos else loading”> <li *ngFor=“let item of todos”> {{ item.title }} </li></ol><ng-template #loading> <p>加载中…</p></ng-template></code></pre><p>运行后发现代码报错,没有 HttpClient 的 provider 。<br/><br/>咱们须要批改 main.ts 增加 <code>provideHttpClient()</code> 到 providers 中去,这样才能够在零碎中通过依赖注入应用 http 相干的服务。</p><blockquote>留神:http 服务在 <code>@angular/common/http</code> 模块中。</blockquote><pre><code class=“JavaScript”>import { bootstrapApplication } from ‘@angular/platform-browser’;import { provideRouter, Routes } from ‘@angular/router’;import { provideHttpClient } from ‘@angular/common/http’;import { AppComponent } from ‘./app/app.component’;…const routes: Routes = [ …];bootstrapApplication(AppComponent, { providers: [provideRouter(routes), provideHttpClient()],}).catch((err) => console.error(err));</code></pre><p>运行成果为:<br/></p><h2>表单</h2><p>通过如下命令创立一个 forms 表单示例组件<br/><code>ng g c forms –standalone –skip-tests</code><br/>批改 FormsComponent 为如下代码,增加 user 对象蕴含 name 和 age,同时增加 save 函数传入 form,验证通过后弹出提交胜利提醒。</p><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;import { FormsModule, NgForm } from ‘@angular/forms’;@Component({ selector: ‘app-forms’, standalone: true, imports: [CommonModule, FormsModule], templateUrl: ‘./forms.component.html’, styleUrls: [’./forms.component.scss’],})export class FormsComponent { user = { name: ‘’, age: ‘’, }; save(form: NgForm) { if (form.valid) { alert(‘Submit success’); } }}</code></pre><p>forms.component.html 编写一个表单,蕴含 name 输入框和 age 数字输入框,通过 <code>[(ngModel)]</code> 双向绑定到 user 对象的 name 和 age,设置 name 输入框为 required 必填,age 数字输入框最大值和最小值为 100 和 1,最终增加 type=“submit” 的提交按钮并在 form 上绑定 <code>(submit)=“save(userForm)"</code> 提交事件。</p><pre><code class=“JavaScript”><form name=“user-form” #userForm=“ngForm” (submit)=“save(userForm)"> <input name=“name” #userName=“ngModel” [(ngModel)]=“user.name” required=”” placeholder=“请输出用户名” /> <input name=“age” type=“number” #userAge=“ngModel” [(ngModel)]=“user.age” max=“100” min=“1” placeholder=“请输出年龄” /> <button type=“submit”>Submit</button> <div *ngIf=“userForm.invalid”> <div *ngIf=“userName.invalid”>用户名不能为空</div> <div *ngIf=“userAge.invalid”>年龄必须在 1-100 之间</div> </div></form></code></pre><p>运行成果为:<br/><br/><code>[()]</code> 是 Angular 双向绑定的语法糖, <code>[(ngModel)]=“value”</code> 相当于</p><pre><code class=“JavaScript”><input [ngModel]=“value” (ngModelChange)=“value = $event” /></code></pre><p>只有组件有一个输出参数和输入事件,且命名合乎 <code>xxx</code> 和 <code>xxxChange</code> ,这个 xxx 能够是任何值,这样就能够通过 <code>[(xxx)]=“value”</code> 这样的语法糖应用,ngModel 是 Angular Forms 表单内置的一个合乎这种规定的语法糖指令,理解更多查看: two-way-binding 。</p><h2>应用第三方类库 material</h2><p>通过如下命令引入 material 组件库。<br/><code>ng add @angular/material</code><br/>依据提醒抉择款式以及其余选项,最终装置依赖并主动批改代码:</p><ul><li>批改 package.json 的 dependencies 增加 <code> @angular/cdk</code> 和 <code>@angular/material</code> </li><li>会主动引入抉择的 theme 主题,在 angular.json 文件中增加 styles</li><li>批改 main.ts 导入 BrowserAnimationsModule (这是因为抉择了反对动画)</li><li>引入 google 字体和款式</li></ul><p>次要变更如下:<br/><br/>让咱们在之前的 forms 示例中导入 <code>MatButtonModule</code> 和 <code>MatInputModule</code> 模块,应用表单和按钮组件丑化一下界面。</p><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;import { FormsModule, NgForm } from ‘@angular/forms’;import { MatButtonModule } from ‘@angular/material/button’;import { MatInputModule } from ‘@angular/material/input’;@Component({ selector: ‘app-forms’, standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatInputModule], templateUrl: ‘./forms.component.html’, styleUrls: [’./forms.component.scss’],})export class FormsComponent { user = { name: ‘’, age: ‘’, }; save(form: NgForm) { if (form.valid) { alert(‘Submit success’); } }}</code></pre><p>forms.component.html 批改为:</p><pre><code class=“JavaScript”><form name=“user-form” #userForm=“ngForm” (submit)=“save(userForm)"> <mat-form-field appearance=“fill”> <mat-label>Name</mat-label> <input matInput name=“username” #userName=“ngModel” [(ngModel)]=“user.name” required=”" placeholder=“请输出用户名” /> </mat-form-field> <mat-form-field appearance=“fill”> <mat-label>Age</mat-label> <input matInput name=“age” type=“number” #userAge=“ngModel” [(ngModel)]=“user.age” max=“100” min=“1” placeholder=“请输出年龄” /> </mat-form-field> <div class=“error-messages” *ngIf=“userForm.invalid”> <div *ngIf=“userName.invalid”>用户名不能为空</div> <div *ngIf=“userAge.invalid”>年龄必须在 1-100 之间</div> </div> <button mat-raised-button color=“primary” type=“submit”>Submit</button></form></code></pre><p>最终的丑化成果为:<br/><br/>同时也应用 <code>MatTabsModule</code> 替换了之前导航链接。<br/>留神:因为通过 <code>ng add @angular/material</code> 装置 <code>material</code> 后批改了 angular.json 文件,须要重新启动才能够看到新的款式,Angular CLI 目前还没有做到 angular.json 变动后实时更新。</p><h2>应用服务</h2><p>在 Angular 中举荐应用服务把相干业务逻辑从组件中独立进来,让组件只关注视图,咱们革新一下 ng-for 示例,先通过以下命令创立一个服务:<br/><code>ng g s todo –skip-tests</code><br/>Angular CLI 会主动帮咱们在 app 目录创立一个 <code>todo.service.ts</code> 文件,代码为:</p><pre><code class=“JavaScript”>import { Injectable } from ‘@angular/core’;@Injectable({ providedIn: ‘root’})export class TodoService { constructor() { }}</code></pre><p>批改代码通过构造函数注入 <code>HttpClient</code> 服务,并增加 <code>fetchTodos</code> 函数调用 HttpClient 的 get 函数获取 todos 列表,并在最初赋值给服务的 todos 属性。</p><pre><code class=“JavaScript”>import { Injectable } from ‘@angular/core’;import { HttpClient } from ‘@angular/common/http’;import { tap } from ‘rxjs’;export interface Todo { id?: string; title: string; created_by?: string;}@Injectable({ providedIn: ‘root’,})export class TodoService { todos!: Todo[]; constructor(private http: HttpClient) {} fetchTodos() { return this.http .get<Todo[]>(‘https://62f70d4273b79d015352b5e5.mockapi.io/items') .pipe( tap((todos) => { this.todos = todos; }) ); }}</code></pre><p>而后咱们批改 ng-for 的示例,这次通过 <code>inject</code> 函数在属性初始化的时注入 TodoService 服务,在初始化时调用 fetchTodos 获取数据。</p><blockquote>Angular 在 v14 之前只能通过结构函数参数注入服务,在 v14 版本之后能够在属性初始化、构造函数以及 factory 函数中通过 <code>inject</code> 注入服务或者其余供应商,理解更多参考: inject</blockquote><pre><code class=“JavaScript”>import { Component, inject, OnInit } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;import { Todo, TodoService } from ‘../todo.service’;@Component({ selector: ‘app-ng-for’, standalone: true, imports: [CommonModule], templateUrl: ‘./ng-for.component.html’, styleUrls: [’./ng-for.component.scss’],})export class NgForComponent implements OnInit { todoService = inject(TodoService); constructor() {} ngOnInit(): void { this.todoService.fetchTodos().subscribe(); }}</code></pre><p>而后在模板中间接应用 <code>todoService</code> 的 todos 数据。</p><pre><code class=“JavaScript”><ol *ngIf=“todoService.todos else loading”> <li *ngFor=“let item of todoService.todos”> {{ item.title }} </li></ol><ng-template #loading> <p>加载中…</p></ng-template></code></pre><p>通过上述示例咱们发现,在 Angular 中把数据和逻辑抽取到服务中,只有设置为组件的属性就能够在模板中绑定服务的数据(也就是状态),这些状态变动后,Angular 视图会实时更新,同时只有多个组件注入同一个服务就实现了数据共享,这样轻松解决了前端的<strong>逻辑复用</strong>和<strong>数据共享</strong>两大难题,这一点和其余框架有很大的不同:</p><ul><li>React 必须要通过 setState 或者 Hooks 的 set 函数设置状态才会从新渲染</li><li>Vue 必须定义在 data 数据中或者通过 ref 标记</li></ul><p>Angular 什么也不须要做是因为通过 Zone.js 拦挡了所有的 Dom 事件以及异步事件,只有有 Dom Event、Http 申请,微工作、宏工作都会触发脏查看,从根组件始终查看到所有叶子组件,只有有数据变动就会更新视图,那么上述的示例中,fetchTodos 函数有一个 API GET 申请,这个申请被 Angular 拦挡,申请完结后赋值 todos 数据后,Angular 就开始从 app-root 根组件向下查看,发现 todos 数据变动了,更新视图。</p><h2>指令组合 API (Directive Composition API)</h2><p>通过服务在 Angular 中能够很好做逻辑复用,然而对于一些偏 UI 操作的逻辑复用,有时候应用服务会多加一些样板代码,因为在 Angular 中除了组件还有一个指令的概念,指令是对曾经的组件或者元素增加行为,咱们在后面示例中应用的 <code>NgIf</code> 、 <code>NgFor</code> 、 <code>NgModel</code> 都是内置的指令,有时候须要反复利用不同的指令组合,如果要实现逻辑复用只能通过 Class 继承和 Mixin 实现相似多重继承的成果,那么指令组合 API 就是解决此类问题的。<br/>让我先通过如下命令创立一个 <code>color</code> 设置文本色彩的指令:<br/><code>ng g d color –standalone –skip-tests</code><br/>而后批改 <code>color.directive.ts</code> 代码如下:</p><pre><code class=“JavaScript”>import { Directive, ElementRef, Input, OnInit, Renderer2 } from ‘@angular/core’;@Directive({ selector: ‘[appColor]’, standalone: true,})export class ColorDirective implements OnInit { @Input() color!: string; constructor(private elementRef: ElementRef, private renderer: Renderer2) {} ngOnInit(): void { this.renderer.setStyle(this.elementRef.nativeElement, ‘color’, this.color); }}</code></pre><p>次要通过注入获取 ElementRef,并通过 Renderer2 服务设置 DOM 元素的 color 款式。</p><blockquote>elementRef.nativeElement 就是以后指令绑定的 DOM 元素,通过 Renderer2 操作 DOM 次要是为了兼容不同的环境,比方服务端渲染等。</blockquote><p>这样在 AppComponent 组件中导入 AppColor 后就能够通过如下模板应用:<br/><code><div appColor color=“red”>我是红色</div></code><br/>展现成果为:<br/><br/>那么咱们再创立一个 <code>directive-composition</code> 组件,这个组件的选择器是 <code>app-directive-composition</code> ,如果这个组件也想要具备设置字体色彩的性能,咱们只能这样应用</p><pre><code class=“JavaScript”> <app-directive-composition appColor color=“blue”>我的字体色彩时红色</app-directive-composition></code></pre><p>如果是多个指令,等于须要在应用的中央自行组合,咱们革新一下这个组件,通过 <code>hostDirectives</code> 设置 <code>ColorDirective</code> 并指令 inputs color。</p><pre><code class=“JavaScript”>import { Component } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;import { ColorDirective } from ‘../color.directive’;@Component({ selector: ‘app-directive-composition’, standalone: true, imports: [CommonModule, ColorDirective], template: ‘<ng-content></ng-content>’, styleUrls: [’./directive-composition.component.scss’], hostDirectives: [ { directive: ColorDirective, inputs: [‘color’], }, ],})export class DirectiveCompositionComponent {}</code></pre><p>这样间接应用 <code>app-directive-composition</code> 传入参数 color 就具备了 appColor 指令的性能。</p><pre><code class=“JavaScript”><div appColor color=“red”>我是红色</div><app-directive-composition color=“blue”>我的字体色彩时红色</app-directive-composition></code></pre><p>展现成果如下:<br/><br/>以上就是组合指令 API 的魅力所在。</p><h2>总结</h2><p>以上我是想通过一种渐进式的 Angular 入门让大家初步理解 Angular 这款我认为特地优良的框架,摈弃了 Modules 后也更加适宜新手入门,站在 2022 乃至 2023 年的工夫点来说,它并不是一个落后的框架,反而是更加的先进,同时 Angular 也在一直的变得更好。 同时上述的性能只是 Angular 框架的冰山一角,深刻后还有更有的宝藏等着你去开掘。<br/>以上所有示例仓储地址为: https://github.com/why520crazy/ng-relearning <br/>Open in StackBlit: https://stackblitz.com/github/why520crazy/ng-relearning</p></article>
...