1. nvm安装
- 说明:一个node的版本管理工具,可以实现node版本的安装、查看、切换等操作,而npm是依赖包的管理工具。
- 下载地址:
-
操作:
nvm ls // 看安装的所有node.js的版本 nvm list available // 查显示可以安装的所有node.js的版本 nvm install // 安装版本号 例如:nvm install 14.xx.x nvm use // 切换到使用指定的nodejs版本 node -v //检测是否切换完成 需要新开一个cmd //nvm命令行操作命令 nvm list //是查找本电脑上所有的node版本 nvm list //查看已经安装的版本 nvm list available //查看网络可以安装的版本 nvm install //安装最新版本nvm nvm use <version> // 切换使用指定的版本node nvm current //显示当前版本 nvm reinstall-packages <version> //在当前版本node环境下,重新全局安装指定版本号的npm包 nvm on //打开nodejs控制 nvm off //关闭nodejs控制 nvm proxy 查看设置与代理 nvm node_mirror [url] //设置或者查看setting.txt中的node_mirror,如果不设置的默认是 https://nodejs.org/dist/ nvm npm_mirror [url] //设置或者查看setting.txt中的npm_mirror,如果不设置的话默认的是: https://github.com/npm/npm/archive/. nvm uninstall <version> //卸载制定的版本 nvm root [path] //设置和查看root路径 nvm version //查看当前的版本
-
设置淘宝镜像
//1.打开nvm所在文件夹中的settings.txt文件 //2.在文件后面继续添加配置 node_mirror: https://npm.taobao.org/mirrors/node/ npm_mirror: https://npm.taobao.org/mirrors/npm/
2. node.js安装
-
问题:
-
由于我的网络是内网,所以在安装node.js时没有使用nvm进行node安装,直接使用node.msi文件引导安装,所以导致问题之一就是,无法使用nvm ls 或者 nvm list 检测到node环境,
还没解决
- 待补充
-
由于我的网络是内网,所以在安装node.js时没有使用nvm进行node安装,直接使用node.msi文件引导安装,所以导致问题之一就是,无法使用nvm ls 或者 nvm list 检测到node环境,
-
下载地址:https://nodejs.org/zh-cn/
-
安装过程:安装过程直接按照默认情况 一路 next 到 finish
-
查验安装情况:win+R 输入:cmd 命令行输入:node -v/npm -v; 显示版本号即安装成功
-
单node版本环境配置:
- 在C:\Program Files\nodejs文件夹下新建 node_global 和 node_cache两个新文件夹
- 打开cmd界面,重新设置默认安装文件夹
npm config set prefix "C:\Program Files\nodejs\node_global" npm config set cache "C:\Program Files\nodejs\node_cache"
-
设置环境变量,打开【系统属性】-【高级】-【环境变量】,在系统变量中新建
变量名:NODE_PATH
变量值:C:\Program Files\nodejs\node_global\node_modules
主要作用: 用来告诉系统, 下载的模块或者包都在这里了
-
在系统变量中Path中添加 %NODE_PATH%
-
编辑用户变量(环境变量)的 path,将默认的 C 盘下 APPData\Roaming\npm 修改成 C:\Program Files\nodejs\node_global
6.自己考虑是否换镜像地址:
npm config set registry https://registry.npm.taobao.org npm config set registry https://registry.npmjs.org //还原
3. angular
- 使用npm安装angular脚手架
//安装angular脚手架
npm install -g @angular/cli
//构建新项目
ng new my-app
//配置
? Would you like to share pseudonymous usage data about this project with the Angular Team
at Google under Google's Privacy Policy at https://policies.google.com/privacy. For more
details and how to change this setting, see https://angular.io/analytics. No
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? CSS
//等待最后成功
√ Packages installed successfully.
//启动
ng serve --o
//最后会自动打开网址
http://localhost:4200/
- 优点与特性
-
跨平台
- 渐进式应用:高性能、离线使用、免安装
- 原生:借助来自 Ionic、NativeScript 和 React Native 中的技术与思想,构建原生移动应用
- 桌面:能在桌面环境下安装的应用,横跨 Mac、Windows 和 Linux 平台
-
性能
-
代码生成:Angular 会把你的模板转换成代码,针对现代 JavaScript 虚拟机进行
高度优化
,轻松获得框架提供的高生产率 - 统一平台:在服务端渲染应用的首屏,像只有 HTML 和 CSS 的页面那样几乎瞬间展现,支持 Node.js®、.NET、PHP,以及其它服务器,为通过 SEO 来优化站点铺平了道路。
- 代码拆分:借助新的组件路由器,Angular 可以实现快速加载。自动代码拆分机制可以让用户仅仅加载那些用于渲染所请求页面的代码。
-
代码生成:Angular 会把你的模板转换成代码,针对现代 JavaScript 虚拟机进行
-
生产率
-
测试:使用 Karma 进行单元测试,每当你保存时都能知道是否弄坏了点什么。
-
动画:通过 Angular 中直观简便的 API 创建高性能复杂编排和动画时间线 —— 只要非常少的代码。
-
无障碍性:通过支持 ARIA 的组件、开发者指南和内置的一体化测试基础设施,创建具有完备无障碍性的应用。
-
-
英雄之旅教程与理解
-
最终效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O9VxYWFM-1661937991351)(https://angular.cn/generated/images/guide/toh/toh-anim.gif#pic_center)] -
创建一个新的工作区和一个初始应用项目
注意
:操作时cmd 需要
管理员权限
运行ng new angular-tour-of-heroes //项目名 angular-tour-of-heroes //启动应用 cd angular-tour-of-heroes ng serve --open //打开浏览器,并访问 http://localhost:4200/
-
angular 组件
-
你所看到的这个页面就是应用的外壳。这个外壳是被一个名叫 AppComponent 的 Angular 组件控制的。
-
组件是 Angular 应用中的基本构造块。它们在屏幕上显示数据,监听用户输入,并且根据这些输入执行相应的动作。
自我理解
:angular组件就是网页中的一个个显示的
块级单位
,统一被AppComponent组件控制,但各个组件之间的样式互不干扰
-
-
差值语法
//app.component.ts (class title property)文件内容 import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'Tour of Heroes'; //title值定义 } //app.component.html (template) 文件内容 <h1>{{title}}</h1> //差值语法 //界面展现结果 Tour of Heroes
-
创建英雄列表组件
ng generate component heroes //app/heroes/heroes.component.ts (initial version) 文件内容 import { Component, OnInit } from '@angular/core'; //@Component 是个装饰器函数,用于为该组件指定 Angular 所需的元数据。 @Component({ //app-heroes 用来在父组件的模板中匹配 HTML 元素的名称,以识别出该组件 selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) //始终要 export 这个组件类,以便在其它地方(比如 AppModule)导入它 export class HeroesComponent implements OnInit { constructor() { } //ngOnInit() 是一个生命周期钩子,Angular 在创建完组件后很快就会调用 ngOnInit()。这里是放置初始化逻辑的好地方 ngOnInit(): void { } } //如何在AppComponent 的模板中显示 HeroesComponent //src/app/app.component.html 文件内容 <h1>{{title}}</h1> <app-heroes></app-heroes>
-
创建 Hero 类
//在 src/app 文件夹中为 Hero 类创建一个文件,并添加 id 和 name 属性 //src/app/hero.ts 文件内容 export interface Hero { id: number; name: string; } //在HeroesComponent 中 导入 引用 import { Hero } from '../hero'; export class HeroesComponent implements OnInit { hero: Hero = { id: 1, name: 'Windstorm' }; constructor() { } ngOnInit(): void { } }
-
使用 UppercasePipe 进行格式化
说明:管道 是格式化字符串、金额、日期和其它显示数据的好办法。Angular 发布了一些内置管道,而且你还可以创建自己的管道
//src/app/heroes/heroes.component.html 文件类容 <h2>{{hero.name | uppercase}} Details</h2> // 变大写
-
双向绑定
自我理解
:使用ngModel绑定的数据在修改时,元数据会改变,所有使用这个元数据进行展示的也会随之改变,类似于angular在后台一直在做监听。
//前提条件1:导入 FormsModule //app.module.ts (FormsModule symbol import) import { FormsModule } from '@angular/forms'; // <-- NgModel lives here imports: [ BrowserModule, FormsModule ], //前提条件2:声明 HeroesComponent (脚手架在创建时会帮我们自动处理) //src/app/app.module.ts 文件内容 import { HeroesComponent } from './heroes/heroes.component'; declarations: [ AppComponent, HeroesComponent ], //src/app/heroes/heroes.component.html (HeroesComponent's template) 文件内容 <div> <label for="name">Hero name: </label> <input id="name" [(ngModel)]="hero.name" placeholder="name"> </div>
-
创建模拟(mock)的英雄数据
//src/app/mock-heroes.ts 文件展示内容 //导入Hero 英雄类 import { Hero } from './hero'; //定义以Hero类为对象的 HEROES集合 export const HEROES: Hero[] = [ { id: 12, name: 'Dr. Nice' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr. IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ];
-
展示这些英雄
注意
:不要忘了 ngFor 前面的星号(
*
),它是该语法中的关键部分//src/app/heroes/heroes.component.ts (import HEROES) 文件内容 //导入HEROES数据 import { HEROES } from '../mock-heroes'; //往类中添加一个 heroes 属性,这样可以暴露出这个 HEROES 数组,以供绑定。 //heroes 就类似于这个类中的 元数据,在hero这个组件中被全局使用 export class HeroesComponent implements OnInit { heroes = HEROES; } //heroes.component.html (heroes template) 文件类容 <h2>My Heroes</h2> <ul class="heroes"> //循环输出 <li *ngFor="let hero of heroes"> <button type="button"> <span class="badge">{{hero.id}}</span> <span class="name">{{hero.name}}</span> </button> </li> </ul>
-
添加 click 事件绑定
说明:- 圆括号会让 Angular 监听这个 元素的 click 事件
- 当用户点击 时,Angular 就会执行表达式 onSelect(hero)
//heroes.component.html (template excerpt) 文件内容 <li *ngFor="let hero of heroes"> //给 button增加绑定事件 <button type="button" (click)="onSelect(hero)"> <!-- ... --> //添加绑定事件 //src/app/heroes/heroes.component.ts (onSelect) 文件内容 //将 hero 属性改名为 selectedHero ,初始没有赋值 selectedHero?: Hero; //添加如下 onSelect() 方法,它会把模板中被点击的英雄赋值给组件的 selectedHero 属性 onSelect(hero: Hero): void { this.selectedHero = hero; } //添加点击按钮显示区 //heroes.component.html (selected hero details) 文件内容 //判断是否存在selectedHero,存在即显示 <div *ngIf="selectedHero"> <h2>{{selectedHero.name | uppercase}} Details</h2> <div>id: {{selectedHero.id}}</div> <div> <label for="hero-name">Hero name: </label> <input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name"> </div> </div> //为选定的英雄设置样式 //heroes.component.html (list item hero) 文件内容 <li *ngFor="let hero of heroes"> //如果当前行的英雄和 selectedHero 相同,Angular 就会添加 CSS 类 selected,否则就会移除它。 <button [class.selected]="hero === selectedHero" type="button" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> <span class="name">{{hero.name}}</span> </button> </li>
-
英雄细节组件创建
//命令行创建HeroDetailComponent 组件 ng generate component hero-detail //文件内容 src/app/hero-detail/hero-detail.component.html <div *ngIf="hero"> <h2>{{hero.name | uppercase}} Details</h2> <div><span>id: </span>{{hero.id}}</div> <div> <label for="hero-name">Hero name: </label> <input id="hero-name" [(ngModel)]="hero.name" placeholder="name"> </div> </div> //文件内容 src/app/hero-detail/hero-detail.component.ts import { Component, OnInit, Input } from '@angular/core'; import { Hero } from '../hero'; @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: ['./hero-detail.component.css'] }) export class HeroDetailComponent implements OnInit { //这个组件所做的只是通过 hero 属性接收一个英雄对象,并显示它 @Input() hero?: Hero; constructor() { } ngOnInit(): void { } } //文件内容 src/app/heroes/heroes.component.html <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes"> <button [class.selected]="hero === selectedHero" type="button" (click)="onSelect(hero)"> <span class="badge">{{hero.id}}</span> <span class="name">{{hero.name}}</span> </button> </li> </ul> //HeroesComponent 中的 hero 绑定到 输入到 HeroDetailComponent 中 //这是一种单向数据绑定。从 HeroesComponent 的 selectedHero 属性绑定到目标元素的 hero 属性, //并映射到了 HeroDetailComponent 的 hero 属性 <app-hero-detail [hero]="selectedHero"></app-hero-detail>
-
创建 HeroService
- 服务是在多个“互相不知道”的类之间共享信息的好办法。你将创建一个 MessageService,并且把它注入到两个地方
//创建 HeroService ng generate service hero //文件内容 src/app/hero.service.ts //@Injectable() 装饰器把这个类标记为依赖注入系统的参与者之一 //HeroService 类将会提供一个可注入的服务,并且它还可以拥有自己的待注入的依赖 import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Hero } from './hero'; import { HEROES } from './mock-heroes'; import { MessageService } from './message.service'; //默认情况下,Angular CLI 命令 ng generate service 会通过给 @Injectable() 装饰器添加 //providedIn: 'root' 元数据的形式,用根注入器将你的服务注册成为提供者 //自我理解:表示为服务提供者,可被其他组件依赖注入 并使用 @Injectable({ providedIn: 'root', }) export class HeroService { //在 HeroService 中导入 MessageService //Angular 将会在创建 HeroService 时把 MessageService 的单例注入到这个属性中 //这是一个典型的“服务中的服务”场景:你把 MessageService 注入到了 HeroService 中, //而 HeroService 又被注入到了 HeroesComponent 中 constructor(private messageService: MessageService) { } //第一种方式:直接返回 HEROES 数组 //getHeroes(): Hero[] { // return HEROES; //} //第二种方式:返回Observable类型,这也是httpClient返回的类型,实际应用也是这样的 getHeroes(): Observable<Hero[]> { const heroes = of(HEROES); this.messageService.add('HeroService: fetched heroes'); return heroes; } } //文件内容 src/app/heroes/heroes.component.ts import { Component, OnInit } from '@angular/core'; //将被替代为HeroService 方式 //import { Hero } from '../hero'; //通过使用注入HeroService 方式,将获取 英雄数据 步骤分离出去 import { HeroService } from '../hero.service'; import { MessageService } from '../message.service'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent implements OnInit { selectedHero?: Hero; heroes: Hero[] = []; //参数声明了一个私有 heroService 属性,同时把它标记为一个 HeroService 的注入点 //当 Angular 创建 HeroesComponent 时,依赖注入系统就会把这个 heroService 参数设置为 HeroService 的单例对象 constructor(private heroService: HeroService, private messageService: MessageService) { } //在 ngOnInit 生命周期钩子中调用 getHeroes(),之后 Angular 会在构造出 HeroesComponent 的实例 //之后的某个合适的时机调用 ngOnInit() ngOnInit(): void { this.getHeroes(); } onSelect(hero: Hero): void { this.selectedHero = hero; this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`); } //创建一个方法,以从服务中获取这些英雄数据 //第一种方式:直接获取到heroes数组进行赋值 getHeroes(): void { this.heroes = this.heroService.getHeroes(); } //第二种方式:获取方式为 从远端服务器返回Observable类型 //of(HEROES) 会返回一个 Observable<Hero[]>,它会发出单个值,这个值就是这些模拟英雄的数组。 getHeroes(): Observable<Hero[]> { const heroes = of(HEROES); return heroes; } //第三种方式:subscribe() 方法把这个英雄数组传给这个回调函数, //该函数把英雄数组赋值给组件的 heroes 属性 //使用这种异步方式,当 HeroService 从远端服务器获取英雄数据时,就可以工作了 getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } }
-
显示消息
//创建 MessagesComponent ng generate component messages //文件内容 src/app/app.component.html <h1>{{title}}</h1> <app-heroes></app-heroes> <app-messages></app-messages> //创建 MessageService ng generate service message //文件内容 src/app/message.service.ts //该服务对外暴露了它的 messages 缓存,以及两个方法:add() 方法往缓存中添加一条消息, //clear() 方法用于清空缓存 import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class MessageService { messages: string[] = []; add(message: string) { this.messages.push(message); } clear() { this.messages = []; } } //文件内容 src/app/messages/messages.component.ts //该服务对外暴露了它的 messages 缓存,以及两个方法:add() 方法往缓存中添加一条消息, //clear() 方法用于清空缓存 import { Component, OnInit } from '@angular/core'; import { MessageService } from '../message.service'; @Component({ selector: 'app-messages', templateUrl: './messages.component.html', styleUrls: ['./messages.component.css'] }) export class MessagesComponent implements OnInit { //添加一个 public 的 messageService 属性。Angular 将会在创建 MessagesComponent 的实例时 //把 MessageService 的实例注入到这个属性中 //这个 messageService 属性必须是公共属性,因为你将会在模板中绑定到它 //Angular 只会绑定到组件的公共属性。 constructor(public messageService: MessageService) {} ngOnInit() { } } //文件内容 src/app/messages/messages.component.html <div *ngIf="messageService.messages.length"> <h2>Messages</h2> <button type="button" class="clear" (click)="messageService.clear()">Clear messages</button> //因为在 模板中绑定了messageService.messages,所以构造器中设置为messageService //为 public <div *ngFor='let message of messageService.messages'> {{message}} </div> </div> //为 hero 服务添加额外的消息 //文件内容 src/app/heroes/heroes.component.ts //自我理解:当选择英雄时,会调用messageService中的add方法,并回显到messages组件中 onSelect(hero: Hero): void { this.selectedHero = hero; this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`); }
-
路由
//添加路由: 添加 AppRoutingModule //--flat 把这个文件放进了 src/app 中,而不是单独的目录中。 //--module=app 告诉 CLI 把它注册到 AppModule 的 imports 数组中。 ng generate module app-routing --flat --module=app //文件内容 src/app/app-routing.module.ts import { NgModule } from '@angular/core'; //导入 RouterModule 和 Routes,以便该应用具有路由功能 import { RouterModule, Routes } from '@angular/router'; //导入 HeroesComponent,它将告诉路由器要去什么地方 import { HeroesComponent } from './heroes/heroes.component'; const routes: Routes = [ //path 用来匹配浏览器地址栏中 URL 的字符串。 //component 导航到该路由时,路由器应该创建的组件。 //告诉路由器把该 URL 与 path:'heroes' 匹配。如果网址类似于 localhost:4200/heroes 就显示 HeroesComponent { path: 'heroes', component: HeroesComponent } ]; @NgModule({ //这个方法之所以叫 forRoot(),是因为你要在应用的顶层配置这个路由器。 //forRoot() 方法会提供路由所需的服务提供者和指令,还会基于浏览器的当前 URL 执行首次导航 imports: [RouterModule.forRoot(routes)], //AppRoutingModule 导出 RouterModule,以便它在整个应用程序中生效 exports: [RouterModule] }) export class AppRoutingModule { } //添加 RouterOutlet //文件内容 src/app/app.component.html <h1>{{title}}</h1> <nav> //routerLink 属性的值为 "/heroes",路由器会用它来匹配出指向 HeroesComponent 的路由。 //routerLink 是 RouterLink 指令的选择器,它会把用户的点击转换为路由器的导航操作 <a routerLink="/heroes">Heroes</a> </nav> //AppComponent 的模板不再需要 <app-heroes>,因为只有当用户导航到这里时, //才需要显示 HeroesComponent //<router-outlet> 会告诉路由器要在哪里显示路由的视图 <router-outlet></router-outlet> <app-messages></app-messages>
-
添加仪表盘视图
ng generate component dashboard //文件内容 src/app/dashboard/dashboard.component.html <h2>Top Heroes</h2> <div class="heroes-menu"> <a *ngFor="let hero of heroes"> {{hero.name}} </a> </div> //文件内容 src/app/dashboard/dashboard.component.ts import { Component, OnInit } from '@angular/core'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: [ './dashboard.component.css' ] }) export class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor(private heroService: HeroService) { } ngOnInit(): void { this.getHeroes(); } //getHeroes() 函数会截取第 2 到 第 5 位英雄,也就是说只返回四个顶层英雄(第二,第三,第四和第五) getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes.slice(1, 5)); } } //添加仪表盘路由 //src/app/app-routing.module.ts 中 添加 import { DashboardComponent } from './dashboard/dashboard.component'; { path: 'dashboard', component: DashboardComponent }, //这个路由会把一个与空路径“完全匹配”的 URL 重定向到路径为 '/dashboard' 的路由 { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, //把仪表盘链接添加到壳组件中 //添加 src/app/app.component.html <h1>{{title}}</h1> <nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/heroes">Heroes</a> </nav> <router-outlet></router-outlet> <app-messages></app-messages>
-
导航到英雄详情
实现结果:当用户在 HeroesComponent 中点击某个英雄条目时,应用应该能导航到 HeroDetailComponent,从英雄列表视图切换到英雄详情视图。英雄列表视图将不再显示,而英雄详情视图要显示出来//添加 src/app/app-routing.module.ts import { HeroDetailComponent } from './hero-detail/hero-detail.component'; const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'dashboard', component: DashboardComponent }, //path 中的冒号(:)表示 :id 是一个占位符,它表示某个特定英雄的 id { path: 'detail/:id', component: HeroDetailComponent }, { path: 'heroes', component: HeroesComponent } ]; //配置英雄链接 //修改 src/app/dashboard/dashboard.component.html //路由器已经有一个指向 HeroDetailComponent 的路由了,修改仪表盘中的英雄连接, //让它们通过参数化的英雄详情路由进行导航 <a *ngFor="let hero of heroes" routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> //修改 src/app/heroes/heroes.component.html <ul class="heroes"> <li *ngFor="let hero of heroes"> <a routerLink="/detail/{{hero.id}}"> <span class="badge">{{hero.id}}</span> {{hero.name}} </a> </li> </ul> //支持路由的 HeroDetailComponent //获取创建本组件的路由 //从这个路由中提取出 id //通过 HeroService 从服务器上获取具有这个 id 的英雄数据。 //修改 src/app/hero-detail/hero-detail.component.ts //ActivatedRoute 保存着到这个 HeroDetailComponent 实例的路由信息。 //这个组件对从 URL 中提取的路由参数感兴趣。其中的 id 参数就是要显示的英雄的 id import { ActivatedRoute } from '@angular/router'; //location 是一个 Angular 的服务,用来与浏览器打交道。 稍后,你就会使用它来导航回上一个视图 import { Location } from '@angular/common'; import { HeroService } from '../hero.service'; constructor( private route: ActivatedRoute, private heroService: HeroService, private location: Location ) {} ngOnInit(): void { this.getHero(); } getHero(): void { //route.snapshot 是一个路由信息的静态快照,抓取自组件刚刚创建完毕之后。 //paramMap 是一个从 URL 中提取的路由参数值的字典。"id" 对应的值就是要获取的英雄的 id。 //路由参数总会是字符串。JavaScript 的 Number 函数会把字符串转换成数字,英雄的 id 就是数字类型 const id = Number(this.route.snapshot.paramMap.get('id')); this.heroService.getHero(id) .subscribe(hero => this.hero = hero); } //添加 HeroService.getHero() //修改 src/app/hero.service.ts //getHero() 也有一个异步函数签名。它用 RxJS 的 of() 函数返回一个 Observable 形式的模拟英雄数据 getHero(id: number): Observable<Hero> { // For now, assume that a hero with the specified `id` always exists. // Error handling will be added in the next step of the tutorial. const hero = HEROES.find(h => h.id === id)!; this.messageService.add(`HeroService: fetched hero id=${id}`); return of(hero); } //从英雄细节到会到列表 //把一个后退按钮添加到组件模板的底部,并且把它绑定到组件的 goBack() 方法 //修改 src/app/hero-detail/hero-detail.component.html <button type="button" (click)="goBack()">go back</button> //修改 src/app/hero-detail/hero-detail.component.ts goBack(): void { this.location.back(); }
-
启用 HTTP 服务
-
HttpClient 是 Angular 通过 HTTP 与远程服务器通讯的机制。
-
要让 HttpClient 在应用中随处可用,需要两个步骤。首先,用导入语句把它添加到根模块 AppModule 中
//添加 src/app/app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryDataService } from './in-memory-data.service'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { HeroesComponent } from './heroes/heroes.component'; import { HeroSearchComponent } from './hero-search/hero-search.component'; import { MessagesComponent } from './messages/messages.component'; @NgModule({ imports: [ BrowserModule, FormsModule, AppRoutingModule, HttpClientModule, // The HttpClientInMemoryWebApiModule module intercepts HTTP requests // and returns simulated server responses. // Remove it when a real server is ready to receive requests. //forRoot() 配置方法接收一个 InMemoryDataService 类来初始化内存数据库 HttpClientInMemoryWebApiModule.forRoot( InMemoryDataService, { dataEncapsulation: false } ) ], declarations: [ AppComponent, DashboardComponent, HeroesComponent, HeroDetailComponent, MessagesComponent, HeroSearchComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } //模拟数据服务器 npm install angular-in-memory-web-api --save ng generate service InMemoryData //文件内容 src/app/in-memory-data.service.ts //in-memory-data.service.ts 文件已代替了 mock-heroes.ts 文件,现在后者可以安全的删除了 import { Injectable } from '@angular/core'; import { InMemoryDbService } from 'angular-in-memory-web-api'; import { Hero } from './hero'; @Injectable({ providedIn: 'root', }) export class InMemoryDataService implements InMemoryDbService { createDb() { const heroes = [ { id: 12, name: 'Dr. Nice' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, { id: 15, name: 'Magneta' }, { id: 16, name: 'RubberMan' }, { id: 17, name: 'Dynama' }, { id: 18, name: 'Dr. IQ' }, { id: 19, name: 'Magma' }, { id: 20, name: 'Tornado' } ]; return {heroes}; } // Overrides the genId method to ensure that a hero always has an id. // If the heroes array is empty, // the method below returns the initial number (11). // if the heroes array is not empty, the method below returns the highest // hero id + 1. genId(heroes: Hero[]): number { return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11; } }
-
英雄与 HTTP
-
HttpClient 的方法返回单个值
-
所有的 HttpClient 方法都会返回某个值的 RxJS Observable。
-
HTTP 是一个请求/响应式协议。你发起请求,它返回单个的响应。
-
通常,Observable 可以在一段时间内返回多个值。但来自 HttpClient 的 Observable 总是发出一个值,然后结束,再也不会发出其它值。
-
具体到这次 HttpClient.get() 调用,它返回一个 Observable<Hero[]>,也就是“一个英雄数组的可观察对象”。在实践中,它也只会返回一个英雄数组。
-
-
HttpClient.get() 返回响应数据
-
HttpClient.get() 默认情况下把响应体当做无类型的 JSON 对象进行返回。如果指定了可选的模板类型 <Hero[]>,就会给返回你一个类型化的对象。
-
服务器的数据 API 决定了 JSON 数据的具体形态。英雄之旅的数据 API 会把英雄数据作为一个数组进行返回。
-
//在 HeroService 中,导入 HttpClient 和 HttpHeaders: //修改 src/app/hero.service.ts import { HttpClient, HttpHeaders } from '@angular/common/http'; constructor( private http: HttpClient, private messageService: MessageService) { } //注意保留对 MessageService 的注入,但是因为你将频繁调用它,因此请把它包裹进一个私有的 log 方法中 private log(message: string) { this.messageService.add(`HeroService: ${message}`); } //把服务器上英雄数据资源的访问地址 heroesURL 定义为 :base/:collectionName 的形式。 //这里的 base 是要请求的资源,而 collectionName 是 in-memory-data-service.ts 中的英雄数据对象 private heroesUrl = 'api/heroes'; // URL to web api //通过 HttpClient 获取英雄 //修改 src/app/hero.service.ts //方法一 getHeroes(): Observable<Hero[]> { const heroes = of(HEROES); return heroes; } //方法二 /** GET heroes from the server */ getHeroes(): Observable<Hero[]> { return this.http.get<Hero[]>(this.heroesUrl) } ```
-
-
错误处理
-
当你从远端服务器获取数据的时候。HeroService.getHeroes() 方法应该捕获错误,并做适当的处理。
-
要捕获错误,你就要使用 RxJS 的 catchError() 操作符来建立对 Observable 结果的处理管道(pipe)。
//修改 src/app/hero.service.ts import { catchError, map, tap } from 'rxjs/operators'; getHeroes(): Observable<Hero[]> { //使用 pipe() 方法来扩展 Observable 的结果,并给它一个 catchError() 操作符 return this.http.get<Hero[]>(this.heroesUrl) .pipe( //catchError() 操作符会拦截失败的 Observable。它把错误对象传给错误处理器,错误处理器会处理这个错误 catchError(this.handleError<Hero[]>('getHeroes', [])) ); } /** * Handle Http operation that failed. * Let the app continue. * * @param operation - name of the operation that failed * @param result - optional value to return as the observable result */ //下面这个 handleError() 将会在很多 HeroService 的方法之间共享, //所以要把它通用化,以支持这些彼此不同的需求。 //它不再直接处理这些错误,而是返回给 catchError 返回一个错误处理函数。 //还要用操作名和出错时要返回的安全值来对这个错误处理函数进行配置 private handleError<T>(operation = 'operation', result?: T) { return (error: any): Observable<T> => { // TODO: send the error to remote logging infrastructure console.error(error); // log to console instead // TODO: better job of transforming error for user consumption this.log(`${operation} failed: ${error.message}`); // Let the app keep running by returning an empty result. return of(result as T); }; }
-
-
窥探 Observable
-
HeroService 的方法将会窥探 Observable 的数据流,并通过 log() 方法往页面底部发送一条消息。
-
它们可以使用 RxJS 的 tap() 操作符来实现,该操作符会查看 Observable 中的值,使用那些值做一些事情,并且把它们传出来。这种 tap() 回调不会改变这些值本身。
-
下面是 getHeroes() 的最终版本,它使用 tap() 来记录各种操作。
//修改 src/app/hero.service.ts //获取英雄列表 getHeroes(): Observable<Hero[]> { return this.http.get<Hero[]>(this.heroesUrl) .pipe( tap(_ => this.log('fetched heroes')), catchError(this.handleError<Hero[]>('getHeroes', [])) ); } //根据id获取对应英雄信息 getHero(id: number): Observable<Hero> { const url = `${this.heroesUrl}/${id}`; return this.http.get<Hero>(url).pipe( tap(_ => this.log(`fetched hero id=${id}`)), catchError(this.handleError<Hero>(`getHero id=${id}`)) ); }
-
-
修改英雄
-
在英雄详情视图中编辑英雄的名字。随着输入,英雄的名字也跟着在页面顶部的标题区更新了。但是当你点击“后退”按钮时,这些修改都丢失了。
-
如果你希望保留这些修改,就要把它们写回到服务器。
-
在英雄详情模板的底部添加一个保存按钮,它绑定了一个 click 事件,事件绑定会调用组件中一个名叫 save() 的新方法
//修改 src/app/hero-detail/hero-detail.component.html <button type="button" (click)="save()">save</button> //在 HeroDetail 组件类中,添加如下的 save() 方法,它使用英雄服务中的 updateHero() 方法 //来保存对英雄名字的修改,然后导航回前一个视图 //修改 src/app/hero-detail/hero-detail.component.ts save(): void { if (this.hero) { this.heroService.updateHero(this.hero) .subscribe(() => this.goBack()); } } //添加 HeroService.updateHero() //修改 src/app/hero.service.ts //HttpClient.put() 方法接受三个参数: URL 地址 要修改的数据(这里就是修改后的英雄) 选项 updateHero(hero: Hero): Observable<any> { return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe( tap(_ => this.log(`updated hero id=${hero.id}`)), catchError(this.handleError<any>('updateHero')) ); } //URL 没变。英雄 Web API 通过英雄对象的 id 就可以知道要修改哪个英雄。 //英雄 Web API 期待在保存时的请求中有一个特殊的头。这个头是在 HeroService 的 httpOptions 常量中定义的。 httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) };
-
-
添加一个新英雄
//修改 src/app/heroes/heroes.component.html <div> <label for="new-hero">Hero name: </label> <input id="new-hero" #heroName /> <!-- (click) passes input value to add() and then clears the input --> <button type="button" class="add-button" (click)="add(heroName.value); heroName.value=''"> Add hero </button> </div> //当点击事件触发时,调用组件的点击处理器(add()),然后清空这个输入框, //以便用来输入另一个名字。把下列代码添加到 HeroesComponent 类: add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } //当指定的名字非空时,这个处理器会用这个名字创建一个类似于 Hero 的对象(只缺少 id 属性),并把它传给服务的 addHero() 方法。 //当 addHero() 保存成功时,subscribe() 的回调函数会收到这个新英雄,并把它追加到 heroes 列表中以供显示。 //往 HeroService 类中添加 addHero() 方法。 addHero(hero: Hero): Observable<Hero> { return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe( tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)), catchError(this.handleError<Hero>('addHero')) ); }
-
删除某个英雄
- 英雄列表中的每个英雄都有一个删除按钮。
- 把下列按钮(button)元素添加到 HeroesComponent 的模板中,就在每个
-
元素中的英雄名字后方
//添加 src/app/heroes/heroes.component.html <ul class="heroes"> <li *ngFor="let hero of heroes"> <a routerLink="/detail/{{hero.id}}"> <span class="badge">{{hero.id}}</span> {{hero.name}} </a> <button type="button" class="delete" title="delete hero" (click)="delete(hero)">x</button> </li> </ul> //添加 src/app/heroes/heroes.component.ts delete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero.id).subscribe(); } //添加 src/app/hero.service.ts deleteHero(id: number): Observable<Hero> { const url = `${this.heroesUrl}/${id}`; return this.http.delete<Hero>(url, this.httpOptions).pipe( tap(_ => this.log(`deleted hero id=${id}`)), catchError(this.handleError<Hero>('deleteHero')) ); }
-
根据名字搜索
- 先把 searchHeroes() 方法添加到 HeroService 中
- 如果没有搜索词,该方法立即返回一个空数组。剩下的部分和 getHeroes() 很像。唯一的不同点是 URL,它包含了一个由搜索词组成的查询字符串
- 打开 DashboardComponent 的模板并且把用于搜索英雄的元素 添加到代码的底部
- 下一步就是添加一个组件,它的选择器要能匹配
//添加 src/app/hero.service.ts searchHeroes(term: string): Observable<Hero[]> { if (!term.trim()) { // if not search term, return empty hero array. return of([]); } return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe( tap(x => x.length ? this.log(`found heroes matching "${term}"`) : this.log(`no heroes matching "${term}"`)), catchError(this.handleError<Hero[]>('searchHeroes', [])) ); } //添加 src/app/dashboard/dashboard.component.html <h2>Top Heroes</h2> <div class="heroes-menu"> <a *ngFor="let hero of heroes" routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> </div> <app-hero-search></app-hero-search> //创建 HeroSearchComponent ng generate component hero-search //添加 src/app/hero-search/hero-search.component.html <div id="search-component"> <label for="search-box">Hero Search</label> <input #searchBox id="search-box" (input)="search(searchBox.value)" /> <ul class="search-result"> //*ngFor 在一个名叫 heroes$ 的列表上迭代,而不是 heroes。 //$ 是一个约定,表示 heroes$ 是一个 Observable 而不是数组 //*ngFor 不能直接使用 Observable,所以要使用一个管道字符(|),后面紧跟着一个 async。 //这表示 Angular 的 AsyncPipe 管道,它会自动订阅 Observable,这样你就不用在组件类中这么做了。 <li *ngFor="let hero of heroes$ | async" > <a routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> </li> </ul> </div> //添加 src/app/hero-search/hero-search.component.ts import { Component, OnInit } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-hero-search', templateUrl: './hero-search.component.html', styleUrls: [ './hero-search.component.css' ] }) export class HeroSearchComponent implements OnInit { //heroes$ 声明为一个 Observable heroes$!: Observable<Hero[]>; //Subject 既是可观察对象的数据源,本身也是 Observable。 //你可以像订阅任何 Observable 一样订阅 Subject private searchTerms = new Subject<string>(); constructor(private heroService: HeroService) {} // Push a search term into the observable stream. //你还可以通过调用它的 next(value) 方法往 Observable 中推送一些值,就像 search() 方法中一样 search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { //串联 RxJS 操作符 this.heroes$ = this.searchTerms.pipe( // wait 300ms after each keystroke before considering the term //在传出最终字符串之前,debounceTime(300) 将会等待,直到新增字符串的事件暂停了 300 毫秒。 //你实际发起请求的间隔永远不会小于 300ms。 debounceTime(300), // ignore new term if same as previous term //distinctUntilChanged() 会确保只在过滤条件变化时才发送请求 distinctUntilChanged(), // switch to new search observable each time the term changes //switchMap() 会为每个从 debounce() 和 distinctUntilChanged() 中通过的搜索词调用搜索服务。 //它会取消并丢弃以前的搜索可观察对象,只保留最近的 switchMap((term: string) => this.heroService.searchHeroes(term)), ); } }
-