Node.js 应用开发实战 - 高级前端开发工程师 - 拉勾教育

前面几讲都是一些知识点的阐述,本讲将应用前面讲到的知识点,来实现一个简单版本的 RESTful 系统架构,并在此架构上实现一些简单的应用。

基础技术点

在学习本讲时会涉及一些技术知识点:

  • 什么是 RESTful 规范;
  • 数据库的读写处理过程;
  • 目前常用的 MVC 架构模式,以及后续本专栏所应用的一套新的、独创的架构模式——MSVC 架构模式。

RESTful

RESTful(Representational State Transfer)是一种架构的约束条件规则。在倡导前后端分离后,该架构规范的应用愈加广泛。具体知识点,你可以参考这里进行学习

MongoDB

由于本讲涉及数据库的操作,本专栏主要使用非关系型数据库——MongoDB,因此这里需要先了解下 MongoDB 的相关操作,以及安装配置方法,你可以参考官网的文档来安装,这里就不细讲。为了使用便利,我们可以直接在官网创建 MongoDB 云服务远程连接,具体请参照官网,以及 API 文档请参考这里

MVC→MSVC

我们应该都比较熟知 MVC 架构,它在前后端分离中起到了非常重要的作用,我们先来看下传统的 MVC 架构的模式,如图 1 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图1

此模式中:

  • M(Model)层处理数据库相关的操作(只有数据库操作时);
  • C(Controller)层处理业务逻辑;
  • V(View)层则是页面显示和交互(本讲不涉及)。

但是在目前服务划分较细的情况下,M 层不仅仅是数据库操作,因此这种架构模式显得有些力不从心,导致开发的数据以及业务逻辑有时候在 M 层,有时候却在 C 层。出现这类情况的核心原因是 C 与 C 之间无法进行复用,如果需要复用则需要放到 M 层,那么业务逻辑就会冗余在 M,代码会显得非常繁杂,如图 2 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图2

图 2 MVC 模式问题

为了解决以上问题,在经过一些实践后,我在研发过程中提出了一套新的架构模式,当然也有他人提到过(比如 Eggjs 框架中的模式)。这种模式也会应用在本专栏的整个架构体系中,我们暂且叫作 MSVC(Model、Service、View、Controller)。

我们先来看下 MSVC 的架构模式,如图 3 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图3

将所有数据相关的操作都集中于 M 层,而 M 层复用的业务逻辑则转到新的 S 层,C 层则负责核心业务处理,可以调用 M 和 S 层。以上是相关知识点,接下来我们进行架构的实践设计。

系统实践

我们先实现一个简单版本的 RESTful 服务,其次为了能够更清晰地了解 MVC 架构和 MSVC 架构的优缺点,我们也会分别实现两个版本的 RESTful 服务。

我们要实现的是一个获取用户发帖的列表信息 API,该 API 列表的内容包含两部分,一部分是从数据库获取的发帖内容,但是这部分只包含用户 ID,另外一部分则是需要通过 ID 批量拉取用户信息。

我们先来设计 RESTful API,由于是拉取列表内容接口,因此这里设计为一个 GET 接口,根据 RESTful 约束规则设计为:GET /v1/contents;另外还需要设计一个独立的服务用来获取用户信息,将接口设计为:GET /v1/userinfos。

为了更清晰些,我绘制了一个时序图来表示,如图 4 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图4

图 4 例子系统时序图

在图 4 中详细的过程是:

  • 用户先调用 /v1/contents API 拉取 restful server 的内容;
  • restful server 会首先去 MongoDB 中获取 contents;
  • 拿到 contents 后解析出其中的 userIds;
  • 然后再通过 /v1/userinfos API 调用 API server 的服务获取用户信息列表;
  • API server 同样需要和 MongoDB 交互查询到所需要的 userinfos;
  • 拿到 userinfos 后通过 addUserinfo 将用户信息整合到 contents 中去;
  • 最后将 contents 返回给到调用方。

在不考虑任何架构模式的情况下,我们来实现一个简单版本的 restful 服务,上面分析了需要实现 2 个 server,这里分别叫作 API serverrestful server

API server

