23、实现待办事项应用的增删功能:Angular 实战

实现待办事项应用的增删功能:Angular 实战

在开发 Angular 应用时,我们不仅要能列出任务,还需要通过 Angular UI 实现添加新任务和删除现有任务的功能。接下来,我们将详细介绍如何实现这些功能。

设计添加任务功能

要实现添加新任务的功能,我们需要在组件和服务中添加一些新代码,管道部分保持不变,同时还需要修改 tasks.component.html 文件。我们先从组件相关的工作开始。

组件的演进

为了支持添加任务功能,组件需要通过以下测试:
- newTask 应正确初始化
- 组件应能将无数据的 newTask 正确转换为 JSON
- 组件应能将有数据的 newTask 正确转换为 JSON
- addTask 应向服务注册处理程序
- updateMessage 应更新消息并调用 getTasks
- disableAddTask 应使用 validateTask

下面是具体的实现步骤:
1. 验证 newTask 初始化 :打开 test/client/app/tasks/tasks.component-test.js 文件,添加以下测试代码:

it('newTask should be initialized properly', function() {
    expect(tasksComponent.newTask.name).to.be.eql('');
    expect(tasksComponent.newTask.date).to.be.eql('');
});

为了使这个测试通过,我们需要在组件的构造函数中进行简单修改。打开 public/src/app/tasks/tasks.component.js 文件,添加以下代码:

this.newTask = {name: '', date: ''};
  1. 验证无数据的 newTask 转换为 JSON :在 test/client/app/tasks/tasks.component-test.js 文件中添加以下测试代码:
it('should properly convert newTask with no data to JSON', function() {
    var newTask = tasksComponent.convertNewTaskToJSON();
    expect(newTask.name).to.be.eql('');
    expect(newTask.month).to.be.NAN;
    expect(newTask.day).to.be.NAN;
    expect(newTask.year).to.be.NAN;
});
  1. 验证有数据的 newTask 转换为 JSON :在 test/client/app/tasks/tasks.component-test.js 文件中添加以下测试代码:
it('should properly convert newTask with data to JSON', function() {
    var newTask = {name: 'task a', date: '6/10/2016'};
    var newTaskJSON = {name: 'task a', month: 6, day: 10, year: 2016};
    tasksComponent.newTask = newTask;
    expect(tasksComponent.convertNewTaskToJSON()).to.be.eql(newTaskJSON);
});

实现 convertNewTaskToJSON 函数,满足上述两个测试:

convertNewTaskToJSON: function() {
    var dateParts = this.newTask.date.split('/');
    return {
        name: this.newTask.name,
        month: parseInt(dateParts[0]),
        day: parseInt(dateParts[1]),
        year: parseInt(dateParts[2])
    };
},
  1. 验证 addTask 与服务的交互 :在 test/client/app/tasks/tasks.component-test.js 文件中添加以下测试代码:
it('addTask should register handlers with service', function() {
    var observableMock =
        sandbox.mock(observable)
           .expects('subscribe')
           .withArgs(updateMessageBindStub, updateErrorBindStub);
    var taskStub = {};
    tasksComponent.convertNewTaskToJSON = function() { return taskStub; };
    sandbox.stub(tasksService, 'add')
       .withArgs(taskStub)
       .returns(observable);
    tasksComponent.addTask();
    observableMock.verify();
});

为了使这个测试通过,我们需要在测试套件中进行两处修改:

var updateMessageBindStub = function() {};
sandbox.stub(tasksComponent.updateMessage, 'bind')
   .withArgs(tasksComponent)
   .returns(updateMessageBindStub);

public/src/app/tasks/tasks.component.js 文件中实现 addTask 函数:

addTask: function() {
    this.service.add(this.convertNewTaskToJSON())
       .subscribe(this.updateMessage.bind(this),
                  this.updateError.bind(this));
},
updateMessage: function() {},
  1. 设计 updateMessage 函数 :在 test/client/app/tasks/tasks.component-test.js 文件中添加以下测试代码:
it('updateMessage should update message and call getTasks', function(done) {
    tasksComponent.getTasks = function() { done(); };
    tasksComponent.updateMessage('good');
    expect(tasksComponent.message).to.be.eql('good');
});

修改 public/src/app/tasks/tasks.component.js 文件中的 updateMessage 函数:

updateMessage: function(message) {
    this.message = message;
    this.getTasks();
},
  1. 实现 disableAddTask 函数
    • 验证 validateTask 属性:在 test/client/app/tasks/tasks.component-test.js 文件中添加以下测试代码:
it('should set validateTask to common function', function() {
    expect(tasksComponent.validateTask).to.be.eql(validateTask);
});

为了使这个测试通过,我们需要在组件的构造函数中添加以下属性:

this.validateTask = validateTask;

同时,记得修改 karma.conf.js 文件的 files 部分,包含对 ./public/javascripts/common/validate-task.js 文件的引用。
- 验证 disableAddTask 函数:在 test/client/app/tasks/tasks.component-test.js 文件中添加以下测试代码:

it('disableAddTask should use validateTask', function() {
    tasksComponent.newTask = {name: 'task a', date: '6/10/2016'};
    var validateTaskSpy = sinon.spy(tasksComponent, 'validateTask');
    expect(tasksComponent.disableAddTask()).to.be.false;
    expect(validateTaskSpy).to.have.been.calledWith(
        tasksComponent.convertNewTaskToJSON());
});

public/src/app/tasks/tasks.component.js 文件中实现 disableAddTask 函数:

disableAddTask: function() {
    return !this.validateTask(this.convertNewTaskToJSON());
},
服务的演进

服务中需要的新 add 函数应该向后端服务器发出 HTTP POST 请求,发送的数据必须是 JSON 格式。我们可以参考示例来了解该函数如何通过 HTTP 对象与后端进行交互。
1. 修改测试文件 :打开 test/client/app/tasks/tasks.service-test.js 文件,在 beforeEach 函数中为 http 存根添加一个新的 post 属性:

beforeEach(function() {
    sandbox = sinon.sandbox.create();
    http = {
        get: function() {},
        post: function() {}
    };
    tasksService = new app.TasksService(http);
    //...
  1. 编写 add 函数测试 :在 test/client/app/tasks/tasks.service-test.js 文件中添加以下测试代码:
it('add should pass task to /tasks using POST', function() {
    var taskStub = {name: 'foo', month: 1, day: 1, year: 2017};
    var options =
        {headers: new ng.http.Headers({'Content-Type': 'application/json'})};
    sandbox.stub(http, 'post')
       .withArgs('/tasks', JSON.stringify(taskStub), options)
       .returns(observable);
    expect(tasksService.add(taskStub)).to.be.eql(observable);
    expect(observable.map.calledWith(tasksService.extractData)).to.be.true;
    expect(observable.catch.calledWith(tasksService.returnError)).to.be.true;
});
  1. 实现 add 函数 :在 public/src/app/tasks/tasks.service.js 文件中添加以下代码:
add: function(task) {
    var options =
        {headers: new ng.http.Headers({'Content-Type': 'application/json'})};
    return this.http.post('/tasks', JSON.stringify(task), options)
       .map(this.extractData)
       .catch(this.returnError);
},
  1. 修改 extractData 函数 :由于后端在添加任务请求时返回纯文本,我们需要修改 extractData 函数以处理 JSON 响应或纯文本响应。在 test/client/app/tasks/tasks.service-test.js 文件中添加以下测试代码:
it('extractData should return text if not json()', function() {
    var fakeBody = 'somebody';
    var response = {status: 200, text: function() { return fakeBody; } };
    expect(tasksService.extractData(response)).to.be.eql(fakeBody);
});

修改 public/src/app/tasks/tasks.service.js 文件中的 extractData 函数:

extractData: function(response) {
    if(response.status !== 200)
        throw new Error('Request failed with status: ' + response.status);
    try {
        return response.json();
    } catch(ex) {
        return response.text();
    }
},
查看添加任务功能

在通过 UI 添加新任务之前,我们需要修改 tasks.component.html 文件,添加输入字段让用户输入新任务的详细信息。修改后的文件如下:

<body>
    <div class="heading">TO-DO</div>
    <div id="newtask">
        <div>Create a new task</div>
        <label>Name</label>
        <input type="text" id="name" [(ngModel)]="newTask.name"/>
        <label>Date</label>
        <input type="text" id="date" [(ngModel)]="newTask.date"/>
        <input type="submit" id="submit" (click)="addTask();"
               [disabled]="disableAddTask()" value="create"/>
    </div>
    <div id="taskslist">
        <p>Number of tasks: <span id="length">{{ tasks.length }}</span>
        <span id="message">{{ message }}</span>
        <table>
            <tr *ngFor ="let task of tasks">
                <td>{{ task.name }}</td>
                <td>{{ task.month }}/{{ task.day }}/{{ task.year }}</td>
            </table>
    </div>
</body>

现在,我们可以启动数据库守护进程,运行 npm start 启动 Express,然后在浏览器中访问 http://localhost:3000 来测试添加任务功能。

设计删除任务功能

要删除现有任务,我们需要再次对组件和服务进行改进,并对 tasks.component.html 文件进行小修改。

组件的再次演进

组件需要一个新函数 deleteTask ,它将删除任务的请求传递给服务。这个新函数可以重用我们在组件中已经实现的响应处理程序。
1. 验证服务存根 :在 test/client/app/tasks/tasks.component-test.js 文件中,确保服务存根包含 delete 函数:

beforeEach(function() {
    tasksService = {
        get: function() {},
        add: function() {},
        delete: function() {}
    };
    tasksComponent = new app.TasksComponent(tasksService, sortPipe);
    //...
  1. 编写 deleteTask 测试 :在 test/client/app/tasks/tasks.component-test.js 文件中添加以下测试代码:
it('deleteTask should register handlers with service', function() {
    var sampleTaskId = '1234123412341234';
    var observableMock =
        sandbox.mock(observable)
           .expects('subscribe')
           .withArgs(updateMessageBindStub, updateErrorBindStub);
    sandbox.stub(tasksService, 'delete')
       .withArgs(sampleTaskId)
       .returns(observable);
    tasksComponent.deleteTask(sampleTaskId);
    observableMock.verify();
});
  1. 实现 deleteTask 函数 :在 public/src/app/tasks/tasks.component.js 文件中添加以下代码:
deleteTask: function(taskId) {
    this.service.delete(taskId)
       .subscribe(this.updateMessage.bind(this),
                  this.updateError.bind(this));
},
服务的再次演进

服务现在需要一个额外的函数 delete
1. 修改测试文件 :打开 test/client/app/tasks/tasks.service-test.js 文件,在 beforeEach 函数中为 http 存根添加一个新的 delete 函数:

beforeEach(function() {
    sandbox = sinon.sandbox.create();
    http = {
        get: function() {},
        post: function() {},
        delete: function() {}
    };
    tasksService = new app.TasksService(http);
    //...
  1. 编写 delete 函数测试 :在 test/client/app/tasks/tasks.service-test.js 文件中添加以下测试代码:
it('delete should pass task to /tasks using DELETE', function() {
    var taskId = '1234';
    sandbox.stub(http, 'delete')
       .withArgs('/tasks/' + taskId)
       .returns(observable);
    expect(tasksService.delete(taskId)).to.be.eql(observable);
    expect(observable.map.calledWith(tasksService.extractData)).to.be.true;
    expect(observable.catch.calledWith(tasksService.returnError)).to.be.true;
});
  1. 实现 delete 函数 :在 public/src/app/tasks/tasks.service.js 文件中添加以下代码:
delete: function(taskId) {
    return this.http.delete('/tasks/' + taskId)
       .map(this.extractData)
       .catch(this.returnError);
},
查看删除任务功能

修改 tasks.component.html 文件,添加删除任务的链接:

<body>
    <div class="heading">TO-DO</div>
    <div id="newtask">
        <div>Create a new task</div>
        <label>Name</label>
        <input type="text" id="name" [(ngModel)]="newTask.name"/>
        <label>Date</label>
        <input type="text" id="date" [(ngModel)]="newTask.date"/>
        <input type="submit" id="submit" (click)="addTask();"
               [disabled]="disableAddTask()" value="create"/>
    </div>
    <div id="taskslist">
        <p>Number of tasks: <span id="length">{{ tasks.length }}</span>
        <span id="message">{{ message }}</span>
        <table>
            <tr *ngFor ="let task of tasks">
                <td>{{ task.name }}</td>
                <td>{{ task.month }}/{{ task.day }}/{{ task.year }}</td>
                <td>
                    <A (click)="deleteTask(task._id);">delete</A>
                </td>
            </table>
    </div>
</body>

再次启动数据库守护进程,运行 npm start 启动 Express,然后在浏览器中访问 http://localhost:3000 来测试删除任务功能。

测量代码覆盖率

当前项目的 package.json 文件已经包含了运行 Istanbul 进行客户端代码覆盖率测试的脚本。在 karma.conf.js 文件中, preprocessors 部分已经引用了 public/src 及其子目录下的所有文件进行覆盖率检测。
运行以下命令来同时运行测试并测量覆盖率:

npm run-script cover-client

测试运行完成后,打开 coverage 目录下的 index.html 文件查看覆盖率报告,你会看到 100% 的覆盖率。

通过以上步骤,我们成功开发了一个功能齐全的 Angular 前端待办事项应用,并且通过测试驱动开发保证了代码的质量和覆盖率。

实现待办事项应用的增删功能:Angular 实战

技术点分析

在整个开发过程中,涉及到多个重要的技术点,下面我们来详细分析。

组件与服务的交互

组件和服务是 Angular 应用中的核心概念,它们之间的交互是实现功能的关键。在添加和删除任务的功能中,组件负责接收用户的输入和操作,然后调用服务中的方法与后端进行通信。例如, addTask 函数在组件中被调用,它将转换后的任务对象传递给服务的 add 方法,服务再通过 HTTP 请求将数据发送到后端。这种分层设计使得代码的职责更加清晰,便于维护和扩展。

功能 组件操作 服务操作
添加任务 转换任务对象为 JSON,调用服务的 add 方法 发送 HTTP POST 请求到后端
删除任务 调用服务的 delete 方法 发送 HTTP DELETE 请求到后端
测试驱动开发(TDD)

测试驱动开发是一种重要的开发方法,它强调在编写代码之前先编写测试用例。在我们的开发过程中,每一个功能都有对应的测试用例,通过测试用例来验证代码的正确性。例如,在实现 convertNewTaskToJSON 函数时,我们先编写了两个测试用例,分别验证无数据和有数据的 newTask 转换为 JSON 的情况,然后再实现函数代码,确保函数能够通过这两个测试用例。这种开发方式可以提高代码的质量,减少 bug 的产生。

以下是一个简单的 TDD 流程:
1. 编写测试用例
2. 运行测试,测试失败
3. 编写代码使测试通过
4. 重构代码,优化代码结构

graph LR
    A[编写测试用例] --> B[运行测试,测试失败]
    B --> C[编写代码使测试通过]
    C --> D[重构代码,优化代码结构]
HTTP 请求处理

在服务中,我们使用 HTTP 请求与后端进行通信。对于添加任务,使用 HTTP POST 请求;对于删除任务,使用 HTTP DELETE 请求。同时,我们还需要处理请求的响应,例如在 extractData 函数中,需要处理 JSON 响应和纯文本响应。以下是处理 HTTP 请求的关键代码:

// 添加任务
add: function(task) {
    var options =
        {headers: new ng.http.Headers({'Content-Type': 'application/json'})};
    return this.http.post('/tasks', JSON.stringify(task), options)
       .map(this.extractData)
       .catch(this.returnError);
},

// 删除任务
delete: function(taskId) {
    return this.http.delete('/tasks/' + taskId)
       .map(this.extractData)
       .catch(this.returnError);
},

// 处理响应数据
extractData: function(response) {
    if(response.status !== 200)
        throw new Error('Request failed with status: ' + response.status);
    try {
        return response.json();
    } catch(ex) {
        return response.text();
    }
},
总结与展望

通过本次开发,我们成功实现了待办事项应用的添加和删除任务功能,并且采用了测试驱动开发的方法,保证了代码的质量和覆盖率。整个开发过程中,我们深入理解了 Angular 组件和服务的使用,以及 HTTP 请求的处理。

在未来的开发中,我们可以进一步扩展这个应用。例如,添加任务的编辑功能,允许用户修改已有的任务信息;添加任务的排序和筛选功能,方便用户查找特定的任务;还可以优化 UI 界面,提高用户体验。同时,我们可以引入更多的测试框架和工具,进一步完善测试体系,确保代码的稳定性和可靠性。

总之,Angular 是一个强大的前端框架,通过不断学习和实践,我们可以开发出更加复杂和功能丰富的应用。希望本文能够对大家在 Angular 开发方面有所帮助。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值