jiayisheji / blog

没事写写文章,喜欢的话请点star,想订阅点watch,千万别fork!
https://jiayisheji.github.io/blog/
505 stars 49 forks source link

使用 Angular6 创建一个 CRUD 应用程序--Todolist #17

Open jiayisheji opened 6 years ago

jiayisheji commented 6 years ago

五一假期过后,Angular6发布正式版,相关联的UI组件库Material 和 脚手架CLI,也一并发布6。 升级核心依赖:

工作中已经完成一个Angular6项目,这里来写一个简单的Angular6教程。

古语云:君子谋而后动,三思而后行。我们做一个功能,先规划功能细节。

先看个效果图: todolist

在本文中,我们将构建一个Angular6 Todo web应用程序,允许用户:

这是一个演示应用程序,我们将一步步从零构建它们。

这里所有的代码都是公开的,所以你可以使用这些代码

这是一个在线编辑器预览

让我们开始吧!

快速开始

这里看官网文档 快速开始。详细安装指南,这里不在一一介绍。

修改一下package.json

  "scripts": {
    "start": "ng serve --open",
    ...
  },

这样可以直接使用npm start启动开发服务器并且自动打开默认浏览器并访问 http://localhost:4200/

生成我们的Todo应用程序

现在我们有了Angular-CLI,我们可以使用它来生成Todo应用程序:

生成我们的Todo应用程序

这里是生成项目的文档

ng new angular-todolist --style=scss

说明:生成一个angular-todolist项目,css预处理器用scss。

生成文件

满足我们的Todo应用程序的需要,我们需要:

我们所有相关应用都放在todoApp组件,在app组件里面使用todoApp组件。

这里是生成文件的文档

生成组件

ng g c todo-app

这样在app文件夹里面就出现todo-app文件夹

生成服务

ng g s todo-app/todo

注意:默认生成的文件都是以app为开始路径,我们需要放在todo-app里,所以是todo-app/todo

生成类

ng g cl todo-app/todo

注意:生成组件是c,生成类是cl

生成指令

ng g d todo-app/after-view-focus

我们现在的todo-app文件夹里结构应该是:

after-view-focus.directive.spec.ts
after-view-focus.directive.ts
todo-app.component.html
todo-app.component.scss
todo-app.component.spec.ts
todo-app.component.ts
todo.service.spec.ts
todo.service.ts
todo.ts

创建Todo类

因为我们使用TypeScript,我们可以使用一个类来表示Todo项目。

让我们打开src/app/todo.ts并将其内容替换为:

export class Todo {
  id: number;
  value: string;
  done: boolean = false;
  edit: boolean = false;
  constructor(values: Object = {}) {
    Object.assign(this, values);
  }
}

我们需要设计数据结构,每个Todo项有三个属性:

构造函数逻辑允许我们在实例化过程中指定属性值:

let todo = new Todo({
  title: 'The first todos',
  done: false
});

我们可以测试一下,Angular-CLI提供单元测试和e2e测试,默认生成类文件不会带测试文件,我们需要手动创建一个todo.spec.ts文件。

import { Todo } from './todo';

describe('Todo', () => {

    it('应该创建一个实例', () => {
        expect(new Todo()).toBeTruthy();
    });

    it('应该在构造函数中接受值', () => {
        const todo = new Todo({
            value: 'hello',
            done: true
        });
        expect(todo.value).toEqual('hello');
        expect(todo.done).toEqual(true);
        expect(todo.edit).toEqual(false);
    });

});

为了保证不受干扰,删除app.component.spec.ts文件,把todo-app.component.spec.ts文件里面代码都注释起来。

为了验证我们的代码是否按预期工作,我们现在可以运行单元测试:

npm test

这将执行业力来运行所有单元测试。如果单元测试失败,可以联系我。

现在我们有了一个Todo类,让我们创建一个Todo服务来为我们管理所有的Todo项。

创建Todo服务

TodoService将负责管理我们的Todo项目。

在以后的文章中,我们将看到如何与REST API通信,但是现在我们将把所有数据存储在内存存储中。

现在,我们可以将todo管理逻辑添加到src/app/todo.services.ts中的TodoService中

import { Injectable } from '@angular/core';
import { Todo } from './todo';

@Injectable()
export class TodoService {
  // Placeholder for todo's
  todos: Todo[] = [];
  /** Used to generate unique ID's */
  nextId = 0;

  constructor() { }

  // Simulate POST /todos
  addTodo(todo: Todo): TodoService {
    todo.id = Date.now();
    this.todos.push(todo);
    return this;
  }

