Angular速成1 官网教程入门#
参考 https://angular.io/ https://angular.cn/ https://github.com/angular/angular
<< ng-book The Complete book on Angular >> << Getting Started With Angular >> << Learning Angular >>
环境#
安装npm#
sudo apt install npm
如果版本低可能需要升级npm和node node -v npm -v
sudo npm cache clean -f sudo npm install -g npm // 更新npm。此时6.14.15。
npm install -g n sudo n stable // 更新node到最新stable。新起shell生效。此时14.18.1。
安装angular#
https://angular.cn/cli
sudo npm install -g @angular/cli
added 236 packages from 183 contributors in 163.56s
xc@testing:~/temp/front/ng12$ ng version
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 12.2.11
Node: 14.18.1
Package Manager: npm 6.14.15
OS: linux x64
Angular:
...
Package Version
------------------------------------------------------
@angular-devkit/architect 0.1202.11 (cli-only)
@angular-devkit/core 12.2.11 (cli-only)
@angular-devkit/schematics 12.2.11 (cli-only)
@schematics/angular 12.2.11 (cli-only)
angular的基本结构和默认代码#
ng new test1
用默认选项。会创建出默认项目目录和代码。包括git信息。
cd test1
ng serve --host 0.0.0.0
进行编译并默认在起http服务在4200端口。 这时可以通过本地浏览器访问 http://127.0.0.1:4200。
目录第一层为一堆配置。
src
为代码
angular的三个主要部分#
Modules 功能模块 比如src/app/app.module.ts 用来引用所需的功能
Components 功能组件 实现主要业务逻辑 被Modules引用和定义
Services 用TypeScript类定义数据,并共享给Components。 一般用来实现一些通用的业务和服务
Modules好比较大的树干,Components好比树枝。Services好比树液,把数据带入树枝。 随着项目变大,功能模块可能增加,相应地Components,Services都会增加。
Modules(模块)#
src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
declarations
声明本模块中的component,directive,pipe。
imports
引用其他模块
providers
填写组件所需的service。如果service在module层面引用,module下的所有组件都可以用这个service。
service也可以在组件层面引用。
bootstrap
填写主组件或根组件,是程序的起点。只有根模块才能定义bootstrap。
export
默认代码并没有包含。可以定义可被其他模块引用的信息。
Components(组件)#
src/app/app.components.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = '六六六';
}
这是入口组件,因为在app模块里的NgModule定义了bootstrap为AppComponent。
templateUrl
指定html模板。
styleUrls
指定css文件。
selector
定义selector或者说html tag。这里的app-root是匹配了index.html的
官网教程#
下面完成官网的英雄教程 https://angular.cn/tutorial
功能#
数据为一个名单,可以是各种类型的数据。我做成产品列表。
页面上端总有两个按钮,一个进主页,一个进名单。
主页下端内容为4个产品的连接。
列表页面下端内容为所有产品名单。
点击某个产品可以进行改名。
初始化#
按 https://angular.cn/tutorial/toh-pt0,修改title,app.component.html。 添加css代码。
修改代码后之前起的ng serve会自动更新。 此时页面上只有title了。
显示产品信息#
添加新的组件product
ng generate component product
在app目录下会生成一个product目录,结构和app基本一致。在app.component.html最后添加一行
<app-product></app-product>
即可显示出product.component.html的内容创建product.ts,Product类。 在product.component.ts里引用并创建实例。 在product.component.html里进行显示。
到此可以显示产品的信息了。
数据绑定#
在product.component.html里添加输入框。
<input id="name" [(ngModel)]="product.name" placeholder="name">
这样是把product.name绑到了输入框,数据双向流动。这时会报错 Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’. ngModel属于FormsModule,需要在app.module.ts导入。
import { FormsModule } from '@angular/forms';
imports里添加FormsModule。 这时编辑输入框时上方的数据会实时改变,已经绑定成功了。
列表#
起一个ts造一些假数据。 component的初始化中用假数据初始化产品列表和当前选中的产品。 html中用*ngFor显示列表。点击事件绑定onSelect。 如果选中某个产品(*ngIf判断),显示产品信息。产品信息还是之前的ngModel绑定。
拆分组件#
功能变多时,可能需要拆分。现在拆分出一个产品详情组件。
ng generate component product-detail
有几种方法把数据传进/传出组件。 https://angular.cn/guide/inputs-outputs
@input可以用来在父子组件之间共享数据。 在子组件类ProductDetailComponent中,把Product实例修饰为@input。 父组件写一个这样的html
<app-product-detail [product]="selectedProduct"></app-product-detail>
就可以把selectedProduct作为product传到app-product-detail也就是子组件里。 子组件就可以直接用product这个数据了。 这是一种单向绑定。把显示和编辑产品信息的相关功能挪到detail组件里,就完成了拆分。
好处:层次清楚、可单独修改不影响其他组件、可复用子组件。
添加服务#
组件通常不应该直接去获取或保存数据,而应该着重去展示数据,把数据的接入交给service。
ng generate service product
创建两个service相关的文件。https://angular.cn/api/core/Injectable 依赖注入dependency injection(DI),是一种设计模式。 功能a需要功能b的数据才能运行,就说a依赖于b。 让a能取到b的数据就叫依赖注入。 injector(注入者),要负责为依赖者准备好自己的数据。
src/app/product.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ProductService {
constructor() { }
}
在service中要用@Injectable()修饰service类,标记为一个di参与者。
providedIn用来定义这个类型在什么层面提供数据。一般用root即app级别的di。
providedIn?: Type<any> | 'root' | 'platform' | 'any' | null
把product组件中的假数据引用挪到service里。添加ProductService的引用。
service添加product和假数据的引用。 添加一个getProducts函数返回假数据。
product组件的constructor里传入service。 用service的getProducts取产品数据。
到此已经把假数据挪到了service,由service给组件提供数据。界面和功能都没变。
Observable#
此时service提供的数据是假数据,getProducts是一个同步调用。 如果现实中请求远程服务器,同步调用的话可能会阻塞一段时间,是不能接受的。 需要有一种异步的处理机制,发出请求后不能让界面卡住。
Observer是一种设计模式。#
observable是被观察的物体,observer是观察者。
observer先向observable订阅,订阅之后当observable的数据状态发生改变时,会异步通知所有的订阅者。
这是所谓Reactive Programming。
RxJs#
https://rxjs-dev.firebaseapp.com/ rxjs是一个reactive programming库,http://reactivex.io/的js版本。 可以用它很容易地做出各种异步或回调代码。 angular使用rxjs的observable。
相关概念: Observable 可被观察者 Observer 观察者 Subscription 订阅 Operators 操作符。rxjs概念 Subjects 一种特殊的Observable,用的是multicast,而一般Observable用unicast。 Schedulers 调度。rxjs概念
Operators#
https://rxjs.dev/guide/operators Operator字面意思是操作符。Observable是rxjs的核心数据。 操作符实际是函数,可对Observable执行各种操作,做各种串联,实现各种功能。
代码改动很小#
在service里引入Observable。
import { Observable, of } from 'rxjs';
service的getProducts改为
getProducts(): Observable<Product[]> {
return of(products);
}
of是rxjs的一个操作符。一般用来把数据转成observable。 https://rxjs-dev.firebaseapp.com/api/index/function/of
组件的getProducts改为
// this.products = this.productService.getProducts();
this.productService.getProducts().subscribe(products => this.products = products);
订阅了service的getProducts返回的observable,且定义了回调函数(给products赋值)。 这样已经变成了异步过程。
显示全局消息#
我们添加一个功能。可在屏幕底部显示消息,可在某些组件或service等逻辑中自由添加消息。
添加messages组件
ng generate component messages
app.component.html底部添加component的html
<app-messages></app-messages>
此时底部会显示默认的messages works!创建message服务
ng generate service message
在service类里定义message数据、添加和清空函数。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MessageService {
constructor() { }
messages: string[] = [];
add(message: string)
{
this.messages.push(message);
}
clear()
{
this.messages = [];
}
}
然后可以在product的service和component里引用MessageService。 在这两者的constructor里传private messageService: MessageService即可。 之后就可以调用MessageService的add和clear函数了。 比如在product组件的onSelect里调 this.messageService.add(“onSelect ${product.name}”); 这样每次点击产品,就会添加一条信息。
再在message组件的html文件里用ngFor把自己的所有信息显示出来。 这样就实现了一个打印公共消息的功能。
导航和路由#
任务#
添加一个dashboard页
可在dashboard页和列表页间切换
在这两个页面点击产品时跳到detail页面
每个产品有独立的url
添加路由#
添加一个独立的module取名app-routing。
ng generate module app-routing --flat --module=app
生成一个新文件app-routing.module.ts。 内容改为
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductComponent } from './product/product.component';
const routes: Routes = [
{ path: 'products', component: ProductComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
定义路由与需要引入Routes和RouterModule https://angular.io/api/router/Route
一个路由一般有两个属性
path url
地址component
进入这个路由时需要创建的组件RouterModule.forRoot(routes)
可以看作对routes进行安装。这时访问页面只剩一个title了。 如果在url后面加上刚才添加的path /products,会显示product组件页面。 即路由已经按配置存在了。
在app的html里添加一个列表的链接
<nav>
<a routerLink="/products">Products</a>
</nav>
添加dashboard组件#
添加组件
ng generate component dashboard
routing里引用并为dashboard添加一项
{ path: 'dashboard', component: DashboardComponent },
app的html里添加一个dashboard的link。 这时已经可以在两个页面切换了。
修改dashboard的component。可基本参照product组件。 引入product的service。 声明一个产品数组。 从service获取数据。用slice取4个产品。 修改dashboard的css,把4个产品做成卡片。
添加默认路由
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
添加每个产品的路由
{ path: 'product/:id', component: ProductDetailComponent },
dashboard的html改成这样
<a *ngFor="let product of products" routerLink="/product/{{product.id}}">
{{product.name}}
</a>
绑定了链接
再修改一些html,按教程改造detail组件的代码。 最后dashboard、product和product-detail都成为独立的组件,有独立的url。
http请求#
cors 跨域问题 ng默认起在4200端口。如果访问他域名或端口会报错。
在根目录创建proxy.conf.json
{
"/api/*": {
"target": "http://xxx.xxx.xxx",
"secure": false,
"logLevel": "debug",
"changeOrigin": true
}
}
这样把/api/都转到target。
根目录的angular.json的projects-项目名-root-architect-serve下添加
"options": {
"proxyConfig": "proxy.config.json"
}
重新ng serve即可访问api了。
api返回的数据格式是不定的,还有报错等各种情况。 当前service里getProducts的返回值为Observable<Hero[]>,肯定是不健康的。
引入错误处理
import { catchError, map, tap } from 'rxjs/operators';
暂时没具体看错误处理。大致猜测是根据http状态码区分出错误,进行处理,返回合适的值。接下来是完善增删查改。其实大同小异。 到此官网教程已经结束。
可以马上想到的实践是把http请求抽象出来统一处理。 新起一个类HttpTool,统一订阅,如果出错,统一报错。否则直接返回数据。各个组件不用再管rxjs那些。