server 包含 2 个部分:解析请求路径解析请求参数,在 Node.js 中我们可以用以下代码来解析:

  1. *
  2. * 创建 http 服务,简单返回
  3. */
  4. const server = http.createServer(async (req, res) => {
  5. const pathname = url.parse(req.url).pathname;
  6. paramStr = url.parse(req.url).query,
  7. param = querystring.parse(paramStr);
  8. if('/v1/userinfos' != pathname) {
  9. return setResInfo(res, false, 'path not found');
  10. }
  11. if(!param || !param['user_ids']) {
  12. return setResInfo(res, false, 'params error');
  13. }
  14. });

上面代码中使用 Node.js 的 url 模块来获取请求路径和 GET 字符串,拿到 GET 的字符串后还需要使用 Node.js 的 querystring 将字符串解析为参数的 JSON 对象。

参数和请求路径解析成功后,再进行路径的判断和校验,如果不满足我们当前的要求,调用 setResInfo 报错返回相应的数据给到前端。setResInfo 这个函数实现比较简单,使用 res 对象来设置返回的数据,具体你可以前往 GitHub 源码中查看。

路径和参数解析成功后,我们再根据当前参数查询 MongoDB 中的 userinfo 数据,具体代码如下:

  1. const baseMongo = require('./lib/baseMongodb')();
  2. const server = http.createServer(async (req, res) => {
  3. const userInfo = await queryData({'id' : { $in : param['user_ids'].split(',')}});
  4. return setResInfo(res, true, 'success', userInfo);
  5. });
  6. *
  7. * @description db 数据查询
  8. * @param object queryOption
  9. */
  10. async function queryData(queryOption) {
  11. const client = await baseMongo.getClient();
  12. const collection = client.db("nodejs_cloumn").collection("user");
  13. const queryArr = await collection.find(queryOption).toArray();
  14. return queryArr;
  15. }

这一代码中使用了 baseMongodb 这个自己封装的库,该库主要基于 mongo 的基础库进行了本地封装处理。在 queryData 中通过 mongo 来查询 nodejs_cloumn 库中的 user 表,并带上查询条件,查询语法你可以参考 API 文档

注意上面代码中,find 查询返回的数据需要使用 toArray 进行转化处理。拿到 MongoDB 查询结果后,再调用 setResInfo 返回查询结果给到前端。

接下来我们继续实现 restful server。

restful server

和 API server 相似,前面 2 个过程是解析请求路径和请求参数,解析成功后,根据时序图先从 MongoDB 中拉取 10 条 content 数据,代码如下:

  1. const server = http.createServer(async (req, res) => {
  2. const pathname = url.parse(req.url).pathname;
  3. paramStr = url.parse(req.url).query,
  4. param = querystring.parse(paramStr);
  5. if('/v1/contents' != pathname) {
  6. return setResInfo(res, false, 'path not found', null, '404');
  7. }
  8. let contents = await queryData({}, {limit: 10});
  9. contents = await filterUserinfo(contents);
  10. return setResInfo(res, true, 'success', contents);
  11. });

在 MongoDB 中查询到具体的 contents 后,再调用 filterUserinfo 这个函数将 contents 中的 user_id 转化为 userinfo,具体代码如图 5 所示(为了代码简洁,我使用了截图,源代码请参考 GitHub 上的):

03 | 如何构建一个简单的 RESTful 服务? - 图5

图 5 filterUserinfo 代码实现

在上面代码中的第 52 行是调用 API server 将用户的 userIds 转化为 userinfos,最后在 64 行,将获取的 userinfos 添加到 contents 中。

最后我们打开两个命令行窗口,分别进入到两个 server 下,运行如下命令启动服务。

运行成功后,我们在浏览器中打开如下地址:

你将会看到一个 JSON 的返回结构,如图 6 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图6

图 6 API server 返回信息

接下来我们访问如下地址,并且打开 chrome 的控制台的 network 状态栏。

你将会看到返回的状态码是 404,如图 7 所示,这也是 restful 的规范之一,即正确地使用 http 状态码。

03 | 如何构建一个简单的 RESTful 服务? - 图7

图 7 异常响应返回

接下来我们请求 restful server 的 API,同样使用浏览器打开如下接口地址:

你将会看到如图 8 所示的响应结果。

03 | 如何构建一个简单的 RESTful 服务? - 图8