  // Simulate DELETE /todos/:id
  deleteTodoById(id: number): TodoService {
    this.todos = this.todos
      .filter(todo => todo.id !== id);
    return this;
  }

  // Simulate POST /todos/delete
  deleteAllTodo(): TodoService {
    this.todos = this.todos
      .filter(todo => !todo.done);
    return this;
  }

  // Simulate PUT /todos/:id
  updateTodoById(id: number, values: Object = {}): Todo {
    const todo = this.getTodoById(id);
    if (!todo) {
      return null;
    }
    Object.assign(todo, values);
    return todo;
  }

  // Simulate GET /todos
  getAllTodos(): Todo[] {
    return this.todos;
  }

  // Simulate GET /todos/done
  getAllDoneTodos(): Todo[] {
    return this.todos.filter(todo => todo.done);
  }

  // Simulate GET /todos/:id
  getTodoById(id: number): Todo {
    return this.todos
      .filter(todo => todo.id === id)
      .pop();
  }

  // Toggle todo done
  toggleTodoDone(todo: Todo) {
    const updatedTodo = this.updateTodoById(todo.id, {
      done: !todo.done
    });
    return updatedTodo;
  }
}

我们已经完成必备的服务,实际的实现细节的方法不是本文的目的所必需的。这是主要表达意思, 我们的业务逻辑集中在服务。

确保我们的逻辑是预期,我们将单元测试添加到src/app/todo.service.spec中。

Angular-cli已为我们生成测试模板,我们只需要关心如何实现测试:

import {
  inject, TestBed
} from '@angular/core/testing';

import { Todo } from './todo';
import { TodoService } from './todo.service';

describe('Todo Service', () => {

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [],
      providers: [TodoService]
    });
  });

  describe('#getAllTodos()', () => {

    it('应该默认返回一个空数组', inject([TodoService], (service: TodoService) => {
      expect(service.getAllTodos()).toEqual([]);
    }));

    it('应该返回所有待办事项', inject([TodoService], (service: TodoService) => {
      const todo1 = new Todo({ value: 'Hello 1', done: false });
      const todo2 = new Todo({ value: 'Hello 2', done: true });
      service.addTodo(todo1);
      service.addTodo(todo2);
      expect(service.getAllTodos()).toEqual([todo2, todo1]);
    }));

  });

  describe('#save(todo)', () => {

    it('应该自动分配一个时间戳的ID', inject([TodoService], (service: TodoService) => {
      const todo1 = service.addTodo(new Todo({ value: 'Hello 1', done: false }));
      const todo2 = service.addTodo(new Todo({ value: 'Hello 2', done: true }));
      expect(service.getTodoById(todo1.id)).toEqual(todo1);
      expect(service.getTodoById(todo2.id)).toEqual(todo2);
    }));

  });

  describe('#deleteTodoById(id)', () => {

    it('应该删除相应ID的待办事项', inject([TodoService], (service: TodoService) => {
      const todo1 = service.addTodo(new Todo({ value: 'Hello 1', done: false }));
      const todo2 = service.addTodo(new Todo({ value: 'Hello 2', done: true }));
      expect(service.getAllTodos()).toEqual([todo2, todo1]);
      service.deleteTodoById(todo1.id);
      expect(service.getAllTodos()).toEqual([todo2]);
      service.deleteTodoById(todo2.id);
      expect(service.getAllTodos()).toEqual([]);
    }));

    it('如果没有找到使用相应ID的待办事项,则不应删除任何内容', inject([TodoService], (service: TodoService) => {
      const todo1 = service.addTodo(new Todo({ value: 'Hello 1', done: false }));
      const todo2 = service.addTodo(new Todo({ value: 'Hello 2', done: true }));
      expect(service.getAllTodos()).toEqual([todo2, todo1]);
      service.deleteTodoById(3);
      expect(service.getAllTodos()).toEqual([todo2, todo1]);
    }));

  });

  describe('#updateTodoById(id, values)', () => {

    it('应该返回相应ID和更新的数据todo', inject([TodoService], (service: TodoService) => {
      const todo = service.addTodo(new Todo({ value: 'Hello 1', done: false }));
      const updatedTodo = service.updateTodoById(todo.id, {
        value: 'new value'
      });
      expect(updatedTodo.value).toEqual('new value');
    }));

    it('如果未找到待办事项应该返回null', inject([TodoService], (service: TodoService) => {
      const todo = service.addTodo(new Todo({ value: 'Hello 1', done: false }));
      const updatedTodo = service.updateTodoById(2, {
        value: 'new value'
      });
      expect(updatedTodo).toEqual(null);
    }));

  });

  describe('#toggleTodoDone(todo)', () => {

    it('应该返回更新后的待办事项与完成状态', inject([TodoService], (service: TodoService) => {
      const todo = new Todo({ value: 'Hello 1', done: false });
      service.addTodo(todo);
      const updatedTodo = service.toggleTodoDone(todo);
      expect(updatedTodo.done).toEqual(true);
      service.toggleTodoDone(todo);
      expect(updatedTodo.done).toEqual(false);
    }));

  });

});

