Angular 2.0 从0到1 (三)
作者:王芃 wpcfan@gmail .com 第一节:Angular 2.0 从0到1 (一) 第三节:建立一个待办事项应用这一章我们会建立一个更复杂的待办事项应用,当然我们的登录功能也还保留,这样的话我们的应用就有了多个相对独立的功能模块。以往的web应用根据不同的功能跳转到不同的功能页面。但目前前端的趋势是开发一个SPA(Single Page Application 单页应用),所以其实我们应该把这种跳转叫视图切换:根据不同的路径显示不同的组件。那我们怎么处理这种视图切换呢?幸运的是,我们无需寻找第三方组件,Angular官方内建了自己的路由模块。 建立routing的步骤由于我们要以路由形式显示组件,建立路由前,让我们先把
imports: [ BrowserModule,FormsModule,HttpModule,RouterModule.forRoot([ { path: 'login',component: LoginComponent } ]) ], 注意到这个形式和其他的比如BrowserModule、FormModule和HTTPModule表现形式好像不太一样,这里解释一下,forRoot其实是一个静态的工厂方法,它返回的仍然是Module,下面的是Angular API文档给出的 forRoot(routes: Routes,config?: ExtraOptions) : ModuleWithProviders 为什么叫forRoot呢?因为这个路由定义是应用在应用根部的,你可能猜到了还有一个工厂方法叫forChild,后面我们会详细讲。接下来我们看一下forRoot接收的参数,参数看起来是一个数组,每个数组元素是一个
error_handler.js:48EXCEPTION: Uncaught (in promise): Error: Cannot find primary outlet to load 'LoginComponent' Error: Cannot find primary outlet to load 'LoginComponent' at getOutlet (http://localhost:4200/main.bundle.js:66161:19) at ActivateRoutes.activateRoutes (http://localhost:4200/main.bundle.js:66088:30) at http://localhost:4200/main.bundle.js:66052:19 at Array.forEach (native) at ActivateRoutes.activateChildRoutes (http://localhost:4200/main.bundle.js:66051:29) at ActivateRoutes.activate (http://localhost:4200/main.bundle.js:66046:14) at http://localhost:4200/main.bundle.js:65787:56 at SafeSubscriber._next (http://localhost:4200/main.bundle.js:9000:21) at SafeSubscriber.__tryOrSetError (http://localhost:4200/main.bundle.js:42013:16) at SafeSubscriber.next (http://localhost:4200/main.bundle.js:41955:27) 下面我们把 RouterModule.forRoot([ { path: '',redirectTo: 'login',pathMatch: 'full' },{ path: 'login',component: LoginComponent } ]) 注意路径配置的顺序是非常重要的,Angular2使用“先匹配优先”的原则,也就是说如果一个路径可以同时匹配几个路径配置的规则的话,以第一个匹配的规则为准。 但是现在还有一点小不爽,就是直接在 import { Routes,RouterModule } from '@angular/router'; import { LoginComponent } from './login/login.component'; export const routes: Routes = [ { path: '',pathMatch: 'full' },{ path: 'login',component: LoginComponent } ]; export const routing = RouterModule.forRoot(routes); 接下来我们在 import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; 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,routing ],providers: [ {provide: 'auth',useClass: AuthService} ],bootstrap: [AppComponent] }) export class AppModule { } 让待办事项变得有意义现在我们来规划一下根路径'',对应根路径我们想建立一个todo组件,那么我们使用 export const routes: Routes = [ { path: '',redirectTo: 'todo',{ path: 'todo',component: TodoComponent },component: LoginComponent } ]; 在浏览器中键入 我们希望的Todo页面应该有一个输入待办事项的输入框和一个显示待办事项状态的列表。那么我们先来定义一下todo的结构,todo应该有一个id用来唯一标识,还应该有一个desc用来描述这个todo是干什么的,再有一个completed用来标识是否已经完成。好了,我们来建立这个todo模型吧,在todo文件夹下新建一个文件 export class Todo { id: number; desc: string; completed: boolean; } 然后我们应该改造一下todo组件了,引入刚刚建立好的todo对象,并且建立一个todos数组作为所有todo的集合,一个desc是当前添加的新的todo的内容。当然我们还需要一个addTodo方法把新的todo加到todos数组中。这里我们暂且写一个漏洞百出的版本。 import { Component,OnInit } from '@angular/core'; import { Todo } from './todo.model'; @Component({ selector: 'app-todo',templateUrl: './todo.component.html',styleUrls: ['./todo.component.css'] }) export class TodoComponent implements OnInit { todos: Todo[] = []; desc = ''; constructor() { } ngOnInit() { } addTodo(){ this.todos.push({id: 1,desc: this.desc,completed: false}); this.desc = ''; } } 然后我们改造一下 <div> <input type="text" [(ngModel)]="desc" (keyup.enter)="addTodo()"> <ul> <li *ngFor="let todo of todos">{{ todo.desc }}</li> </ul> </div> 如上面代码所示,我们建立了一个文本输入框,这个输入框的值应该是新todo的描述(desc),我们想在用户按了回车键后进行添加操作( 如果我们还记得之前提到的业务逻辑应该放在单独的service中,我们还可以做的更好一些。在todo文件夹内建立TodoService: import { Injectable } from '@angular/core'; import {Todo} from './todo.model'; import { UUID } from 'angular2-uuid'; @Injectable() export class TodoService { todos: Todo[] = []; constructor() { } addTodo(todoItem:string): Todo[] { let todo = { id: UUID.UUID(),desc: todoItem,completed: false }; this.todos.push(todo); return this.todos; } } 当然我们还要把组件中的代码改成使用service的 import { Component,OnInit } from '@angular/core'; import { Todo } from './todo.model'; import { TodoService } from './todo.service'; @Component({ selector: 'app-todo',styleUrls: ['./todo.component.css'],providers:[TodoService] }) export class TodoComponent implements OnInit { todos: Todo[] = []; desc = ''; constructor(private service:TodoService) { } ngOnInit() { } addTodo(){ this.todos = this.service.addTodo(this.desc); this.desc = ''; } } 为了可以清晰的看到我们的成果,我们为chrome浏览器装一个插件,在chrome的地址栏中输入 建立模拟web服务和异步操作实际的开发中我们的service是要和服务器api进行交互的,而不是现在这样简单的操作数组。但问题来了,现在没有web服务啊,难道真要自己开发一个吗?答案是可以做个假的,假作真时真亦假。我们在开发过程中经常会遇到这类问题,等待后端同学的进度是很痛苦的。所以Angular内建提供了一个可以快速建立测试用web服务的方法:内存 (in-memory) 服务器。 一般来说,你需要知道自己对服务器的期望是什么,期待它返回什么样的数据,有了这个数据呢,我们就可以自己快速的建立一个内存服务器了。拿这个例子来看,我们可能需要一个这样的对象 class Todo { id: string; desc: string; completed: boolean; } 对应的JSON应该是这样的 { "data": [ { "id": "f823b191-7799-438d-8d78-fcb1e468fc78","desc": "blablabla","completed": false },{ "id": "c316a3bf-b053-71f9-18a3-0073c7ee3b76","desc": "tetssts",{ "id": "dd65a7c0-e24f-6c66-862e-0999ea504ca0","desc": "getting up","completed": false } ] } 首先我们需要安装 import { InMemoryDbService } from 'angular-in-memory-web-api'; import { Todo } from './todo.model'; export class InMemoryTodoDbService implements InMemoryDbService { createDb() { let todos: Todo[] = [ {id: "f823b191-7799-438d-8d78-fcb1e468fc78",desc: 'Getting up',completed: true},{id: "c316a3bf-b053-71f9-18a3-0073c7ee3b76",desc: 'Go to school',completed: false} ]; return {todos}; } } 可以看到,我们创建了一个实现 import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryTodoDbService } from './todo/todo-data'; 然后在imports数组中紧挨着 现在我们在service中试着调用我们的“假web服务”吧 import { Injectable } from '@angular/core'; import { Http,Headers } from '@angular/http'; import { UUID } from 'angular2-uuid'; import 'rxjs/add/operator/toPromise'; import { Todo } from './todo.model'; @Injectable() export class TodoService { //定义你的假WebAPI地址,这个定义成什么都无所谓 //只要确保是无法访问的地址就好 private api_url = 'api/todos'; private headers = new Headers({'Content-Type': 'application/json'}); constructor(private http: Http) { } // POST /todos addTodo(desc:string): Promise<Todo> { let todo = { id: UUID.UUID(),desc: desc,completed: false }; return this.http .post(this.api_url,JSON.stringify(todo),{headers: this.headers}) .toPromise() .then(res => res.json().data as Todo) .catch(this.handleError); } private handleError(error: any): Promise<any> { console.error('An error occurred',error); return Promise.reject(error.message || error); } } 上面的代码我们看到定义了一个api_url = 'api/todos',你可能会问这个是怎么来的?分两部分看,api/todos中前面的api定义成什么都可以,但后面这个todos是有讲究的,我们回去看一下srcapptodotodo-data.ts返回的return {todos},这个其实是return {todos: todos}的省略表示形式,如果我们不想让这个后半部分是todos,我们可以写成{nahnahnah: todos}。这样的话我们改写成api_url = 'blablabla/nahnahnah'也无所谓,因为这个内存Web服务的机理是拦截Web访问,也就是说随便什么地址都可以,内存Web服务会拦截这个地址并解析你的请求是否满足RESTful API的要求 简单来说RESTful API中GET请求用于查询,PUT用于更新,DELETE用于删除,POST用于添加。比如如果url是api/todos,那么
在service的构造函数中我们注入了Http,而angular的Http封装了大部分我们需要的方法,比如例子中的增加一个todo,我们就调用 这个请求发出后返回的是一个Observable(可观察对象),我们把它转换成Promise然后处理res(Http Response)。Promise提供异步的处理,注意到then中的写法,这个和我们传统编程写法不大一样,叫做lamda表达式,相当于是一个匿名函数, 还要一点需要强调的是:在用内存Web服务时,一定要注意 下一步我们来更改Todo组件的addTodo方法以便可以使用我们新的异步http方法 addTodo(){ this.service .addTodo(this.desc) .then(todo => { this.todos = [...this.todos,todo]; this.desc = ''; }); } 这里面的前半部分应该还是好理解的: let arr = [1,2,3]; let arr2 = [...arr]; arr2.push(4); // arr2 变成了 [1,3,4] // arr 保存原来的样子 let arr3 = [0,1,2]; let arr4 = [3,4,5]; arr3.push(...arr4); // arr3变成了[0,5] let arr5 = [0,2]; let arr6 = [-1,...arr5,3]; // arr6 变成了[-1,3] 所以呢我们上面的 //srcapptodotodo.service.ts import { Injectable } from '@angular/core'; import { Http,Headers } from '@angular/http'; import { UUID } from 'angular2-uuid'; import 'rxjs/add/operator/toPromise'; import { Todo } from './todo.model'; @Injectable() export class TodoService { private api_url = 'api/todos'; private headers = new Headers({'Content-Type': 'application/json'}); constructor(private http: Http) { } // POST /todos addTodo(desc:string): Promise<Todo> { let todo = { id: UUID.UUID(),{headers: this.headers}) .toPromise() .then(res => res.json().data as Todo) .catch(this.handleError); } // PUT /todos/:id toggleTodo(todo: Todo): Promise<Todo> { const url = `${this.api_url}/${todo.id}`; console.log(url); let updatedTodo = Object.assign({},todo,{completed: !todo.completed}); return this.http .put(url,JSON.stringify(updatedTodo),{headers: this.headers}) .toPromise() .then(() => updatedTodo) .catch(this.handleError); } // DELETE /todos/:id deleteTodoById(id: string): Promise<void> { const url = `${this.api_url}/${id}`; return this.http .delete(url,{headers: this.headers}) .toPromise() .then(() => null) .catch(this.handleError); } // GET /todos getTodos(): Promise<Todo[]>{ return this.http.get(this.api_url) .toPromise() .then(res => res.json().data as Todo[]) .catch(this.handleError); } private handleError(error: any): Promise<any> { console.error('An error occurred',error); return Promise.reject(error.message || error); } } 然后更新 import { Component,OnInit } from '@angular/core'; import { TodoService } from './todo.service'; import { Todo } from './todo.model'; @Component({ selector: 'app-todo',providers: [TodoService] }) export class TodoComponent implements OnInit { todos : Todo[] = []; desc: string = ''; constructor(private service: TodoService) {} ngOnInit() { this.getTodos(); } addTodo(){ this.service .addTodo(this.desc) .then(todo => { this.todos = [...this.todos,todo]; this.desc = ''; }); } toggleTodo(todo: Todo) { const i = this.todos.indexOf(todo); this.service .toggleTodo(todo) .then(t => { this.todos = [ ...this.todos.slice(0,i),t,...this.todos.slice(i+1) ]; }); } removeTodo(todo: Todo) { const i = this.todos.indexOf(todo); this.service .deleteTodoById(todo.id) .then(()=> { this.todos = [ ...this.todos.slice(0,...this.todos.slice(i+1) ]; }); } getTodos(): void { this.service .getTodos() .then(todos => this.todos = [...todos]); } } 更新模板文件 <section class="todoapp"> <header class="header"> <h1>Todos</h1> <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="desc" (keyup.enter)="addTodo()"> </header> <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"> <div class="view"> <input class="toggle" type="checkbox" (click)="toggleTodo(todo)" [checked]="todo.completed"> <label (click)="toggleTodo(todo)">{{todo.desc}}</label> <button class="destroy" (click)="removeTodo(todo); $event.stopPropagation()"></button> </div> </li> </ul> </section> <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> </section> 更新组件的css样式: .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0,0.2),0 25px 50px 0 rgba(0,0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp 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; } .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); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } 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; } .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; } .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 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; } /* 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; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } 更新 /* You can add global styles to this file,and also import other style files */ html,body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { font: 14px 'Helvetica Neue',Helvetica,Arial,sans-serif; line-height: 1.4em; background: #f5f5f5; color: #4d4d4d; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 300; } :focus { outline: 0; } .hidden { display: none; } .info { margin: 65px auto 0; color: #bfbfbf; font-size: 10px; text-shadow: 0 1px 0 rgba(255,255,0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } 现在我们看看成果吧,现在好看多了 第一节:Angular 2.0 从0到1 (一) (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |