我是如何构建我的后端服务器的

最近有空的时候一直在整理自己的后端代码,希望整理出一套较为通用的代码,可以运用到大多数的项目中去。现在的项目基本都是前后端分离的,后端只负责提供restful api接口,根据不同的项目,和前端会有不同的集成方式,例如app的后端,基本就是一个独立的项目,我个人的博客后端,我会考虑使用nginx做一个转发,当接收到/到请求时,转发给前端页面处理;当接受到/service请求时,转发给后端处理,这些请求基本上都是前端的ajax请求,避免了跨域的麻烦。后端的代码中我们只要用一个简单的use方法做转发可以将/service的请求转回原地址了。

我的项目目录:

1
controller // 存储控制器的文件夹
library // 存储通用代码的文件夹
- error.js // 通用错误定义
- helper.js // 系统级通用函数定义,例如jwt令牌生产和校验
- middleware.js // 中间件定义
- util.js // 常规通用函数定义,一般以lodash或系统内置的util模块为基础,md5等自己常用的方法
model // 存储mysql、mongo及redis数据模型的文件夹
- mysql // mysql
- mongo // mongodb
- redis // redis
- index.js
node_modules // node.js的模块
router // 系统路由
- v1.js // 版本1的路由
- v1_1.js // 版本1.1的路由
- index.js // 通过use方法调用各版本路由
service // controller和model之间的业务逻辑代码
test // 单元测试文件夹
.gitignore
app.js // 启动文件
config.js // 配置文件
package.json // 模块配置文件
readme.md

所有的请求都会经过路由再到达控制器,路由是所有代码内容的第一关,特别是在app项目里面,url一旦发布就难以修改(客户端也可以通过默写热修复的方法处理),url地址的设计好坏对于项目维护很重要。我在定义路由的时候,会有一个总的index.js文件,app.js只要引入这个文件即可,项目就可以访问到所有版本的api。

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use strict';
/* jshint node: true */

const Mdl = require('../library/middleware');
const Ctr = require('../controller');
const Router = require('koa-router');
const v_1 = require('./v1');
const v_1_1 = require('./v1_1');
const v_admin = require('./admin');
const router = new Router();

// restful api v1
router.use('/1', v_1.routes(), v_1.allowedMethods());
// restful api v1.1
rotuer.use('/1.1', v_1_1.routes(), v_1_1.allowedMethods());
// manage api
router.use('/admin', v_admin.routes(), v_admin.allowedMethods());

router.all('*', function*() {
this.status = 404;
});

module.exports = router;

v1.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';
/* jshint node: true */

const Ctr = require('../controller').v1;
const Router = require('koa-router');
const v1 = new Router();

// 增加用户
v1.post('/user', Ctr.User.create);
// 删除用户
v1.delete('/user/:user_id', Ctr.User.delete);
// 查询用户信息
v1.get('/user/:user_id', Ctr.User.read);
v1.get('/user', Ctr.User.read);
// 修改用户信息
v1.put('/user/:user_id', Ctr.User.update);

在我的controller service model三个文件夹下,都会有一个index.js的文件,controller会被路由引入,controller又会引入serviceservice又引入model,最终到达数据库。我比较习惯用一个单独的大写字母来标示一些特殊的集合。

1
2
3
4
const U = require('../library/util');
const H = require('../library/helper');
const S = require('../library/service');
const M = require('../library/model');

一开始用这种方法的时候,总会担心别人的批评,因为这种看不出是什么鬼的定义很容易被喷,有点过不去心里的坎,不过后来想想,规则就是人定的,为什么一定要遵守别人的规则,自己觉得好久即可。以前做PHP用think PHP框架也有类似的语法。用一个字母表示一个集合,确实是蛮实用的。

service作为服务层,承载着所有增删改查的业务逻辑处理,虽然有模型层把控数据格式,但是模型层很多时候只会返回诸如Validation error之类的单一错误提示,这样的提示很难判定是哪个地方出了异常,所以在数据进入模型层之前,我会对参数做断言判断,而断言的异常情况,会统一定义在library/error.js文件里。

登录验证的服务层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
exports.verify = function*(account, password) {
assert(H.isAccount(account), 'ACCOUNT_INVALID');
assert(U.isPassword(password), 'PASSWORD_INVALID');

let rs = yield M.Mysql.User.findOne({
where: { $or: [{ mobile: account }, { email: account }] },
include: [{
model: M.Mysql.UserProfile,
through: {
attributes: ['gender']
}
}]
});

assert(rs, 'USER_GONE');
assert(rs.password == H.generatePassword(password, rs.password_salt), 'PASSWORD_ERROR');

let access_token = yield H.signToken(U.extend(rs, { password_salt: null }, U.extend.RN));
let refresh_token = yield H.signToken({ user_id: rs._id }, refresh_token_expires);

return { access_token: access_token, refresh_token: refresh_token };
};

error.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'use strict';
/* jshint node: true */

exports.ACCOUNT_INVALID = {message: '账号无效', code: 400};
exports.PASSWORD_INVALID = {message: '密码无效', code: 400};
exports.PASSWORD_ERROR = {message: '密码错误', code: 400};
exports.USER_GONE = {message: '用户不存在', code: 410};
exports.PARAM_INVALID = {message: '参数错误', code: 400};
exports.MOBILE_EXISTS = {message: '手机号码已存在', code: 409};
exports.EMAIL_EXISTS = {message: '电子邮箱已存在', code: 409};
exports.OBJECT_ID_INVALID = {message: '无效的查询ID', code: 400};
exports.EMAIL_INVALID = {message: '电子邮箱格式错误', code: 400};
exports.MOBILE_INVALID = {message: '手机号码格式错误', code: 400};
exports.BOOK_GONE = {message: '书籍不存在', code: 400};

有了assert,于是代码里必然要出现try {} catch(e) {}的代码。每个接口都写一遍try catch会很麻烦,既然error可以统一由error.js定义,那么统一处理也是可以的

登录验证的controller

1
2
3
4
5
6
7
8
exports.verify = H.f(function*() {
let account = this.request.body.account;
let password = this.request.body.password;

let ret = yield S.Auth.verify.call(this, account, password);

this.send(200, ret);
});

正常来讲,verify这个方法只要返回一个Generator function即可,为了把try catch的代码提取出来,我包多了一层函数用来处理异常。这个函数定义在我的library/helper.js文件里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
exports.f = function(func, error) {
error = error || {};
return function*() {
try {
yield func.call(this);
} catch (e) {
let obj = error[e.message] || E[e.message] || {message: C.debug ? e.stack : '未知异常', code: 500};
if (typeof obj.message === 'string') {
obj.message = {
code: 1,
message: obj.message
};
}
this.send(obj.code, obj.message);
}
};
};

函数的名字主要是为了简短,又想不出太好的命名,就直接给了个f,函数的两个参数,第一个就是传入进来的generator function,第二个是自定义的错误释义,因为有时候我们需要返回的错误解释也许不是error.js里面定义的那个,或者说有时候同样是401的状态码,但是可能是不同情况导致的,例如有可能是令牌过期,也有可能是用户修改了密码,要求重新登录,于是你必须给出不同的释义。

模型层基本用的都是orm框架,较好的把控了数据问题,关系数据都存在mysql数据库,较少的数据存mongo,例如书籍这类树形的数据。

文章目录
,