检查我们的业务逻辑是否有效,我们运行单元测试:

npm test

好了,现在我们有一个可以通过测试的TodoService,是时候实现应用程序的主要部分了。

创建TodoApp组件

组件是Angular最小的单元了,整个Angular应用就是一个颗组件树构成。

我们生成项目时候,angular-cli默认为我们创建了app-root根组件,我们现在生产的app-todo-app,放到app.component.html里面,删除其他html。

一个组件是有3部分组建成:

  1. 模板结构 todo-app.component.html
  2. 样式美化 todo-app.component.scss
  3. 交互行为 todo-app.component.ts

模板和样式也可以内联脚本文件中指定。Angular-CLI默认创建单独的文件,所以在本文中我们将使用单独的文件。

import { Component } from '@angular/core';
@Component({
  selector: 'app-todo-app',
  templateUrl: './todo-app.component.html',
  styleUrls: ['./todo-app.component.scss']
})
export class TodoAppComponent implements OnInit {
    constructor() { }
}

我们先来添加组件的视图todo-app.component.html

<header class="header">
    <h1>Todos</h1>
    <form class="todo-form" (ngSubmit)="addTodo()">
        <input class="add-todo" [(ngModel)]="newTodo" name="first" placeholder="What needs to be done?" required="required" autocomplete="off">
        <button type="submit" class="add-btn" *ngIf="newTodo.length">+</button>
    </form>
</header>
<main class="main" *ngIf="todos.length">
    <input class="toggle-all" type="checkbox" [(ngModel)]="allDone" (ngModelChange)="toggleAllTodoDone($event)">
    <ul class="todo-list">
        <li *ngFor="let todo of todos" [class.completed]="todo.done" (dblclick)="editingTodo(todo)">
            <div class="view" *ngIf="!todo.edit">
                <input class="toggle" type="checkbox" [checked]="todo.done" (click)="toggleDoneTodo(todo)">
                <label>{{ todo.value }}</label>
                <button class="destroy" (click)="destroyTodo(todo)"></button>
            </div>
            <input class="edit" *ngIf="todo.edit" appAfterViewFocus [value]="todo.value" #edit (blur)="cancelEditingTodo(todo)" placeholder="What do you need to write?" (keyup.enter)="editedTodo(todo, edit)">
        </li>
    </ul>
</main>
<footer class="footer" *ngIf="todos.length">
    <span class="todo-count">
    <strong>{{ todoCount }}</strong>
    <span> items left</span>
    </span>
    <button class="clear-completed" (click)="destroyAllTodo()" [class.clear-operate]="clearCount">
      <span>Clear </span>
      <strong>{{ clearCount }}</strong>
      <span> done items</span>
    </button>
</footer>

来简单说一下Angular模板语法:

更多的Angular模板语法,你应该阅读官方的文档模板的语法。

让我们一一介绍:

整个模板分为3个结构块: header,main,footer;

先说输入创建一个新的待办事项:

<form class="todo-form" (ngSubmit)="addTodo()">
    <input class="add-todo" [(ngModel)]="newTodo" name="first" placeholder="What needs to be done?" required="required" autocomplete="off">
    <button type="submit" class="add-btn" *ngIf="newTodo.length">+</button>
</form>

不要担心newTodo或addTodo()从哪里来,我们很快就会讲到那里,现在只需要试着去理解的模板语法。

接下来是一段显示待办事项:

<main class="main" *ngIf="todos.length"></main>

在这个部分中,我们循环一个元素来显示每个待办事项:

<li *ngFor="let todo of todos" [class.completed]="todo.done" (dblclick)="editingTodo(todo)"></li>

最后我们显示待办事项的细节为每个ngFor中的待办事项:

<div class="view" *ngIf="!todo.edit">
  <input class="toggle" type="checkbox" [checked]="todo.done" (click)="toggleDoneTodo(todo)">
  <button class="destroy" (click)="destroyTodo(todo)"></button>
</div>
<input class="edit" *ngIf="todo.edit" appAfterViewFocus [value]="todo.value" #edit (blur)="cancelEditingTodo(todo)" placeholder="What do you need to write?" (keyup.enter)="editedTodo(todo, edit)">