图 8 contents 响应结果

以上就实现了一个简单 restful 服务的功能,你可以看到代码都堆积在 index.js 中,并且代码逻辑还比较简单,如果稍微复杂一些,这种架构模式根本没法进行团队合作,或者后期维护,因此就需要 MVC 和 MVCS 架构模式来优化这种场景。

接下来我们先来看看使用 MVC 来优化。

进阶实现

没有架构模式虽然也能按照需求满足接口要求,但是代码是不可维护的。而 MVC 已经被实践证明是非常好的架构模式,但是在现阶段也存在一些问题,接下来我们就逐步进行优化,让我们的架构和代码更加优秀。

MVC

既然是 M 和 C,我们就先思考下,上面的 restful server 中哪些是 M 层的逻辑,哪些是 C 层的逻辑。

03 | 如何构建一个简单的 RESTful 服务? - 图9

以上是所有的逻辑,根据表格,我们首先创建两个目录分别是 modelController

  • 在 model 中创建一个 content.js 用来处理 content model 逻辑;
  • 在 Controller 中也创建一个 content.js 用来处理 content 的 Controller 逻辑。

在源代码中有一个 index.js 文件,在没有架构模式时,基本上处理了所有的业务,但是根据当前架构模式,如表 1 所示,只适合处理 url 路径解析、路由判断及转发,因此需要简化原来的逻辑,和第一部分代码一样,我们就不再列举了,主要看路由判断。首先需要根据 restful url 路由配置一份路由转发逻辑,配置如下:

  1. const routerMapping = {
  2. '/v1/contents' : {
  3. 'Controller' : 'content',
  4. 'method' : 'list'
  5. },
  6. '/v1/test' : {
  7. 'Controller' : 'content',
  8. 'method' : 'test'
  9. }
  10. };

上面代码的意思是:

  • 如果请求路径是 /v1/contents 就转发到 content.js 这个 Controller,并且调用其 list 方法;
  • 如果是 /v1/test 则也转发到 content.js 这个 Controller,但调用的是 test 方法。

注意:其中 test 是一个同步方法,list 是一个异步方法。

路由配置完成以后,就需要根据路由配置,将请求路径、转发到处理相应功能的模块或者类、函数中去,代码如图 9 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图10

图 9 index 核心逻辑

  • 第一个红色框内的部分,判断的是路由是否在配置内,不存在则返回 404;
  • 第二个红色框内的部分,加载对应的 Controller 模块;
  • 第三个红色框内的部分,表示判断所调用的方法类型是异步还是同步,如果是异步使用 await 来获取执行结果,如果是同步则直接调用获取返回结果。

注意:这里使用 try catch 的目的是确保调用安全,避免 crash 问题。

接下来我们实现一个 Controller,为了合理性,我们先实现一个基类,然后让每个 Controller 继承这个基类:

  • 在项目根目录下我们创建一个 core 文件夹,并创建一个 Controller.js 作为基类;
  • 然后我们把一些相同的功能放入这个基类,比如 res 和 req 的赋值,以及通用返回处理,还有 url 参数解析等。

我们来看下这部分代码,如图 10 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图11

图 10 Controller 基类

功能还是比较简单的,只是提炼了一些 Controller 共同的部分。接下来我们再来实现 content.js 这个 Controller,代码如图 11 所示:

03 | 如何构建一个简单的 RESTful 服务? - 图12

图 11 content.js Controller

我们在初次实现时,可以不关注图 11 中的第 2 和 3 行,实现红色框内的代码即可。可以将 list 暂时设置为空,实现完成后,我们在根目录运行以下命令,启动服务。

接下来打开浏览器访问:

你就可以看到响应了一个 JSON 数据,这样就实现了 Controller 部分了。如下代码所示:

  1. {
  2. ret: 0,
  3. message: "good",
  4. data: { }
  5. }

接下来我们再来实现 Model 层部分,和 Controller 类似,我们也需要一个基类来处理 Model 层相似的逻辑,然后其他 Model 来继承这个基类,这部分如图 12 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图13

图 12 Model 基类

这个基类首先设置了 db 名称,其次定义了一个 GET 方法来获取表的操作句柄,这部分代码与上面简单 restful 服务的类似。完成基类后,我们再来完善 model 中的 content.js 逻辑。

