本教程是向您介绍 Node.js 基本概念并展示如何应用这些概念的系列教程。到目前为止,您已经安装了 Node,学习了基本的 Node.js 概念,并深入研究了 Node 事件循环。现在是时候将这些知识运用于现实世界的项目了。
获取代码
您在此学习路径中要运行的代码及相关示例位于我的 GitHub 存储库中。
项目
设想一下,您所在公司召开了一场会议,并邀请了您参加。当您到达会场时,项目经理解释说,一家大型零售商聘请贵公司编写应用程序套件。
作为概念证明,零售商要求设计购物清单应用程序。贵公司启动了为购物清单编写最小可行产品 (MVP) 的项目,但负责该项目的 Node 开发者突然离开了公司。该项目必须完成,否则客户就会将其业务转至其他公司。
您的角色是完成由先前 Node 开发者启动的 JavaScript 代码,并确保代码符合功能规范。您能够胜任这项任务吗?
用户案例
购物清单 MVP 包含 10 个用户案例。本部分包含在向客户展示 MVP 之前必须完成的案例。由于时间实在太短,这些案例将用作购物清单 MVP 的功能规范。
每个案例都有一个简短的描述,以及在案例被视为已接受之前必须满足的高级别需求的列表。
按照下面给定的顺序来实施这些案例。
商品:按 ID 查找
数据库中的每个商品都有一个唯一的 ID,用户需要能够通过其 ID 在数据库中查找特定商品。
给定一个 ID,系统将会从数据库返回与指定 ID 匹配的单个商品。
商品:按部分描述搜索
数据库中的每个商品都有一个描述,用户需要能够使用部分描述(例如"咳嗽药"、"香蕉味"或"自由范围")在数据库中找到一个或多个商品。
给定一个文本字符串(部分描述),系统将会返回数据库中包含指定文本的零个或零个以上匹配商品。
商品:按 UPC 查找
数据库中的每个商品都有一个唯一的通用产品代码 (UPC),用户需要能够通过其 UPC 在数据库中查找特定商品。
给定一个 UPC,系统将会从数据库返回与该 UPC 匹配的单个商品。
清单:创建购物清单
用户需要能够创建新的购物清单。系统将提供一种使用以下属性在数据库中创建新购物清单的方法:
描述
清单:按 ID 查找购物清单,仅返回购物清单
在创建购物清单后,会为其分配唯一 ID。 用户需要能够使用该 ID 在数据库中找到购物清单。
给定一个 ID,系统将会返回与指定 ID 匹配的单个购物清单。
清单:向购物清单添加商品
用户需要能够向数据库中的购物清单添加商品。给定一个商品 ID,系统必须能够将该商品以及以下属性添加到购物清单:
数量
清单:按 ID 查找购物清单,返回清单中的所有商品
在创建购物清单后,会为其分配唯一 ID。用户需要能够使用该 ID 在数据库中找到购物清单。
给定一个 ID,系统将会返回与指定 ID 匹配的单个购物清单以及该清单中的所有商品。
清单:更新购物清单
用户需要能够修改购物清单属性:
描述
给定一个 ID 和已更新属性的值,系统将会提供一种可更新数据库中购物清单的方法。
清单:更新购物清单中的商品
用户需要能够更新以下有关购物清单中商品的属性:
数量
已选取(该商品是否已被选取)
给定一个购物清单 ID 和商品 ID,系统将会提供一种更新数据库中商品属性的方法。
清单:从购物清单中删除商品
用户需要能够从购物清单中删除商品。
给定一个购物清单 ID 和商品 ID,系统将会提供一种从数据库中删除商品(购物清单属性)的方法。
这 10 个案例将共同定义系统的行为。
功能测试
行为驱动的方法需要一个测试套件来反映分配给 MVP 项目的用户案例。为了节省时间,采用与案例相同的顺序来运行功能测试。所有测试都通过后,您就大功告成了。
您将执行以下操作:
运行功能测试套件。
如果案例中的任何步骤(即,套件中的任何测试功能)失败:
编写代码以实现相应案例中的功能。
返回至第 1 步。
如果所有步骤都成功:
您就已大功告成。
运行功能测试
首先,我要解释一下如何运行功能测试。您不需要立即执行此操作,但我想先解释一下它是如何工作的及其运行情况,这样当您稍后在本单元中运行它时,就会理解它应该如何工作。
要运行功能测试,必须从 GitHub 中提取源代码。
导航至 Unit-6 目录并运行:
1 | npm test |
当输出看起来类似如下时,即表示大功告成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $ npm test > shopping-list@1.0.0 test /Users/sperry/home/development/projects/IBM-Code/Node.js/Course/Unit-6 > node ./test/functional-test 1531086599944:INFO: testItemFindById(): TEST PASSED 1531086599982:INFO: testItemFindByDescription(): TEST PASSED 1531086599985:INFO: testListsCreate(): TEST PASSED 1531086599987:INFO: testListsAddItem(): TEST PASSED 1531086599988:INFO: testListsFindByIdWithAllItems(): TEST PASSED 1531086599989:INFO: testListsUpdate(): TEST PASSED 1531086599989:INFO: testListsUpdateItem(): TEST PASSED 1531086599989:INFO: testListsRemoveItem(): TEST PASSED 1531086599990:INFO: testListsFindById(): TEST PASSED 1531086599990:INFO: testItemFindByUpc(): TEST PASSED |
如果测试失败,看起来将类似如下:
1 2 3 4 5 6 7 8 9 | $ npm test > shopping-list@1.0.0 test /private/tmp/IBM-Code/Node.js/Course/Unit-6 > node ./test/functional-test 1531087259727:ERROR: testItemFindById(): TEST FAILED. Try again. 1531087259728:ERROR: testItemFindById(): ERROR MESSAGE: Unexpected token N in JSON at position 0. . . |
在此情况下,您需要编写代码来修复失败的测试,然后再次运行功能测试。
当所有功能测试都通过后,您将完成操作。
数据
购物清单 MVP 的数据来自 Open Grocery Database Project,可以免费使用。
来自 Open Grocery Database Project 的数据采用两个 MS Excel 电子表格形式:
Grocery_Brands_Database.xlsx 包含与数据库中的品牌相关的信息。
Grocery_UPC_Database.xlsx 包含数据库中每个商品(按 UPC)的信息。
每个商品都具有唯一的 UPC。此外,每个商品仅与一个品牌相关联。
这两个 Excel 电子表格都已转换为 CSV 文件。这些文件已作为 CSV 文件放入 GitHub 存储库中。
现在,数据将被加载到 SQLite 数据库中。用于创建和访问数据库的代码是在您加入项目之前编写的,因此您只需要了解一下,以防遇到问题。
数据模型
数据模型包含以下表:
item 用于存储商品数据。
brand 用于存储品牌数据。
shopping_list 用于存储购物清单数据。
shopping_list_item 用于存储有关已添加到购物清单的商品的信息。
这些表中的每一个表在 ./scripts 中的相应源文件中都有一个定义,我们稍后将在本单元的代码走查期间进行介绍。
用于访问数据库的代码位于 Unit-6/models 目录中。此代码由先前的 Node 开发者编写,但您应该研究一下,以防遇到问题:
items-dao-sqlite3.js
是用于访问数据库以支持商品相关案例的代码。lists-dao-sqlite3.js
是用于访问数据库以支持清单相关案例的代码。
为将应用程序与底层数据源隔离,已启动数据访问对象 (DAO) 层,但从未完成:
items-dao.js
是用于支持商品相关案例的隔离层。lists-dao.js
是用于支持清单相关案例的隔离层。
为了完成分配给您的案例,您需要完成隔离层。代码中的注释将为您提供指导。
应用程序框架
您应该了解应用程序架构中的一些约束:
应用程序不得修改
item
或brand
数据。您可能只单纯使用 Node.js,这意味着只有 Node.js API,没有任何其他来自 npm 注册表的包。但 node-sqlite3 模块是一个例外,在您运行
npm install
时,将安装该模块。您必须使用 RESTful 服务实现后端。
每个用户案例都有一个 RESTful 服务,而每个 RESTful 服务都有一个 DAO 函数。总结如下:
用户案例 | HTTP 方法 | RESTful 路径 | DAO 函数 |
---|---|---|---|
商品:按 ID 查找 | GET | /items?id=123 | itemsDao.findById() |
商品:按部分描述搜索 | GET | /items?description=free range | itemsDao.findByDescription() |
商品:按 UPC 查找 | GET | /items?upc=123456789012 | itemsDao.findByUpc() |
清单:创建购物清单 | POST | /lists | listsDao.create() |
清单:按 ID 查找购物清单,仅返回购物清单 | GET | /lists/123 | listsDao.findById() |
清单:向购物清单添加商品 | POST | /lists/123/items | listsDao.addItem() |
清单:按 ID 查找购物清单,返回清单中的所有商品 | GET | /lists/123 | listsDao.findById() |
清单:更新购物清单 | PUT | /lists/123 | listsDao.update() |
清单:更新购物清单中的商品 | PUT | /lists/123/items/567 | listsDao.updateItem() |
清单:从购物清单中删除商品 | DELETE | /lists/123/items/567 | listsDao.removeItem() |
每个 RESTful 路径由两个类之一来处理:
商品:由
items-handler.js
处理清单:由
lists-handler.js
处理
代码走查
项目源目录中包含以下文件:
图 1. 项目源目录中的文件
您已经在前面的部分中看到了其中的一些内容,我们将在下面进行介绍。您需要研究其中的每个文件,以便可以编写代码来完成您的案例。
config
与配置相关的源代码存在于 config 目录中。
app-settings.js
包含名为appSettings
的对象中的应用程序设置,该对象集中了配置。
controllers
控制器逻辑(将应用程序逻辑和 Node 粘合在一起)存在于 controller 目录中。
items-handler.js
代表路由器 (routes.js
) 为所有商品路由调用 DAO 层。lists-handler.js
代表路由器 (routes.js
) 为所有清单路由调用 DAO 层。routes.js
代表 HTTP REST 服务器 (server.js
) 调用路由处理程序。
您需要为 items-handler.js
和 lists-handler.js
提供缺失的实现代码。标记 TODO
的注释将为您提供指导。
data
数据文件存在于 data 目录中。
Grocery_Brands_Database.csv 包含与品牌相关的信息。
Grocery_UPC_Database.csv 包含与商品相关的信息。
models
DAO 存在于 models 目录中。
items-dao-sqlite3.js
调用 SQLite 数据库以检索应用程序的数据。items-dao.js
是应用程序与 SQLite 数据库之间的隔离层。lists-dao-sqlite3.js
调用 SQLite 数据库以检索应用程序的数据。lists-dao.js
是应用程序与 SQLite 数据库之间的隔离层。
您需要为 items-dao.js
和 lists-dao.js
提供缺失的实现代码。标记 TODO
的注释将为您提供指导。
scripts
SQL 脚本存在于 scripts 目录中。
brand.sql
是用于创建 brand 表的 SQL。item.sql
是用于创建 item 表的 SQL。shopping_list.sql
是用于创建 shopping_list 表的 SQL。shopping_list_item.sql
是用于创建 shopping_list_item 表的 SQL。
test
与测试相关的源文件和其他工件存在于 test 目录中。
functional-test.js
是由测试负责人创建的功能测试套件,您应该运行该套件来验证您的代码。REST-Project-Unit6-soapui-project.xml
是用于测试项目的 SoapUI 项目(可选,为方便起见而包含此项)。unit-test.js
包含项目中代码的所有单元测试。
utils
实用程序存在于 utils 目录中。实用程序是提供实用程序功能的模块。
load-db.js
是用于在数据库中加载 Open Grocery Database Project 数据的模块。logger.js
是用于在 console.log 上放置更好的接口以及日志级别等内容的模块。utils.js
包含的实用程序对于自己的模块来说太小了,但在任何特定模块(例如 URL 解析)之外都很有用。
根目录
根目录中有三个文件:
package-lock.json
- 现在不要担心这个文件,我们将在第 8 单元中详细介绍。package.json
是应用程序的项目文件。server.js
是应用程序的 HTTP 服务器前端。
现在您已经进行了代码走查,您自己应该更深入地研究该代码,了解它是如何组合在一起的。
在下一节中,您将行动起来,编写代码以通过所有功能测试。
准备、设置和执行
既然已彻底研究了代码,是时候编写代码来通过所有功能测试了。但是,首先需要做一些事情。我们将一起完成以下步骤。
第 1 步. 设置您的环境
首先,确保您使用的是正确版本的 Node 和 npm,应该分别为 10 和 6:
1 | node -v && npm -v |
您应该会看到类似如下的输出(您看到的输出可能不完全像这样,但主要版本应该是匹配的):
1 2 3 | $ node -v && npm -v v10.6.0 6.1.0 |
导航到 Unit-6 目录并运行以下命令来设置环境:
1 | npm install |
这将在根目录中创建 node_modules 目录。它包含 sqlite3
模块及其所有依赖项。这与所需的"单纯的" Node.js 方法有所偏差。
既然已安装了 sqlite3
模块,您就已准备好设置本地数据库。
第 2 步. 加载本地 SQLite 数据库
要设置本地数据库以进行测试,需要将 Open Grocery Database Project 数据加载到数据库中。为此编写了一个 Node 模块 (load-db.js
)。
要运行数据库加载模块,可运行 npm run load-db
,您将看到类似如下的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | $ npm run load-db > shopping-list@1.0.0 load-db /Users/sperry/home/development/projects/IBM-Code/Node.js/Course/Unit-6 > node ./utils/load-db 1531086312416:INFO: mainline(): Script start at: 7/8/2018, 4:45:12 PM 1531086312419:INFO: createDbFixtures(): Dropping all tables... 1531086312422:INFO: createDbFixtures(): Dropping all tables, done. 1531086312424:INFO: createDbFixtures(): Creating item table... 1531086312424:INFO: createDbFixtures(): Creating item table, done. 1531086312424:INFO: createDbFixtures(): Creating brand table... 1531086312424:INFO: createDbFixtures(): Creating brand table, done. 1531086312425:INFO: createDbFixtures(): Creating shopping_list table... 1531086312425:INFO: createDbFixtures(): Creating shopping_list table, done. 1531086312425:INFO: createDbFixtures(): Creating shopping_list_item table... 1531086312425:INFO: createDbFixtures(): Creating shopping_list_item table, done. 1531086312425:INFO: createDbFixtures(): DONE 1531086312425:INFO: mainline:createDbFixtures(resolved Promise): Loading data for brand... 1531086312426:INFO: loadData(): Loading data files... 1531086312427:INFO: loadData():readableStream.on(open): Opened file: ./data/Grocery_Brands_Database.csv 1531086320293:INFO: loadData():readableStream.on(close): Closed file: ./data/Grocery_Brands_Database.csv 1531086320293:INFO: mainline:createDbFixtures(resolved Promise): Loading brand data, done. 1531086320293:INFO: mainline:createDbFixtures(resolved Promise): Loading data for item... 1531086320293:INFO: loadData(): Loading data files... 1531086320293:INFO: loadData():readableStream.on(open): Opened file: ./data/Grocery_UPC_Database.csv 1531086433275:INFO: loadData():readableStream.on(close): Closed file: ./data/Grocery_UPC_Database.csv 1531086433275:INFO: mainline:createDbFixtures(resolved Promise): Loading item data, done. 1531086433275:INFO: mainline:createDbFixtures(resolvedPromise): Script finished at: 7/8/2018, 4:47:13 PM |
现在,数据已加载到本地 SQLite 数据库中,您已准备好开始编码和测试!