appAfterViewFocus是angular属性型指令,在 Angular 中有三种类型的指令:

  1. 组件 — 拥有模板的指令
  2. 结构型指令 — 通过添加和移除 DOM 元素改变 DOM 布局的指令
  3. 属性型指令 — 改变元素、组件或其它指令的外观和行为的指令。

注意: 为什么要写这个指令,它作用是什么?它作用是当编辑时,input出现时候,自动获取焦点,不用用户再次去点击输入框,触发获取焦点事件,还有一个更重要的原因,如果没有焦点,失去焦点事件就无法执行,这样输入就不会被隐藏。它的写法很简单:

import { Directive, AfterViewInit, ElementRef } from '@angular/core';

@Directive({
  selector: '[appAfterViewFocus]'
})
export class AfterViewFocusDirective implements AfterViewInit {

  constructor(private elementRef: ElementRef) { }

  ngAfterViewInit() {
    this.elementRef.nativeElement.focus();
  }

}

#edit是angular模板引用变量;

注意: 这里拿到就是input这个dom,我们可以直接操作获取它上面的属性和方法。

为什么不用双向绑定[(ngModel)]?

什么是双向绑定: 数据模型(Module)和视图(View)之间的双向绑定。

如果使用双向绑定,我们修改以后,我们数据就直接跟着一起被修改,那么我们要操作取消操作怎么办,增加一个临时的属性来记录它,取消时候就直接回滚,确认就直接清除这个临时属性。

如果不使用双向绑定,我们先赋值给视图,视图修改以后,我们的数据还没有变,取消操作直接取消就行,确认操作,拿到dom引用,把dom的值去更新数据。

关于全选效果

<input class="toggle-all" type="checkbox" [(ngModel)]="allDone" (ngModelChange)="toggleAllTodoDone($event)">

我们来说最后一块统计结构:

<footer class="footer" *ngIf="todos.length">
    <span class="todo-count">
    <strong>{{ todoCount }}</strong>
    <span> items left</span>
    </span>
    <button class="clear-completed" (click)="destroyAllTodo()" [class.clear-operate]="clearCount">
      <span>Clear </span>
      <strong>{{ clearCount }}</strong>
      <span> done items</span>
    </button>
</footer>

模板我们已经介绍完,关于css不是我们重点,这里忽略讲解。

接下来我们该介绍todo-app.component.ts:

首先需要引入依赖

import { TodoService } from './todo.service';
import { Todo } from './todo';

接下来就是angular特色之一依赖注入,这里不过多介绍。

@Component({
  selector: 'app-todo-app',
  templateUrl: './todo-app.component.html',
  styleUrls: ['./todo-app.component.scss'],
  providers: [TodoService]
})
export class TodoAppComponent {
  newTodo: string = '';
  constructor(
    private todoService: TodoService
  ) { }

注意:providers可以在模块下,也可以在组件里,这也限定他们使用范围。模块里面注册,适用于该模块下所有的组件,服务,指令等;组件里面注册,只适用于当前组件和子组件。

每当视图中输入值的变化,更新组件实例的价值。当组件实例中的值改变,视图中输入元素中的值的变化。

接下来,我们实现我们在视图中使用的所有方法。

  /**
   * add todo
   * @memberof TodoAppComponent
   */
  addTodo(): void {
    if (!this.newTodo) {
      return alert('What do you need to write?');
    }
    this.todoService.addTodo(new Todo({
      value: this.newTodo
    }));
    this.newTodo = '';
  }

  /**
   * destroy todo
   * @memberof TodoAppComponent
   */
  destroyTodo(todo: Todo): void {
    this.todoService.deleteTodoById(todo.id);
  }

  /**
   * destroy done todo
   * @memberof TodoAppComponent
   */
  destroyAllTodo(): void {
    if (!this.clearCount) {
      return;
    }
    if (!confirm('Do you need to delete the selected one?')) {
      return;
    }
    this.todoService.deleteAllTodo();
  }

  /**
   * toggle todo done
   * @memberof TodoAppComponent
   */
  toggleDoneTodo(todo: Todo): void {
    this.todoService.toggleTodoDone(todo);
  }

  /**
   * toggle all todo done
   * @memberof TodoAppComponent
   */
  toggleAllTodoDone(event: boolean): void {
    this.todos.forEach(item => item.done = event);
  }

  /**
   * editing todo
   * @memberof TodoAppComponent
   */
  editingTodo(todo: Todo): void {
    if (!todo.done) {
      todo.edit = true;
    }
  }

  /**
   * cancel editing todo
   * @memberof TodoAppComponent
   */
  cancelEditingTodo(todo: Todo): void {
    todo.edit = false;
  }

  /**
   * edited todo
   * @memberof TodoAppComponent
   */
  editedTodo(todo: Todo, input: HTMLInputElement): void {
    todo.value = input.value;
    todo.edit = false;
  }

  /**
   * get todos
   * @memberof TodoAppComponent
   */
  get todos(): Todo[] {
    return this.todoService.getAllTodos();
  }

  /**
   * get todos all done be get todos
   * @memberof TodoAppComponent
   */
  get allDone(): boolean {
    const todos = this.todos;
    return todos.length && todos.filter(item => item.done).length === todos.length;
  }

  /**
   * get todos all not done number
   * @memberof TodoAppComponent
   */
  get todoCount(): number {
    return this.todos.filter(item => !item.done).length;
  }

  /**
   * get todos all done number
   * @memberof TodoAppComponent
   */
  get clearCount(): number {
    return this.todos.filter(item => item.done).length;
  }

这里有4个 get,在Typescript存取器, 通过getters/setters来截取对对象成员的访问。 它能帮助我们有效的控制对对象成员的访问。这里只要控制器里面值发送变化,模板就会更着改变,很方便。

注意:无论是服务还是组件里,都是需要熟练使用原生数据操作方法,比如数组,对象,字符串等。这里主要使用数组相关方法,如果你对这些还不熟,请赶紧去提升一下。es6以后又新增很多方法,操作数据会更方便。angular是数据驱动,如果不会玩转操作,基本很难继续下去。

功能很小,应该不言自明todoService我们代表所有的业务逻辑。

委派业务逻辑服务是良好的编程实践,因为它能让我们集中管理和测试业务逻辑。

我们还为大家编写一个E2E测试用例,可以查阅e2e文件里面文件,运行命名npm run e2e即可。

部署到GitHub页面

github给我们每个项目都运行有一个预览页面,我们叫它github-pages

提交代码

  1. 先打包本地代码
ng build --prod --base-href https://jiayisheji.github.io/angular-todolist/

注意:github-pages预览地址是 你的用户名.github.io/你的项目名/

--base-href:修改html里面的basehref属性,如果有路由必须要使用的。

  1. 提交dist文件夹的内容到gh-pages分支
git add -f dist && git commit -n -m \"(release): git-pages\" && git subtree push --prefix dist origin gh-pages

注意:就是打包以后的目录,需要特别注意一下,angular-cli6是一个多工程的脚手架,打包后生成的是dist/angular-todolist,我们最终需要上传是这个文件夹里面的内容,那么就需要改脚本。

git add -f dist && git commit -n -m \"(release): git-pages\" && git subtree push --prefix dist/angular-todolist origin gh-pages
  1. 提交本地代码到远程master并打tags
git push --follow-tags origin master

我在所有项目里面都会用到它

  1. 写成npm命令
"_github": "ng build --prod --base-href https://jiayisheji.github.io/angular-todolist/",
"_publish": "git add -f dist && git commit -n -m \"(release): git-pages\" && git subtree push --prefix dist/angular-todolist origin gh-pages",
"git-pages": "npm run _github && npm run _publish"
"release":"git push --follow-tags origin master"

运行命令

npm run git-pages
npm run release

代码提交需要去github,项目下设置里面开启github-pages.

开启 Github-pages

  1. 打开你的项目,点击设置

gq 6u 0 f 2w s ghh6z4

  1. ctrl+f 搜索 GitHub Pages

4zd_h1d 4 2 h 0 7 2a

  1. 点击设置分支,默认是none,选择gh-pages branch

7tpn yli ne amuuyl 1 7

  1. 点击save.

hx8q8f4 p5u6vf 81r8ey7t

就好出现你的Github Pages链接,你可以做代码演示,分享给其他小伙伴观看,也可以做静态blog

注意:一旦启用就不能再选择none,只能你删除项目。你删除分支,访问就会出现404。

总结

毫无疑问,Angular是一个平台。一个非常强大前端框架!

我们讨论了许多让我们回顾所学在本文中:

麻雀虽小,五脏俱全,Todo应用看起来,功能很简单,其实它里面功能可以做很多衍生,都是我们平常业务需要的,比如购物车, 全选等。

这个有个类似的变种需求功能:我也不知道叫什么名字,antd里面叫穿梭框。这就留个大家一个作业吧。 k 69ahw_ 3q3w8m wksqwp

如果你不知道如何下手,可以跟我交流