03 | 如何构建一个简单的 RESTful 服务? - 图14

图 13 model content.js 代码实现

这部分代码主要方法是 getList,原理和简单 restful server 中的查询类似,在第 11 行通过父类的 GET 方法获取表 content 的操作句柄,再调用 MongoDB 的 find 方法查询 contents。有了 model content 后,我们再回去完善 content.js Controller 中的 list 函数部分逻辑,代码封装的比较简洁,如下所示:

  1. async list() {
  2. let contentList = await new ContentModel().getList();
  3. contentList = await this._filterUserinfo(contentList);
  4. return this.resAPI(true, 'success', contentList);
  5. }

上面代码中的第 4 行,只能在当前 Controller 下实现一个私有方法 _filterUserinfo 来处理用户信息部分,这部分逻辑也和简单 restful 服务的一样。

这样就实现了一个 MVC 的架构,将原来的复杂不可扩展性的代码,转化为可扩展易维护的代码,这部分核心代码可以参考 GitHub 源码

MVCS

在上面的代码中存在一个问题,就是 _filterUserinfo 是放在 Controller 来处理,这个方法又会涉及调用 API server 的逻辑,看起来也是数据处理部分,从原理上说这部分不适合放在 Controller。其次在其他 Controller 也需要 _filterUserinfo 时,这时候就比较懵逼了,比如我们现在有另外一个 Controller 叫作 recommend.js,这里面也是拉取推荐的 content,也需要这个 _filterUserinfo 方法,如图 14 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图15

图 14 MVC 复用性问题例子

其中左边是存在的矛盾,因为 _filterUserinfo 在 Controller 是私有方法,recommend Controller 调用不到,那么为了复用,我们只能将该方法封装到 content-model 中,并且将数据也集中在 Model 层去。

虽然解决了问题,但是你会发现:

  • Model 层不干净了,它现在既要负责数据处理,又要负责业务逻辑;
  • Controller 层的业务减少了,但是分层不明确了,有些业务放在 Model,有些又在 Controller 层,对于后期代码的维护或者扩展都非常困难了。

为了解决这个问题,有一个新的概念——Service 层,具体如图 15 所示。

03 | 如何构建一个简单的 RESTful 服务? - 图16

图 15 MSVC 优化效果

  • 图中的浅红色框内,就是新架构模式的 M 层;
  • 两个绿色框内为 C 层;
  • 最上面的浅蓝色框则为 Service 层。

这样就可以复用 _filterUserinfo,并解决 M 与 C 层不明确的问题。接下来我们来实践这部分代码:

  • 首先我们需要创建一个文件夹 service 来存放相应的 Service 层代码;
  • 然后创建一个 content.js 来表示 content-service 这个模块;
  • 再将原来代码中的 _filterUserinfo 逻辑转到 content-service 中去;
  • 最后修改 Controller 代码。

如下代码所示:

  1. async list() {
  2. let contentList = await new ContentModel().getList();
  3. contentList = await contentService.filterUserinfo(contentList);
  4. return this.resAPI(true, 'success', contentList);
  5. }

注意代码中的第 4 行,从原来调用本类的方法,修改为调用 contentService 的 filterUserinfo。

总结

本讲最开始介绍了一些技术知识点,这些是你开始学习本专栏必需巩固的技术,接下来根据实践开发了一个微型的 restful 服务,由于代码的不可维护性以及不可扩展性,我们接下来就应用了 MVC 架构设计模式进行了优化,最后由于 MVC 的缺陷,进而提出了使用 MSVC 来解决 MVC 中 M 和 C 业务界定不清晰的问题。

学完本讲后,你就能自己写一个 restful API 了,并且能够掌握 MVC 和 MSVC 的架构原理,同时能够开发出轻量版本的框架。在实践过程中有任何问题或者心得,都可以在留言区留言。

讲解完我们自身设计的简版框架后,在下一讲要介绍 Node.js 目前业界使用最广的三个框架,并且进行深入对比分析其优缺点。


03 | 如何构建一个简单的 RESTful 服务? - 图17

《大前端高薪训练营》

对标阿里 P7 技术需求 + 每月大厂内推,6 个月助你斩获名企高薪 Offer。点击链接,快来领取!