这一章我们要讲的是 MVC 的 C,也就是 Controller。在 Web Application 里谈到 Controller,我们就不可避免地要了解 MVC 对数据处理的流程 和涉及 路由 这个概念。大家可以通过这个简略的流程图来辅助理解下面的一些概念。
http://www.thinkingincrowd.me/2016/11/27/Node-js-Wechat-Web-App-Tutorial-Business-Logic/
通过在浏览器输入上面的网址(URL),我们就能看到相应的网页内容。这背后其实是有相应的服务器,根据网址把与其相对应的内容返回给你。服务器由域名,也就是 www.thinkingincrowd.me 部分决定。而具体内容就是根据路径 2016/11/27/Node-js-Wechat-Web-App-Tutorial-Business-Logic 来定位的。
路由大致可分为两种:静态文件,和动态内容。但是它们的原理是一样的。静态文件的路由,就是那个路径直接对应了服务器背后的图片,HTML,JS 或者 CSS 等静态文件,服务器不需要额外处理,找到了就直接返回文件的内容。 Web Application 中处理动态内容的路由,需要通过调用绑定的 Controller,再经过 Model 或业务逻辑层处理,最后把结果返回 。服务器如果根据路径找不到对应的 Controller,一般会返回 404 状态,而你们就有可能见到这种类似的页面。
Controller 的功能是什么呢?
浏览网页的时候,用户可能不需要提供额外的数据给服务器,只需要网址 URL 就可以了。但是,比如当我们要查询酒店信息,下订单的时候,我们还是要提交相应的数据让服务器处理。
用户和服务器打交道,其实是通过 HTTP 协议提供的 GET, POST, PUT, PATCH, DELETE 等操作来实现的。
GET: 这种操作是从服务器获取数据。 它不应该对你要获取的数据做任何的修改 。当我们通过 GET 操作和服务器沟通的时候,数据是同 URL 网址一起传到服务器的,会受到长度的限制。
POST: 这种操作是创建或更改后台数据。POST 操作的数据是放在请求体里面,并没有数据大小的限制。
大多数情况,我们用的是这两种 HTTP 操作,其它几种在一些旧的服务器中并不支持。所以,我们暂时了解 GET 和 POST 的不同,以及要注意的地方就够了。大家有时间可以看看 这个简易教程 来了解其它 HTTP 操作,但不是必须。
很多新手,都会把大量的代码写到 Controller,使之变得越来越臃肿,庞大。究其原因,就是因为他们没法很好区分 M 和 C 的区别,并把代码组织得更加合理。我们写代码的时候,应该时刻考虑它们是应该放到 Controller,还是业务逻辑层。
最关键的判断依据就是: Controller 里面只放和 V (View) 相关的逻辑,不同的 V,通常有不同的 C 来对应和处理。业务逻辑层的代码,不应该和 View 有关系,而是应该可以给不同的 Controller 和 View 共用 。
View 是什么?就是不同的表现层,不同的前端界面。同样的一个业务逻辑,是可以提供给不同的界面,比如 Web 页面,iOS,Android,邮件或者外部系统等。当然,现在大家可能无法很好的理解这一点。先留个印象,代码写的越多,就越有体会。
什么叫要求的格式呢?谁提出来的要求?前端。
其实不同的格式要求,也是可以看作是不同 View 的其中一种情况。
现在我们来结合 evergrow 脚手架生成的代码,进一步理解上面说的基本概念和使用套路。
在业务逻辑层那一章,我们讲到 CRUD 这四个最基本的数据操作。Controller module/image/image-controller.js
其实也包含了对应的几个方法:
module.exports = { listImagePage: listImagePage, loadImagePage: loadImagePage, listImage: listImage, loadImage: loadImage, createImage: createImage, updateImage: updateImage, removeImage: removeImage }
listImagePage
和 loadImagePage
是打开图片列表页和详情页的,就像你上淘宝或者京东那种产品列表和产品详情页。这个我们回头讲 View 的时候再展开,我们先来看其它的 CRUD 方法。
首先,我们来看读取单一图片数据的方法:
functionloadImage(req, res, next){ ImageManager.load(req.params.id) .then(function(entity){ return res.json(buildApiResponse(entity)) }) .catch(next) }
这个方法是我们 MVC 的一个 Controller,但其实也是一个 Express 框架的中间件 。中间件是什么呢?我们来看下图:
我们可以把中间件想像成流水线上负责不同生产步骤的工人。每一个工人都拿着上游传递过来的半成品(Req)进行检查或处理。如果某一位置的工人处理时发现半成品有问题,可以中途打回,如果没问题,加工后再传递到下一层。能顺利通过所有的检查和处理的,就到达业务逻辑层,最后经由(Res)返回成品。
所以,上面的方法 loadImage
其实是最后一个 Middleware。数据到达这里开始交给业务逻辑层 ImageManager
处理。然后(then)通过调用 res.json()
方法返回 JSON (JavaScript Object Notation) 格式的数据。
大家如果仔细看文件 image-controller.js
里面几个不同的 Controller,会发现有几个微小的 req
用法差异:
req.query
req.params.id
req.body
它们的作用都是从 req
里面获取用户请求的数据。具体的使用方法,将在下面讲解路由的时候结合说明。
Controller 里面有两个比较特别的东西。这里只做基本的解释,如果大家暂时觉得不好理解,可以先记住它们的作用,学会在框架里使用这种套路就好。
next
:它是一个函数。调用方法 next()
其实就会把控制权交到下一个 Middleware,把请求交给它处理。 catch
:它能捕获上面处理业务逻辑时候的错误。把 next
放在这里,作用就是,当有错误发生的时候,Express 注册的 错误处理中间件 就会处理这个错误。 前面说到: MVC 中的路由,就是路径和 Controller 的对应关系 。那在这个框架里面,我们怎么定义这个对应关系呢?答案就在 module/image/image-routes.js
。
{ method: 'get', path: '/', summary: 'Gets all Images', description: '', action: ImageController.listImage, validators: { query: joi.object().keys({ sort: joi.string().valid('createdAt', 'updatedAt'), direction: joi.string().valid('desc', 'asc').default('desc'), limit: joi.number().integer().max(100).default(10), page: joi.number().integer() }).with('sort', 'direction') } }, { method: 'post', path: '/', summary: 'Creates Image', description: '', action: ImageController.createImage }, { method: 'get', path: '/:id', summary: 'Load Image profile', description: '', action: ImageController.loadImage }
上面这三个路由,分别定义的对应关系是:
GET
和 URL '/'
对应了 listImage
,也就是批量读取图片。 POST
和 URL '/'
对应了 createImage
,也就是创建图片。 GET
和 URL '/:id'
对应了 loadImage
,也就是读取单一图片。 因为在 image-routes.js
里还有这么一段: module.exports.basePath = '/image'
,所以,批量读取图片的完整 URL 路径就是 /image/
。 basePath
的作用是为了隔离不同模块。
这里有两大点要注意:
module.exports.routes
数组里面,URL 和 Controller 的对应关系,是按顺序注册到 Express 框架里面的。所以,如果一不小心,顺序安排不当,前端发起的请求很可能就被意料之外的 Controller 拦截。 到这里,其实我们可以测试一下那些能操作图片数据的服务了。没有页面测不了?作为后端开发者,肯定不能说要依赖页面才能测试自己写好的 REST API。下面我们来看看怎么使用 Postman 来做测试。
我们先启动我们的项目。在命令行,进入项目目录,输入 node index.js
。项目启动成功应该会输出类似的日志。
然后我们启动 Postman:
我们先通过它来创建一条图片记录:
大家在红色框的地方输入相应的参数,然后点击 Send 按钮,就可以看到服务器给予正确的返回了。这样就表示数据库已经成功地创建了一条记录。
Controller 里面使用的 req.body
所获取的数据,就是我们在 Body
这里输入的 JSON 数据。大家可以尝试一下,在 JSON 数据里面加多一些属性,比如 width, height 之类的,看能否保存到数据库。
另外,我们其实还可以对传过去的参数做一定的限制和检查。如果我们把路由的定义稍微改一下,你们看如果一些数据不传有什么结果。
{ method: 'post', path: '/', summary: 'Creates Image', description: '', action: ImageController.createImage, validators: { body: joi.object().keys({ url: joi.string().required(), createdUserId: joi.string().required(), sex: joi.string() }) } }
joi 还有更多有趣的用法,有兴趣的可以自己去了解一下。
通过上面的练习和尝试,我们应该已经创建出来一些测试数据了。现在我们来测试读取记录。
首先是读取某一条记录:
服务器怎么知道我要读哪一条记录?Controller 里使用的 req.params.id
其实对应的是路由里配置的 '/:id'
,它通过和真正的URL http://localhost:3000/image/585699146c7b082fba163580
进行匹配。截取出来的最后一段,就是记录的 id
。
批量读取多条记录的方式就如下图:
大家可以看到,GET 请求的另一种传递参数方式是如何拼接到 URL 上面的。在 Controller 里面通过 req.query
就能获取到这些参数。大家可以通过在 Controller 里加上 console.log(req.query)
的方法来调试并打印数据出来看看。你们看到那些特殊的参数,是 evergrow 框架在批量读取数据时,支持的一些特定的系统参数。它们的作用是指示后台如何在数据量比较多的时候,分页返回记录。
Updates Image profile
这个路由,完善好 body
的验证部分吗?可以给前面一章新加的 approve
方法,加对应的 Controller 和路由吗?