Angular 2.x 从0到1 (四)史上最简单的Angular2教程
第一节:Angular 2.0 从0到1 (一) 作者:王芃 wpcfan@gmail.com 第四节:进化!模块化你的应用一个复杂组件的分拆上一节的末尾我偷懒的甩出了大量代码,可能你看起来都有点晕了,这就是典型的一个功能经过一段时间的需求累积后,代码也不可避免的臃肿起来。现在我们看看怎么分拆一下吧。 <footer class="footer" *ngIf="todos?.length > 0"> <span class="todo-count"> <strong>{{todos?.length}}</strong> {{todos?.length == 1 ? 'item' : 'items'}} left </span> <ul class="filters"> <li><a href="">All</a></li> <li><a href="">Active</a></li> <li><a href="">Completed</a></li> </ul> <button class="clear-completed">Clear completed</button> </footer> 观察上面的代码,我们看到似乎所有的变量都是 <footer class="footer" *ngIf="itemCount > 0"> <span class="todo-count"> <strong>{{itemCount}}</strong> {{itemCount == 1 ? 'item' : 'items'}} left </span> <ul class="filters"> <li><a href="">All</a></li> <li><a href="">Active</a></li> <li><a href="">Completed</a></li> </ul> <button class="clear-completed">Clear completed</button> </footer> 这样的话也就是说如果在 import { Component,OnInit,Input } from '@angular/core'; @Component({ selector: 'app-todo-footer',templateUrl: './todo-footer.component.html',styleUrls: ['./todo-footer.component.css'] }) export class TodoFooterComponent implements OnInit { //声明itemCount是可以一个可输入值(从引用者处) @Input() itemCount: number; constructor() { } ngOnInit() { } } 运行一下看看效果,应该一切正常! 类似的我们建立一个Header组件,键入 <header class="header"> <h1>Todos</h1> <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="desc" (keyup.enter)="addTodo()"> </header> 这段代码看起来有点麻烦,主要原因是我们好像不但需要给子组件输入什么,而且希望子组件给父组件要输出一些东西,比如输入框的值和按下回车键的消息等。当然你可能猜到了,Angular2里面有 <app-todo-header placeholder="What do you want" (onTextChanges)="onTextChanges($event)" (onEnterUp)="addTodo()" > </app-todo-header> 但是第三个需求也就是“在输入框输入文字时父组件能够得到这个字符串”,这个有点问题,如果每输入一个字符都要回传给父组件的话,系统会过于频繁进行这种通信,有可能会有性能的问题。那么我们希望可以有一个类似滤波器的东东,它可以过滤掉一定时间内的事件。因此我们定义一个输入型参数delay。 <app-todo-header placeholder="What do you want" delay="400" (textChanges)="onTextChanges($event)" (onEnterUp)="addTodo()" > </app-todo-header> 现在的标签引用应该是上面这个样子,但我们只是策划了它看起来是什么样子,还没有做呢。我们一起动手看看怎么做吧。 //todo-header.component.html <header class="header"> <h1>Todos</h1> <input class="new-todo" [placeholder]="placeholder" autofocus="" [(ngModel)]="inputValue" (keyup.enter)="enterUp()"> </header> 记住子组件的模板是描述子组件自己长成什么样子,应该有哪些行为,这些东西和父组件没有任何关系。比如 import { Component,Input,Output,EventEmitter,ElementRef } from '@angular/core'; import {Observable} from 'rxjs/Rx'; import 'rxjs/Observable'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; @Component({ selector: 'app-todo-header',templateUrl: './todo-header.component.html',styleUrls: ['./todo-header.component.css'] }) export class TodoHeaderComponent implements OnInit { inputValue: string = ''; @Input() placeholder: string = 'What needs to be done?'; @Input() delay: number = 300; //detect the input value and output this to parent @Output() textChanges = new EventEmitter<string>(); //detect the enter keyup event and output this to parent @Output() onEnterUp = new EventEmitter<boolean>(); constructor(private elementRef: ElementRef) { const event$ = Observable.fromEvent(elementRef.nativeElement,'keyup') .map(() => this.inputValue) .debounceTime(this.delay) .distinctUntilChanged(); event$.subscribe(input => this.textChanges.emit(input)); } ngOnInit() { } enterUp(){ this.onEnterUp.emit(true); this.inputValue = ''; } } 下面我们来分析一下代码:
onTextChanges(value) { this.desc = value; } 最后由于组件分拆后,我们希望也分拆一下css,这里就直接给代码了 h1 { position: absolute; top: -155px; width: 100%; font-size: 100px; font-weight: 100; text-align: center; color: rgba(175,47,0.15); -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } input::-moz-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } input::input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .new-todo,.edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; border: 0; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0,0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0,0.003); box-shadow: inset 0 -2px 1px rgba(0,0.03); }
.footer { color: #777; padding: 10px 15px; height: 20px; text-align: center; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0,0.2),0 8px 0 -3px #f6f6f6,0 9px 1px -3px rgba(0,0 16px 0 -6px #f6f6f6,0 17px 2px -6px rgba(0,0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a:hover { border-color: rgba(175,0.1); } .filters li a.selected { border-color: rgba(175,0.2); } .clear-completed,html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; } .clear-completed:hover { text-decoration: underline; } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } 当然上述代码要从 .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0,0 25px 50px 0 rgba(0,0.1); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: 506px; padding: 12px 16px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto,since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle:after { content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>'); } .todo-list li .toggle:checked:after { content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>'); } .todo-list li label { word-break: break-all; padding: 15px 60px 15px 15px; margin-left: 45px; display: block; line-height: 1.2; transition: color 0.4s; } .todo-list li.completed label { color: #d9d9d9; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: '×'; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } label[for='toggle-all'] { display: none; } .toggle-all { position: absolute; top: -55px; left: -12px; width: 60px; height: 34px; text-align: center; border: none; /* Mobile Safari */ } .toggle-all:before { content: '?'; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked:before { color: #737373; } /* Hack to remove background from Mobile Safari. Can't use it globally since it destroys checkboxes in Firefox */ @media screen and (-webkit-min-device-pixel-ratio:0) { .toggle-all,.todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } .toggle-all { -webkit-transform: rotate(90deg); transform: rotate(90deg); -webkit-appearance: none; appearance: none; } } 封装成独立模块现在我们的todo目录下好多文件了,而且我们观察到这个功能相对很独立。这种情况下我们似乎没有必要将所有的组件都声明在根模块AppModule当中,因为类似像子组件没有被其他地方用到。Angular中提供了一种组织方式,那就是模块。模块和根模块很类似,我们先在todo目录下建一个文件 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: [ {provide: 'todoService',useClass: TodoService} ] }) export class TodoModule {} 注意一点,我们没有引入BrowserModule,而是引入了CommonModule。导入 BrowserModule 会让该模块公开的所有组件、指令和管道在 AppModule 下的任何组件模板中直接可用,而不需要额外的繁琐步骤。CommonModule 提供了很多应用程序中常用的指令,包括 NgIf 和 NgFor 等。BrowserModule 导入了 CommonModule 并且 重新导出 了它。 最终的效果是:只要导入 BrowserModule 就自动获得了 CommonModule 中的指令。几乎所有要在浏览器中使用的应用的 根模块 ( AppModule )都应该从 @angular/platform-browser 中导入 BrowserModule 。在其它任何模块中都 不要导入 BrowserModule,应该改成导入 CommonModule 。 它们需要通用的指令。它们不需要重新初始化全应用级的提供商。 import { Routes,RouterModule } from '@angular/router'; import { TodoComponent } from './todo.component'; export const routes: Routes = [ { path: 'todo',component: TodoComponent } ]; export const routing = RouterModule.forChild(routes); 这里我们只定义了一个路由就是“todo”,另外一点和根路由不一样的是 import { Routes,RouterModule } from '@angular/router'; import { LoginComponent } from './login/login.component'; export const routes: Routes = [ { path: '',redirectTo: 'login',pathMatch: 'full' },{ path: 'login',component: LoginComponent },{ path: 'todo',redirectTo: 'todo' } ]; export const routing = RouterModule.forRoot(routes); 注意到我们去掉了TodoComponent的依赖,而且更改todo路径定义为redirecTo到todo路径,但没有给出组件,这叫做“无组件路由”,也就是说后面的事情是TodoModule负责的。 import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { TodoModule } from './todo/todo.module'; import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryTodoDbService } from './todo/todo-data'; import { AppComponent } from './app.component'; import { LoginComponent } from './login/login.component'; import { AuthService } from './core/auth.service'; import { routing } from './app.routes'; @NgModule({ declarations: [ AppComponent,LoginComponent ],imports: [ BrowserModule,InMemoryWebApiModule.forRoot(InMemoryTodoDbService),routing,TodoModule ],providers: [ {provide: 'auth',useClass: AuthService} ],bootstrap: [AppComponent] }) export class AppModule { } 而且此时我们注意到其实没有任何一个地方目前还需引用 更真实的web服务这里我们不想再使用内存Web服务了,因为如果使用,我们无法将其封装在TodoModule中。所以我们使用一个更“真”的web服务:json-server。使用 { "todos": [ { "id": "f823b191-7799-438d-8d78-fcb1e468fc78","desc": "blablabla","completed": false },{ "id": "dd65a7c0-e24f-6c66-862e-0999ea504ca0","desc": "getting up",{ "id": "c1092224-4064-b921-77a9-3fc091fbbd87","desc": "you wanna try",{ "id": "e89d582b-1a90-a0f1-be07-623ddb29d55e","desc": "have to say good","completed": false } ] } 在 // private api_url = 'api/todos'; private api_url = 'http://localhost:3000/todos'; 并将addTodo和getTodos中then语句中的 import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { TodoModule } from './todo/todo.module'; import { AppComponent } from './app.component'; import { LoginComponent } from './login/login.component'; import { AuthService } from './core/auth.service'; import { routing } from './app.routes'; @NgModule({ declarations: [ AppComponent,bootstrap: [AppComponent] }) export class AppModule { } 另外打开一个命令窗口,进入工程目录,输入 欣赏一下成果吧 完善Todo应用在结束本节前,我们得给Todo应用收个尾,还差一些功能没完成:
TodoItem和TodoList组件在命令行窗口键入 //todo.component.html <section class="todoapp"> <app-todo-header placeholder="What do you want" (textChanges)="onTextChanges($event)" (onEnterUp)="addTodo()" > </app-todo-header> <app-todo-list [todos]="todos" (onRemoveTodo)="removeTodo($event)" (onToggleTodo)="toggleTodo($event)" > </app-todo-list> <app-todo-footer [itemCount]="todos?.length"></app-todo-footer> </section> 那么TodoItem哪儿去了呢?TodoItem是TodoList的子组件,TodoItem的模板应该是todos循环内的一个todo的模板。TodoList的HTML模板看起来应该是下面的样子: <section class="main" *ngIf="todos?.length > 0"> <input class="toggle-all" type="checkbox"> <ul class="todo-list"> <li *ngFor="let todo of todos" [class.completed]="todo.completed"> <app-todo-item [isChecked]="todo.completed" (onToggleTriggered)="onToggleTriggered(todo)" (onRemoveTriggered)="onRemoveTriggered(todo)" [todoDesc]="todo.desc"> </app-todo-item> </li> </ul> </section> 那么我们先从最底层的TodoItem看,这个组件怎么剥离出来?首先来看 <div class="view"> <input class="toggle" type="checkbox" (click)="toggle()" [checked]="isChecked"> <label [class.labelcompleted]="isChecked" (click)="toggle()">{{todoDesc}}</label> <button class="destroy" (click)="remove(); $event.stopPropagation()"></button> </div> 我们需要确定有哪些输入型和输出型参数
//todo-item.component.ts import { Component,EventEmitter } from '@angular/core'; @Component({ selector: 'app-todo-item',templateUrl: './todo-item.component.html',styleUrls: ['./todo-item.component.css'] }) export class TodoItemComponent{ @Input() isChecked: boolean = false; @Input() todoDesc: string = ''; @Output() onToggleTriggered = new EventEmitter<boolean>(); @Output() onRemoveTriggered = new EventEmitter<boolean>(); toggle() { this.onToggleTriggered.emit(true); } remove() { this.onRemoveTriggered.emit(true); } } 建立好TodoItem后,我们再来看TodoList,还是从模板看一下 <section class="main" *ngIf="todos?.length > 0"> <input class="toggle-all" type="checkbox"> <ul class="todo-list"> <li *ngFor="let todo of todos" [class.completed]="todo.completed"> <app-todo-item [isChecked]="todo.completed" (onToggleTriggered)="onToggleTriggered(todo)" (onRemoveTriggered)="onRemoveTriggered(todo)" [todoDesc]="todo.desc"> </app-todo-item> </li> </ul> </section> TodoList需要一个输入型参数todos,由父组件(TodoComponent)指定,TodoList本身不需要知道这个数组是怎么来的,它和TodoItem只是负责显示而已。当然我们由于在TodoList里面还有TodoITem子组件,而且TodoList本身不会处理这个输出型参数,所以我们需要把子组件的输出型参数再传递给TodoComponent进行处理。 import { Component,EventEmitter } from '@angular/core'; import { Todo } from '../todo.model'; @Component({ selector: 'app-todo-list',templateUrl: './todo-list.component.html',styleUrls: ['./todo-list.component.css'] }) export class TodoListComponent { _todos: Todo[] = []; @Input() set todos(todos:Todo[]){ this._todos = [...todos]; } get todos() { return this._todos; } @Output() onRemoveTodo = new EventEmitter<Todo>(); @Output() onToggleTodo = new EventEmitter<Todo>(); onRemoveTriggered(todo: Todo) { this.onRemoveTodo.emit(todo); } onToggleTriggered(todo: Todo) { this.onToggleTodo.emit(todo); } } 上面代码中有一个新东东,就是在 现在回过头来看一下 //todo.component.html <section class="todoapp"> <app-todo-header placeholder="What do you want" (textChanges)="onTextChanges($event)" (onEnterUp)="addTodo()" > </app-todo-header> <app-todo-list [todos]="todos" (onRemoveTodo)="removeTodo($event)" (onToggleTodo)="toggleTodo($event)" > </app-todo-list> <app-todo-footer [itemCount]="todos?.length"></app-todo-footer> </section> 讲到这里大家可能要问是不是过度设计了,这么少的功能用得着这么设计吗?是的,本案例属于过度设计,但我们的目的是展示出更多的Angular实战方法和特性。 填坑,完成漏掉的功能现在我们还差几个功能:全部反转状态(ToggleAll),清除全部已完成任务(Clear Completed)和状态筛选器。我们的设计方针是逻辑功能放在TodoComponent中,而其他子组件只负责表现。这样的话,我们先来看看逻辑上应该怎么完成。 用路由参数传递数据首先看一下过滤器,在Footer中我们有三个过滤器:All,Active和Completed,点击任何一个过滤器,我们只想显示过滤后的数据。 image_1b17mtibdkjn105l1ojl1dgr9il9.png-6.5kB 这个功能其实有几种可以实现的方式,第一种我们可以按照之前讲过的组件间传递数据的方式设置一个 { path: 'todo/:filter',component: TodoComponent } 这个 <ul class="filters"> <li><a routerLink="/todo/ALL">All</a></li> <li><a routerLink="/todo/ACTIVE">Active</a></li> <li><a routerLink="/todo/COMPLETED">Completed</a></li> </ul> 当然我们还需要在 { path: 'todo/:filter',component: TodoComponent } 根路由定义也需要改写一下,因为原来todo不带参数时,我们直接重定向到todo模块即可,但现在有参数的话应该重定向到默认参数是“ALL”的路径; { path: 'todo',redirectTo: 'todo/ALL' } 现在打开
constructor( @Inject('todoService') private service,private route: ActivatedRoute,private router: Router) {} 然后在 ngOnInit() { this.route.params.forEach((params: Params) => { let filter = params['filter']; this.filterTodos(filter); }); } 从 filterTodos(filter: string): void{ this.service .filterTodos(filter) .then(todos => this.todos = [...todos]); } 最后我们看看在 // GET /todos?completed=true/false 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(); } } 至此大功告成,我们来看看效果吧。现在输入 批量修改和批量删除ToggleAll和ClearCompleted的功能其实是一个批量修改和批量删除的过程。 <button class="clear-completed" (click)="onClick()">Clear completed</button>
//todo-footer.component.ts ... @Output() onClear = new EventEmitter<boolean>(); onClick(){ this.onClear.emit(true); } ... 类似的,ToggleAll位于TodoList中,所以在 <input class="toggle-all" type="checkbox" (click)="onToggleAllTriggered()"> 在 @Output() onToggleAll = new EventEmitter<boolean>(); onToggleAllTriggered() { this.onToggleAll.emit(true); } 在父组件模板中添加子组件中刚刚声明的新属性,在 ... <app-todo-list ... (onToggleAll)="toggleAll()" > </app-todo-list> <app-todo-footer ... (onClear)="clearCompleted()"> </app-todo-footer> ... 最后在父组件( toggleAll(){ this.todos.forEach(todo => this.toggleTodo(todo)); } clearCompleted(){ const todos = this.todos.filter(todo=> todo.completed===true); todos.forEach(todo => this.removeTodo(todo)); } 先保存一下,点击一下输入框左边的下箭头图标或者右下角的“Clear Completed”,看看效果 let p1 = Promise.resolve(3); let p2 = 1337; let p3 = new Promise((resolve,reject) => { setTimeout(resolve,100,"foo"); }); Promise.all([p1,p2,p3]).then(values => { console.log(values); // [3,1337,"foo"] }); 但是还有个问题,我们目前的 //todo.component.ts片段 toggleTodo(todo: Todo): Promise<void> { const i = this.todos.indexOf(todo); return this.service .toggleTodo(todo) .then(t => { this.todos = [ ...this.todos.slice(0,i),t,...this.todos.slice(i+1) ]; return null; }); } removeTodo(todo: Todo): Promise<void> { const i = this.todos.indexOf(todo); return this.service .deleteTodoById(todo.id) .then(()=> { this.todos = [ ...this.todos.slice(0,...this.todos.slice(i+1) ]; return null; }); } toggleAll(){ Promise.all(this.todos.map(todo => this.toggleTodo(todo))); } clearCompleted(){ const completed_todos = this.todos.filter(todo => todo.completed === true); const active_todos = this.todos.filter(todo => todo.completed === false); Promise.all(completed_todos.map(todo => this.service.deleteTodoById(todo.id))) .then(() => this.todos = [...active_todos]); } 现在再去试试效果,应该一切功能正常。当然这个版本其实还是有问题的,本质上还是在循环调用 // It was PUT /todos/:id before // But we will use PATCH /todos/:id instead // Because we don't want to waste the bytes those don't change toggleTodo(todo: Todo): Promise<Todo> { const url = `${this.api_url}/${todo.id}`; let updatedTodo = Object.assign({},todo,{completed: !todo.completed}); return this.http .patch(url,JSON.stringify({completed: !todo.completed}),{headers: this.headers}) .toPromise() .then(() => updatedTodo) .catch(this.handleError); } 最后其实Todo的所有子组件其实都没有用到ngInit,所以不必实现NgInit接口,可以去掉ngInit方法和相关的接口引用。 本节代码: https://github.com/wpcfan/awe... 第一节:Angular 2.0 从0到1 (一) (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |