前言
我今年二月的时候就准备筹划一整个React
的学习系列,但是发现自己真的是不够强,理解还不够深刻,加上真的没有理想的那么多时间,所以直接简化到这几篇闲来无事系列。
本篇用Koa
、 MongoDB
和 Redis
搞一个以json
通信的服务器,用于之后所有客户端的接口服务!
系统说明
整个系统在设计下来之后,发现也不是想象的那么简单,但是也算不上复杂,大体上就是一个以发布话题为主体,围绕展开的功能。
数据结构
整个数据结构分为五个大块,分别是用户
、话题
、回复
、消息
和行为
。前三个都好说,消息
其实也很好理解,比如一个用户关注了另一个用户,被关注的人肯定是有提醒的,主要说一下行为
,为什么要叫行为呢?主要是用户的动作有多种,比如收藏、点赞等等,我不想为每一种行为都单独设定一个数据结构,它们的结构也完全可以类似,所以整体就叫做行为
了。
多说两句
大部分内容还是容易理解,我就说一下差不多写完以后才发现错了好多的地方,第一个就是数据层没有单独提出来,也就是操作数据库的部分还是封在了controller
里面,但其实这样使得整个controller
层中的代码很臃肿,后来我发现我的积分制度居然忘记加了,就开始一个文件一个文件的找每次需要增加积分的地方修改,其实如果抽离了数据层,修改起来就会简单许多。
还有一个容易错的地方,由于我把整个用户行为都封在了一个数据结构中,以type
字段区分,但是行为与行为直接还是有些许差异的,如果处理不当,就会看起来很混乱,这也是我不想看到的情况。
目录设计
目录其实完全可以按照个人喜好来规划目录,但是我还是觉得写代码嘛,越规范越好,但是我也不是说我这种目录结构就是最规范的…..只是做个参考,遵循MVC
模式,只是这里我们的服务只负责数据,不负责视图,所以就只剩MC
,模型和控制器。
1 | ├── sever |
这个树形结构基本上已经把整个目录描述出来了,写过Node
的小伙伴可能一眼就能猜出每个目录的作用了,没有写过也没关系,让我慢慢说。
来一个完整的API
每个地方的拆分来说的话,对于阅读其实没有那么友好,所以先走一个完整的流程可以帮助我们更好更快的
去理解一次请求所经历的所有地方。
开启一个服务
没得说,首先我们肯定是需要一个能够接受到客户端请求的服务器,使用Koa
能够快速搭建服务器,这里我就直接贴出更复杂完整一些的代码。
1 | const Koa = require('koa'); |
我在上述代码中暂时注释了跨域和域名拦截的代码,方便我测试。为了帮助大家更好的理解其中的流程,我把流程写了下来。
- 引入依赖
- 引入自写
mongodb
模块,连接数据库 - 实例化
Koa
为app
,同时暴露app
方便单元测试 - 引入中间件,包括请求体解析,
jwt
和自写错误处理 - 引入路由
- 处理请求路径不在路由中的
404
- 记录服务器错误
- 监听配置端口,开启服务
对于上述代码如果还不是很清楚,可以对照流程和代码一起看,我相信看起来应该没有这么复杂了吧!
设置一个路由地址
一起来思考一下,现在我们的服务已经开启了,客户端如何请求,服务器又如何知道客户端请求的是哪一个接口呢?很明显是通过路由(也就是url)来判断的,我们现在以一个登录接口来说明。
在入口文件app.js
中,我们引用了router.js
,这个文件就是路由的匹配文件,一起来看一下:
1 | const Router = require('koa-router'); |
我删掉了其他API
的路由,只留下了登录一个,这样看起来是不是很简单了,还是来理一下整个流程:
- 客户端发起一个请求,请求地址为
/signin
。 - 服务器接收到这个请求并去匹配路由。
- 找到匹配路由的处理方法交给控制器
User
的signin
方法处理。
可以看得出来最后请求会交个控制器去处理,也就是我们的逻辑代码。
控制器处理这个请求
到了这一步,也就是最后一步,这个控制器存储了很多个方法,分别处理不同的请求地址,这里我们只看其中signup
方法,我像之前那样只留下signup
方法。
1 | const jwt = require('jsonwebtoken'); |
我没有在代码中写注释是因为我觉得里面的代码并不复杂,我直接来解析一下其中的流程。
- 接上述流程来到
signin
方法。 - 方法接收一个参数
ctx
,这个参数是Koa
提供的。 - 解析
ctx
参数,拿到请求体中的参数email
和password
。 - 校验参数是否合法。
- 根据
email
判断是否存在这个用户。 - 使用密码比较方法对比传入的
password
和数据库中的password
是否抑制。 - 组装带有用户
id
和role
字段的jwt
。 - 使用
ctx
参数返回结果。
到这里客户端就能接收到服务器返回的结果了,一个完整的API
请求的整个流程也算是完成了,其他API
请求类似,读者可以自行对号观看,接下来就说说其中比较特别的地方,API
流程我也不再继续展开。
关于测试
本来一开始写这文的时候,还是没有这一节的,当时写代码也觉得写测试太麻烦了,也不是很熟,后来还是觉得做事情确实要好好做,还是把测试加上了,我这里依然以登录为例,带大家走一次完整测试的流程。
创建测试文件
一开始我本来打算以一个控制器去划分一个测试文件,后来发现一个控制器中可能有N
个API
,那代码不得超长了,所以后来我就以一个API
划分一个测试文件,以注册为例,创建__test__/user/signin.test.js
文件。
编写一个测试用例
书写测试能用的依赖有很多,由于我本身对测试不是很熟的原因,还是挑了使用最为广泛的mocha
,大家也可以自行选择。下面是一个以mocha
为例的测试:
1 | const app = require('../../app').listen(); |
我大量简化了其中的代码,留下其中一个测试用例给大家参考,其实这样看起来也并不复杂对吧…所谓的测试,就是去验证返回的结果和预期的结果是否相等,比如上述登录接口是需要验证密码是否正确,而我传的是一个错误密码,所以应该返回的信息是密码错误
。
模块说明
测试除了使用mocha
以外,还用了supertest
和should
两个模块,这两个东西都用来配合mocha
使用,它本身不提供能够直接连接服务器的东西所以用supertest
来连接我们的服务器,should
是断言库,所谓的断言就是判断得到的结果是否和预期一致。
API设计规范
标题略微狂妄…与其说是规范,不如说是个人习惯吧。我在工作过程中也算是遇到过很多不同的API
习惯,有些喜欢直接返回结果,失败客户端自行加入拦截器去拦截错误码,有些又喜欢自己返回是否错误的信息,我比较喜欢后者,所以关于这个服务的API
也是这么做的。
由于改由Koa
编写后,全部API
均遵循RESTful
风格,所以去除该部分内容,
关于中间件拦截
在客户端请求的时候,有些接口是有前置条件的,比如查看用户信息,如果没有登录,如何能够查看用户信息,所以这个时候就需要路由拦截,去校验是否有这个权限,这个服务在设计的时候有三种权限类型,分别是普通用户
、管理员
、超级管理
。路由拦截定义在middleware
里面,设置于router.js
中,比如用户现在要请求一个查看当前用户信息的接口,来一起看一下:
1 | router.get('/info', Auth.userRequired, User.getCurrentUser); // 获取当前用户信息 |
在router.js
里面的这个接口中可以发现一个Auth.userRequired
的方法,也就是在进入这个接口之前,会先执行这个方法,在到User.getCurrentUser
这个方法里面去,来看看Auth
里面的东西:
1 | class Auth { |
同上我没有贴上完整的代码,只贴了关于用户校验的部分,这个时候你会发现这个方法里面多了一个next
参数,也就是如果回传回来的参数头关于jwt
能解析并且没有过期,就会走到User.getUserInfo
的方法里面,如果没有在这个方法里面就会抛出错误。
数据模型的额外的事
这个里面没什么太多要说的,主要是看业务用到哪些表,哪些字段是弄这个东西。我单独拿出来要说的是手动生成id
,这样做需要多出一个表结构,我命名为ids
。
在初始化数据库的时候会动态的生成一条数据,这条数据也就是这个表唯一的数据,会每次将对应表的id
加一,这种做法我是参照了Github
一个比较厉害的人搞得,其实具体有什么优势我内心也是懵逼的(哈哈哈哈),靠大家自行摸索了。
后来我仔细想了一下,也问了一下写后端的同事,确实好像没有必要这么搞去手动生成一个自定义的id
,徒增工作量,所以去掉了这一部分。
这两天多看了一下mongoose
的文档,做了一点细微的调整,加入一个模型插件/models/plugin
:
1 | const moment = require('moment'); |
在数据查出转换的时候,生成一个id
并且删除了_id
和__v
,省去了每次都需要加入查询条件,而且id
更符合惯性的思维。同时给数据模型手动添加一个个方法create_at_ago
,表示从创建时间到现在过了多久。
ProtoType的存在
其实prototype
完全是可以省略的,如果你没怎么用上前面说的动态生成id
,由于我用了动态生成id
,也就意味着在每一个控制器中都需要一个同样的方法去生成id
,所以就用到了这个东东,不过在这个服务里面我加入了用Node
画图形验证码的方法。
生成Id
的方法是getId
,画图形验证码用到了gd-bmp
库,早在去年十月的时候我就开始想搞一个Node
能画图形验证码的库,也搜了好多,由于gd-bmp
真的好用加上时间也没有这么空余,就直接用的别人的,不过是真的不错!!
去掉了手动去生成id
的方法以后,我一度认为这个东东没什么用了,毕竟画验证码也只是在验证码模块中使用,并不涉及到所有的,后来我加入存用户行为和用户消息这两个结构后,又重新使用了这个东东,改为controllers/base
,把一些公共需要的方法放入其中。
在捋一次完整的流程
看完上述你会发现其实真的不难,当然还有很多东西没有去弄,也有很多需要优化的地方,但是一个简单的能作为数据服务的服务器就这么弄出来了,写代码确实是一件很有意思的事情。整个流程再来看一下:
- 创建对应的数据模型(就像
uesr
表) - 创建对应的路由拦截(就像
/signin
登录一样) - 创建处理方法(就像
controller/user
里面的signin
方法一样) - 开启服务
- 客户端去请求这个地址
- 客户端得到服务器的返回数据
一个完整的流程就这么简单,所有其他的API
也是这么做出来的,代码虽然我还在完善,不过大部分的API
都已经完成了,后期也还可能在改一改,也会记录在CHANGELOG里面。
结语
本篇作为我终极实战系列的第一篇,讲述用Node
完整搭建一个数据服务器的过程,一次请求的完整流程,也是作为后面所有不同终端的数据服务,所以本篇的意义就是讲述一个提供服务的过程。