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那些。