Angular2别学边记

一、起项目

根据官方教程,执行以下:

git clone https://github.com/angular/quickstart.git quickstart
cd quickstart
npm install
npm start

然后要把下面文件略微改一下(官方没有写),否则编译出的js和ts混在一起,很乱

//tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "outDir": "./built",//*加这一句,将编译的js文件归到这个目录
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": [ "es2015", "dom" ],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}

这时直接运行会报错,还需要改一下这个文件:

//systemjs.config.js
......
   map: {
      // our app is within the app folder
      app: 'built/app',
......

这时候npm start就行了

不过还是推荐用angular-cli起项目,用了一下确实很方便,但是需要注意一点,国内的特殊网络环境。直接用ng new xxxxx命令一般会在Installing packages for tooling via npm.这个地方卡死。解决办法是用ng new xxxx --skip-npm命令,然后进入文件夹npm i。还有一个坑就是在mac os上,这个命令创建的目录不在当前文件夹下,而是在/home/user下。别的系统我没试。

要点记录:

  • 双向绑定必须要导入FormsModule

//app.module.ts
import {FormsModule} from '@angular/forms';
@NgModule({
  imports:      [ 
    BrowserModule, 
    FormsModule
    ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
  • 命名约定

文件名用烤串命名法

  • angular moduleId的作用(angular-cli工程不用)

  • 在英雄教程里看到这个,当时不明白什么意思,google后找到答案: 这个是相对路径使用的。
  • 在使用commonJS的前提下,不使用angular moduleId将从根路径开始寻找,使用后是从相对路径,webpack不受影响。
//不使用angular moduleId:
@Component({
  selector: 'my-component',
  templateUrl: 'app/components/my.component.html', <- Starts from base path
  styleUrls:  ['app/components/my.component.css'] <- Starts from base path
})
//使用angular moduleId
@Component({
  moduleId: module.id,
  selector: 'my-component',
  templateUrl: 'my.component.html', <- relative to the components current path
  styleUrls:  ['my.component.css'] <- relative to the components current path
})
//tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs", <- need to change this if you want to use module.id property
...
  • 注意:使用官方的angular-cli产生的工程,这个angular moduleId要去掉,否则会报错,因为编译模式是es6

  • 使用router导航的两种方式:

//在html中导航
 <a class="col-1-4" *ngFor="let hero of heroes" [routerLink]="['/detail',hero.id]">

//--------------------------------------------------------------------------
//在组件js中导航
//从angular router库中导入Router组件
import { Router } from '@angular/router';
//...
export class HeroesComponent implements OnInit {
//依赖注入
  constructor(private router:Router) { }
  //...
  gotoDetail():void  {
//调用navigate()
      this.router.navigate(['/detail',this.selectedHero.id]);
    }
}
  • 架构概览

Paste_Image.png

Angular 模块(无论是根模块还是特性模块)都是一个带有@NgModule装饰器的类。
NgModule是一个装饰器函数,它接收一个用来描述模块属性的元数据对象。其中最重要的属性是:

  • declarations: 声明本模块中拥有的视图类。 Angular 有三种视图类:组件指令管道
    <u>只有*可以声明的 — 组件、指令和管道 — 属于declarations数组。 不要将其他类型的类添加到declarations中,例如NgModule类, 服务类,模型类。</u>
  • exports:declarations 的子集,可用于其它模块的组件模板
  • imports: 模块声明的组件模板需要的类所在的其它模块。

<u>imports数组中应该只有NgModule类。不要放置其它类型的类。</u>

  • providers: 服务的创建者,并加入到全局服务列表中,可用于应用任何部分。
  • bootstrap: 指定应用的主视图(称为根组件),它是所有其它视图的宿主。只有根模块才能设置bootstrap
    属性。

元数据

@Component({
  moduleId: module.id,
  selector:    'hero-list',
  templateUrl: 'hero-list.component.html',
  providers:  [ HeroService ]
})
export class HeroListComponent implements OnInit {

解释一下:
@Component是装饰器,它把紧随其后的HeroListComponent类标记成了组件类
@Component装饰器中我们传入了一些数据,这些数据叫做元数据
其它装饰器包括@Injectable、@Input和@Output等。所以装饰器后必须有(),即使你不打算向其中传入任何元数据

模板、元数据和组件共同描绘出这个视图

指令 (directive)

指令概览
在 Angular 中有三种类型的指令:

  1. Components—组件 — 拥有模板的指令
  2. Structural directives—结构型指令 — 通过添加和移除 DOM 元素改变 DOM 布局的指令。
  3. Attribute directives—属性型指令 — 改变元素显示和行为的指令。

组件是这三种指令中最常用的。 你在快速起步例子中第一次见到组件。
结构型指令修改视图的结构。例如,NgFor 和 NgIf。
属性型指令改变一个元素的外观或行为。例如,内置的 NgStyle 指令可以同时修改元素的多个样式。

Angular 模板是动态的。当 Angular 渲染它们时,它会根据指令提供的操作对 DOM 进行转换。严格来说组件就是一个指令,但是组件非常独特,并在 Angular 中位于中心地位,所以在架构概览中,我们把组件从指令中独立了出来。还有两种其它类型的指令:结构型指令和属性 (attribute) 型指令。

  1. 结构型指令通过在 DOM 中添加、移除和替换元素来修改布局。
<li *ngFor="let hero of heroes"></li>
<hero-detail *ngIf="selectedHero"></hero-detail>
  1. 属性型 指令修改一个现有元素的外观或行为。 在模板中,它们看起来就像是标准的 HTML 属性,故名。
//ngModel指令就是属性型指令的一个例子,它实现了双向数据绑定。 ngModel修改现有元素(一般是<input>)的行为:设置其显示属性值,并响应 change 事件。
<input [(ngModel)]="hero.name">

Angular 还有少量指令,它们或者修改结构布局(例如 ngSwitch), 或者修改 DOM 元素和组件的各个方面(例如 ngStylengClass)。

依赖注入

constructor(private service: HeroService) { }

简单的说,必须在要求注入HeroService之前,在注入器中注册HeroService的Provider。 Provider用于创建并返回一个服务,通常是服务类本身。
<u>我们可以在模块或组件中注册提供商。</u>
通常会把提供商添加到根模块上,以便在任何地方使用服务的同一个实例。

安全导航操作符 ( ?. ) 和空属性路径

The null hero's name is {{nullHero.firstName}}

当currentHero为空的时候,应用崩溃了。
解决方案有:

<div *ngIf="nullHero">The null hero's name is {{nullHero.firstName}}</div>
//或者
The null hero's name is {{nullHero && nullHero.firstName}}

这些方法都有价值,但是会显得笨重,特别是当这个属性路径非常长的时候。 Angular 安全导航操作符 (?.) 是在属性路径中保护空值的更加流畅、便利的方式。 表达式会在它遇到第一个空值的时候跳出。 显示是空的,但应用正常工作,而没有发生错误。

<!-- No hero, no problem! -->
The null hero's name is {{nullHero?.firstName}}

用户输入

  • 模板引用变量
<p>
 <input #name (keyup)="0">
</p>
 {{name.value}}

angular可以通过$event获得DOM,并通过$event.target.value这样来获得input的值,但不建议这么做,因为这样相当于把html DOM元素暴露给了组件,提倡的做法是利用上面的模板引用变量
另一个例子:

@Component({
  selector: 'key-up2',
  template: `
    <input #box (keyup)="onKey(box.value)">
    <p>{{values}}</p>
  `
})
export class KeyUpComponent_v2 {
  values = '';
  onKey(value: string) {
    this.values += value + ' | ';
  }
}

更为完整

@Component({
  selector: 'key-up4',
  template: `
    <input #box
      //keyup.enter)键盘事件过滤器
      (keyup.enter)="update(box.value)"
      //失去焦点事件
      (blur)="update(box.value)">
    <p>{{value}}</p>
  `
})
export class KeyUpComponent_v4 {
  value = '';
  update(value: string) { this.value = value; }
}

小结

  • 使用模板变量来引用元素
  • 传递数值,而非元素
  • 保持模板语句简单

表单

NgModel 指令不仅仅跟踪状态。它还使用特定的 Angular CSS 类来更新控件,以反映当前状态。 可以利用这些 CSS 类来修改控件的外观,显示或隐藏消息。

Paste_Image.png
input type="text" class="form-control" id="name" required
         [(ngModel)]="model.name" name="name" #name="ngModel">
<div class="alert alert-danger" 
        [hidden]="name.valid || name.pristine"> name 是必填字段</div>
  • name="ngModel"相当于定义“模板引用变量”,并将其初始化为ngModel。这样在下面的警告框就能根据#name(当前ngModel)的状态,显示或者隐藏(当控件是有效的 (valid) 或全新的 (pristine) 时,隐藏消息。 “全新的”意味着从它被显示在表单中开始,用户还从未修改过它的值。)
表单的reset()

添加用户时,这个name项并不是“全新的”了,要通过调用表单的reset()方法重置控件的“全新”状态

<button type="button" class="btn btn-default" 
(click)="this.model = new Hero(2, '', ''); heroForm.reset();">
New Hero</button>

使用ngForm和ngSubmit校验并提交表单

<form (ngSubmit)="onSubmit()" #userForm="ngForm">
<button type="submit" class="btn btn-primary" 
[disabled]="!userForm.form.valid">Submit</button>

  • 什么是ngForm?

NgForm指令为form元素扩充了额外的特性。 它持有通过ngModel指令和name属性为各个元素创建的那些控件,并且监视它们的属性变化,包括有效性。 它还有自己的valid属性,只有当其中所有控件都有效时,它才有效。

通过userForm变量把按钮的disabled属性绑定到表单的整体有效性。
现在,删除姓名,们违反了“必填姓名”规则,它还是像以前那样显示出错误信息。同时,Submit 按钮也被禁用了。

Paste_Image.png

模板语法

  • attribute 是由 HTML 定义的。property 是由 DOM (Document Object Model) 定义的。
  • attribute 初始化 DOM property,然后它们的任务就完成了。property 的值可以改变;attribute 的值不能改变。
  • 在 Angular 的世界中,attribute 唯一的作用是用来初始化元素和指令的状态。 当进行数据绑定时,只是在与元素和指令的 property 和事件打交道,而 attribute 就完全靠边站了。
  • 数据绑定的目标是 DOM 中的某些东西。 这个目标可能是(元素 | 组件 | 指令的)property、(元素 | 组件 | 指令的)事件,或(极少数情况下) attribute 名。
数据绑定一览表
  • 属性绑定还是插值表达式?
//我们通常得在插值表达式和属性绑定之间做出选择。 下列这几对绑定做的事情完全相同:
<p>![]({{heroImageUrl}}) is the <i>interpolated</i> image.</p>
<p><img [src]="heroImageUrl"> is the <i>property bound</i> image.</p>
<p><span>"{{title}}" is the <i>interpolated</i> title.</span></p>
<p>"<span [innerHTML]="title"></span>" is the <i>property bound</i> title.</p>

在多数情况下,插值表达式是更方便的备选项。

attribute 绑定

如前所述,html中有一些元素只有attribute没有property,如colspan,我们知道angular只能绑定到propertiy,如果非要绑定到attribute需要特殊的语法:

//attr前缀,一个点 (.) 和 attribute 的名字组成
<td [attr.colspan]="1 + 1">One-Two</td>

父子组件通讯

  1. 父 → 子(@Input())
//父组件
<app-todo-footer [itemCount]="todos?.length"></app-todo-footer>
//子组件
import { Component, OnInit,Input } from '@angular/core';
@Input() 
itemCount:number;
  1. 子 → 父(@Output()、EventEmitter):
//子组件
template: `
<div>
  <button  (cilick)="sendEvent();">Event Emitter Test</button>
</div>`

// component
  @Output() 
  onRemoveTodo = new EventEmitter<Todo>();
sendEvent(todo:Todo){
    this.onRemoveTodo.emit(todo);
  }
//父组件,收到eventEmitter消息后执行handleEvent方法,
//通过$event拿到传过来的Todo类型值
<child (eventEmitter)="handleEvent($event)" ></child>

如何将模块(Module)独立出来

绝大部分摘自@接灰的电子产品神级码农

模块>组件, Module is greater than Component

Paste_Image.png
  • 可以看到,模块包括三种视图类:component/directive/pipe,使用 declarations:[...]导入
  • 模块还可以包括其它模块,使用imports导入,如
 imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    TodoModule,
    InMemoryWebApiModule.forRoot(TodoDbService),
    routing
  ],

随着应用逐渐变大,模块化势在必行,如,登录模块,注册模块。。。。如何把一个应用用模块组织起来?

  1. 建立模块文件,如todo.module.ts
import { CommonModule } from '@angular/common';
import{ NgModule } from '@angular/core';
import {HttpModule} from '@angular/http';
import {FormsModule} from '@angular/forms';

import {routing } from './todo.routes';

import { TodoComponent } from './todo.component';
import { TodoFooterComponent } from './todo-footer/todo-footer.component';
import { TodoHeaderComponent } from './todo-header/todo-header.component';
import { TodoService } from './todo.service';
@NgModule({
    imports: [
        CommonModule,
        FormsModule,
        HttpModule,
        routing
    ],
    declarations:[
        TodoComponent,
        TodoFooterComponent,
        TodoHeaderComponent
    ],
    providers : [
      TodoService
    ]
})
export class TodoModule{}

可以看到其和app.module.ts非常类似,不同的地方有2处:

  1. 使用CommonModule而不是BrowserModule

导入 BrowserModule 会让该模块公开的所有组件、指令和管道在 AppModule 下的任何组件模板中直接可用,而不需要额外的繁琐步骤。CommonModule 提供了很多应用程序中常用的指令,包括 NgIf 和 NgFor 等。BrowserModule 导入了 CommonModule 并且 重新导出 了它。 最终的效果是:只要导入 BrowserModule 就自动获得了 CommonModule 中的指令。几乎所有要在浏览器中使用的应用的 根模块 ( AppModule )都应该从 @angular/platform-browser 中导入 BrowserModule 。在其它任何模块中都 不要导入 BrowserModule,应该改成导入 CommonModule 。 它们需要通用的指令。它们不需要重新初始化全应用级的提供商。

  1. routing的写法:
import { Routes, RouterModule } from '@angular/router';
import { TodoComponent } from './todo.component';

export const routes: Routes = [
  {
    path: 'todo',
    component: TodoComponent
  }
];
//注意是forChild
export const routing = RouterModule.forChild(routes);

forRoot只能用于根目录,所有非根模块的其他模块路由都只能用forChild

添加了自定义模块后根模块的变动:

  1. 引入TodoModule
import {TodoModule} from './todo/todo.module';
...
@NgModule({
  declarations: [
    AppComponent,
    LoginComponent 
  ],
  imports: [
    ...
    TodoModule, 
    routing,
   ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

2.routing的变动:

  {
    path: 'todo',
    component: TodoComponent
  },

变为

  {
    path: 'todo',
    redirectTo:'todo'
  },

去掉了TodoComponent的依赖,而且更改todo路径定义为redirecTo到todo路径,但没有给出组件,这叫做“无组件路由”,也就是说后面的事情是TodoModule负责的。

一般来说,如果要生成某个模块下的组件,输入ng g c 模块名称/组件名称。在命令行窗口键入ng g c todo/todo-item,angular-cli会十分聪明的帮你在todo目录下建好TodoItem组件,并且在TodoModule中声明。

通过路由传参

  1. router中定义动态路由:
 {
    path: 'todo/:filter',
    component: TodoComponent
  }
  1. Component.ts中:
//引入相关类
import { Router, ActivatedRoute, Params } from '@angular/router';
//依赖注入
 constructor(  
    private route: ActivatedRoute,
    private router: Router
    ) { }
//初始时定义路由解析
 ngOnInit() {
    this.getTodos();
    this.filterTodos();
  }
  filterTodos(){
  this.route.params.forEach((params:Params)=>{
    let filter = params['filter'];
    this.service.filterTodos(filter)
      .then(todos=>this.todos=[...todos]);
  })
  }

再看看service类(用到的服务器是json-server):

    //根据路由筛选
   filterTodos(filter: string): Promise<Todo[]> {
    switch(filter){
      case 'ACTIVE': return this.http
                        .get(`${this.api_url}?completed=false`)
                        .toPromise()
                        .then(res => res.json() as Todo[])
                        .catch(this.handleError);
      case 'COMPLETED': return this.http
                          .get(`${this.api_url}?completed=true`)
                          .toPromise()
                          .then(res => res.json() as Todo[])
                          .catch(this.handleError);
      default:
        return this.getTodos();
    }
  }

乱七八糟写了一堆,就当是笔记吧

推荐阅读更多精彩内容