前言
续上篇文章 Node.js 后端开发笔记后续 - 1 利用 Sequelize-Cli
工具已经完成表结构设计以及数据填充,下面使用 Sequelize
插件库本身的数据模型 model
的查询能力来实现表查询。
Sequelize 连接 MySQL 数据库
Sequelize
连接数据库的核心代码主要就是通过 new Sequelize
(database
, username
, password
, options
) 来实现,其中 options
中的配置选项,除了最基础的 host
与 port
、数据库类型外,还可以设置连接池的连接参数 pool
,数据模型命名规范 underscored
等等。
之前使用 cli
工具初始化 models
目录,里面包含数据库表模型入口模块,希望遵循 MySQL 数据库表字段的下划线命名规范,所以,需要全局开启一个 underscore: true
的定义,来使系统中默认的 createdAt
与 updatedAt
能以下划线的方式,与表结构保持一致。修改下 model/index.js
文件:
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const configs = require('../config/config.js');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = {
...configs[env],
define: {
underscored: true,
}
};
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs.readdirSync(__dirname).filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
}).forEach(file => {
const model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
定义数据库业务相关的 model
在项目中我创建个事项清单表设计,根据上诉生成的文件:
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('todos', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
comment: '主键',
},
content: {
type: Sequelize.STRING,
allowNull: false,
comment: '事项清单内容描述',
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
comment: '关联 users 表用户id',
},
is_complete: {
type: Sequelize.BOOLEAN,
allowNull: false,
comment: '是否已经完成事项清单',
},
created_at: {
type: Sequelize.DATE,
comment: '事项清单创建时间',
},
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('todos');
}
};
迁移导入即可,结合业务所需在 models
目录下继续创建一系列的 model
来与数据库表结构做对应:
├── models # 数据库 model
│ ├── index.js # model 入口与连接
│ ├── users.js # 用户表
│ ├── todos.js # 事项清单表
定义用户的数据模型 users
module.exports = (sequelize, DataTypes) => sequelize.define(
'users',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
avatar: DataTypes.STRING,
},
{
tableName: 'users',
}
);
定义事项清单的数据模型 todos
module.exports = (sequelize, DataTypes) => sequelize.define(
'todos',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
content: {
type: DataTypes.STRING,
allowNull: false,
},
user_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
is_complete: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
},
{
tableName: 'todos',
}
);
实现用户列表接口
简单实现用户列表接口
const Joi = require('@hapi/joi');
const models = require("../models");
const GROUP_NAME = 'users';
module.exports = [
{
method: 'GET',
path: `/${GROUP_NAME}`,
options: {
handler: async (request, h) => {
// 通过 await 来异步查取数据
return await models.users.findAll();
},
description: '获取用户列表',
notes: '这是获取用户列表信息展示笔记',
tags: ['api', GROUP_NAME]
},
}
];
隐藏返回列表中不需要的字段
如果项目并不希望 findAll
来将数据表中的所有数据全都暴露出来,比如在查询用户列表时,用户的密码的值,便是特别敏感的数据。 项目可以在 findAll
中加入一个 attributes
的约束,可以是一个要查询的属性(字段)列表,或者是一个 key
为 include
或 exclude
对象的键,比如对于用户表,findAll({ attributes: { exclude: ['password'] } })
,就可以排除密码字段的查询露出。
下面隐藏用户的更新时间和创建时间的字段,只显示 id
name
avatar
:
const Joi = require('@hapi/joi');
const models = require("../models");
const GROUP_NAME = 'users';
module.exports = [
{
method: 'GET',
path: `/${GROUP_NAME}`,
options: {
handler: async (request, h) => {
// 通过 await 来异步查取数据
return await models.users.findAll({
attributes: [
'id', 'name', 'avatar'
]
});
},
description: '获取用户列表',
notes: '这是获取用户列表信息展示笔记',
tags: ['api', GROUP_NAME]
},
}
];
再次访问接口,发现功能已经实现了。
列表分页
当数据很多的时候,分页也是个很重要的业务,实现这个业务很简单,同样可以利用 hapi-pagination
插件来完成需求。
npm install hapi-pagination --save # 安装插件
在 plugins
目录下新增一个 hapi-pagination
的插件。options
的具体配置参数细节说明,参见 hapi-pagination。
const hapiPagination = require('hapi-pagination');
const options = {
query: {
// 按页面划分的资源数量。默认值为25,默认名称为limit
limit: {
name: 'limit',
default: 25
},
// 将返回的页面数。默认值为1,默认名称为page
page: {
name: 'page',
default: 1
},
// 是否开启分页功能,默认 true
pagination: {
name: 'pagination',
default: true,
active: true
},
// 无效返回结果
invalid: 'defaults',
},
meta: {
name: 'meta',
// ... 此处篇幅考虑省略 meta 的相关配置代码,参看章节 github 案例
},
results: {
name: 'results'
},
reply: {
paginate: 'paginate'
},
routes: {
include: [
'/users' // 用户列表支持分页特性
],
exclude: []
}
};
module.exports = {
plugin: hapiPagination,
options: options,
};
在 app.js
中注册使用 hapi-pagination
:
// 引入自定义的 hapi-pagination 插件配置
const pluginHapiPagination = require('./plugins/hapi-pagination');
// 注册插件
await server.register([
// 为系统使用 hapi-swagger
...pluginHapiSwagger,
// 为系统使用 hapi-pagination
pluginHapiPagination,
]);
为 GET /users
的接口添加分页的入参校验,同时更新 Swagger
文档的入参契约。考虑到系统中未来会有不少接口需要做分页处理,在 utils/router-helper.js
中增加一个公共的分页入参校验配置:
const Joi = require('@hapi/joi');
const paginationDefine = {
limit: Joi.number().integer().min(1).default(10).description('每页的条目数'),
page: Joi.number().integer().min(1).default(1).description('页码数'),
pagination: Joi.boolean().description('是否开启分页,默认为true'),
};
module.exports = {paginationDefine};
回到 router/users.js
,实现最后的分页配置逻辑。考虑到分页的查询功能除了拉取列表外,还要获取总条目数,Sequelize
提供了 findAndCountAll
的 API
,来为分页查询提供更高效的封装实现,返回的列表与总条数会分别存放在 rows
与 count
字段的对象中。
const models = require("../models");
const { paginationDefine } = require('../utils/router-helper');
const GROUP_NAME = 'users';
module.exports = [
{
method: 'GET',
path: `/${GROUP_NAME}`,
options: {
handler: async (request, h) => {
const { rows: results, count: totalCount } = await models.users.findAndCountAll({
attributes: [
'id', 'name', 'avatar'
],
limit: request.query.libraries,
offset: (request.query.page - 1) * request.query.limit,
});
return {results, totalCount}
},
auth: false,
description: '获取用户列表',
notes: '这是获取用户列表信息展示笔记',
tags: ['api', GROUP_NAME],
validate: {
query: {
...paginationDefine
}
}
},
},
];
通过 Swagger
文档工具 http://localhost:3000/documentation 查看用列表的接口调用返回数据,以及调用所需要的参数信息。
关联表数据查询
之前新建个 todos
表,里面包含用户 id
字段,下面尝试利用某个用户 id
去查询事项清单的列表信息,同样也需要支持分页信息展示:
const Joi = require("@hapi/joi");
const models = require("../models");
const { paginationDefine } = require('../utils/router-helper');
const GROUP_NAME = 'users';
module.exports = [
{
method: 'GET',
path: `/${GROUP_NAME}`,
options: {
handler: async (request, h) => {
const { rows: results, count: totalCount } = await models.users.findAndCountAll({
attributes: [
'id', 'name', 'avatar'
],
limit: request.query.libraries,
offset: (request.query.page - 1) * request.query.limit,
});
return {results, totalCount}
},
auth: false,
description: '获取用户列表',
notes: '这是获取用户列表信息展示笔记',
tags: ['api', GROUP_NAME],
validate: {
query: {
...paginationDefine
}
}
},
},
{
method: 'GET',
path: `/${GROUP_NAME}/{userId}/todos`,
options: {
handler: async (request, h) => {
const { rows: results, count: totalCount } = await models.todos.findAndCountAll({
// 基于 user_id 的条件查询
where: {
user_id: request.params.userId
},
attributes: [
'id', 'content', 'is_complete', 'created_at'
],
limit: request.query.libraries,
offset: (request.query.page - 1) * request.query.limit,
});
return {results, totalCount}
},
auth: false,
description: '获取某个用户事项清单列表',
notes: '这是获取某个用户事项清单列表信息展示笔记',
tags: ['api', GROUP_NAME],
validate: {
params: {
userId: Joi.number().integer().required().description('用户id'),
},
query: {
...paginationDefine
}
}
},
},
];
同样在 plugins/hapi-pagination.js
添加白名单路由:
//......
routes: {
include: [
'/users', // 用户列表支持分页特性
'/users/{user_id}/todos',
],
}
//......
更多关于 models 的操作请查看官方手册 Model 使用
身份验证实现
这里的项目使用当今主流的 JWT
验证身份方案,JWT
全称 JSON Web Token
,是为了方便在各系统之间安全地传送 JSON
对象格式的信息,而采用的一个开发标准,基于 RFC 7519
定义。服务器在接收到 JWT
之后,可以验证它的合法性,用户登录与否的身份验证便是 JWT
的使用场景之一。具体原理和设计内容请参考:JSON Web Token 入门教程
基于 JWT 的通用身份验证流程
在实际的项目应用场景中,JWT
的身份验证流程大致如下:
- 用户使用用户名密码、或第三方授权登录后,请求应用服务器;
- 服务器验证用户信息是否合法;
- 对通过验证的用户,签发一个包涵用户 ID、其他少量用户信息(比如用户角色)以及失效时间的
JWT token
; - 客户端存储
JWT token
,并在调用需要身份验证的接口服务时,带上这个JWT token
值; - 服务器验证
JWT token
的签发合法性,时效性,验证通过后,返回业务数据。
使用 jsonwebtoken 签发 JWT
jsonwebtoken 是 Node.js
生态里用于签发与校验 JWT
的流行插件,这里需要该插件来完成 JWT
字符串的生成签发。
npm i jsonwebtoken --save # 安装插件
根据文档得知 JWT
的签发语法是 jwt.sign(payload, secretOrPrivateKey, [options, callback])
。默认的签发算法基于 HS256 (HMAC SHA256)
,可以在 options
参数的 algorithm
另行修改。JWT
签发规范中的一些标准保留字段比如 exp
,nbf
,aud
,sub
,iss
等都没有默认值,可以一并在 payload 参数中按需声明使用,亦可以在第三个参数 options
中,通过 expiresIn
,notBefore
,audience
,subject
,issuer
来分别赋值,但是不允许在两处同时声明。
下面是一个最简单的默认签发,1 小时后失效。
const jwt = require('jsonwebtoken');
// 签发一条 1 小时后失效的 JWT
const token = jwt.sign(
{
foo: 'bar',
exp: Math.floor(Date.now() / 1000) + (60 * 60),
},
'your-secret'
);
实现接口 POST /users/createJWT
实际应用中的 JWT
签发会把便于识别用户的 userId
的信息,签发在 payload
中,并同时给予一个失效时间。继续完善 route/users.js
路由,增加一个 JWT
测试性质的签发接口定义 POST
/users/createJWT
:
const JWT = require('jsonwebtoken');
const env = require('../dotenv');
const GROUP_NAME = 'users';
module.exports = [
{
method: 'post',
path: `/${GROUP_NAME}/createJWT`,
options: {
handler: async (request, h) => {
const generateJWT = (jwtInfo) => {
const payload = {
userId: jwtInfo.userId,
exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60,
};
return JWT.sign(payload, env.JWT_SECRET);
};
return generateJWT({
userId: 1,
})
},
auth: false, // 约定此接口不参与 JWT 的用户验证,会结合下面的 hapi-auth-jwt 来使用
description: '用于测试的用户 JWT 签发',
notes: '这是用于测试的用户 JWT 签发信息展示笔记',
tags: ['api', 'test'],
},
},
];
jwt.sign 的第二个参数 secret 是一个重要的敏感信息,可以通过 .env 的配置 JWT_SECRET 来分离。
Secret 的秘钥签发,可以通过一些在线的 AES 加密工具来生成一串长度 32 或 64 的随机字符串。比如: http://tool.oschina.net/encrypt/ 。太长的字符串会一定程度上影响 jwt 验证的计算效率,所以找寻一个平衡点为宜。
访问 swagger-ui
测试 JWT
签发,可以得到 JWT
的测试签发结果,通过 jwt.io 来 decode JWT
中的 payload
信息,看能否拿到 userId
。
hapi-auth-jwt2 接口用户验证
通过 hapi-auth-jwt2 插件,来赋予系统中的部分接口,需要用户登录授权后才能访问的能力。
npm install hapi-auth-jwt2 --save # 安装插件
配置插件 plugins/hapi-auth-jwt2.js
:
const env = require('../dotenv');
const validate = (decoded, request, callback) => {
let error;
/*
接口 POST /users/createJWT 中的 jwt 签发规则
const payload = {
userId: jwtInfo.userId,
exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60,
};
return JWT.sign(payload, process.env.JWT_SECRET);
*/
// decoded 为 JWT payload 被解码后的数据
const { userId } = decoded;
if (!userId) {
return callback(error, false, userId);
}
const credentials = {
userId,
};
// 在路由接口的 handler 通过 request.auth.credentials 获取 jwt decoded 的值
return callback(error, true, credentials);
};
module.exports = (server) => {
server.auth.strategy('jwt', 'jwt', {
key: env.JWT_SECRET,
validate: validate,
});
server.auth.default('jwt');
};
在 app.js
中注册 hapi-auth-jwt2
插件,hapi-auth-jwt2
的注册使用方式与其他插件略有不同,是在插件完成 register
注册之后,通过获取 server
实例后才完成最终的配置,所以,在代码书写上,存在一个先后顺序问题。
const Hapi = require('@hapi/hapi');
const hapiAuthJWT2 = require('hapi-auth-jwt2');
// 引入路由
const routesHelloHapi = require('./routes/hello-hapi');
const routesUsers = require('./routes/users');
// 引入自定义的 hapi-swagger 插件配置
const pluginHapiSwagger = require('./plugins/hapi-swagger');
// 引入自定义的 hapi-pagination 插件配置
const pluginHapiPagination = require('./plugins/hapi-pagination');
// 引入自定义的 hapi-auth-jwt2 插件配置
const pluginHapiAuthJWT2 = require('./plugins/hapi-auth-jwt2');
// 载入环境配置文件
const env = require('./dotenv');
const init = async () => {
const server = Hapi.server({
port: env.SERVER_PORT,
host: env.SERVER_HOST
});
// 注册插件
await server.register([
// 为系统使用 hapi-swagger
...pluginHapiSwagger,
// 为系统使用 hapi-pagination
pluginHapiPagination,
// 为系统使用 hapi-auth-jwt2
hapiAuthJWT2,
]);
// 载入身份验证插件
pluginHapiAuthJWT2(server);
server.route([
...routesHelloHapi,
...routesUsers,
]);
await server.start();
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();
一旦在 app.js
中,引入 hapi-auth-jwt
插件后,所有的接口都默认开启 JWT
认证(接口配置 auth: 'jwt'
),需要在接口调用的过程中,在 header
中添加带有 JWT
的 authorization
的字段。此时通过 Swagger
文档访问先前的 users
任意接口,由于没有传输 JWT
,接口都会返回 401
的错误。
{
"statusCode": 401,
"error": "Unauthorized",
"message": "Missing authentication"
}
如果希望一些特定接口不通过 JWT
验证,可以在 router
中的 options
定义 auth: false
的配置,再通过 Swagger
文档试试对应配置的接口。同步更新 validate
中针对 authorization
的 header
入参校验,在 Swagger
文档中也会同步自动更新。
options: {
validate: {
headers: Joi.object({
authorization: Joi.string().required(),
}).unknown(),
}
}
迅速重构整理公共的 header 定义
编写 utils/router-helper.js
文件实现公用业务:
const Joi = require('@hapi/joi');
const paginationDefine = {
limit: Joi.number().integer().min(1).default(10).description('每页的条目数'),
page: Joi.number().integer().min(1).default(1).description('页码数'),
pagination: Joi.boolean().description('是否开启分页,默认为true'),
};
const jwtHeaderDefine = {
headers: Joi.object({
authorization: Joi.string().required(),
}).unknown(),
};
module.exports = {paginationDefine, jwtHeaderDefine};
在需要使用到 authorization
的 header
配置处只需要使用如下语法即可:
options: {
validate: {
...jwtHeaderDefine
}
}
handler 中使用 JWT 的获取 userId
在 plugins/hapi-auth-jwt2.js
会通过 callback(error, true, credentials)
的第三个参数,将 JWT
解码过所需要露出的数据字段与值追加到 request.auth
中,然后在路由 handler
的生命周期中,通过 request.auth.credentials
来获取对应的信息。使用 POST /users/createJWT
来生成一段 JWT
。 再通过 routes/hello-hapi.js
的接口 /restricted
做一个实验性验证,修改 plugins/hapi-auth-jwt2.js
内容:
const env = require('../dotenv');
const validate = async (decoded, request, h) => {
let error;
/*
接口 POST /users/createJWT 中的 jwt 签发规则
const payload = {
userId: jwtInfo.userId,
exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60,
};
return JWT.sign(payload, process.env.JWT_SECRET);
*/
// decoded 为 JWT payload 被解码后的数据
const { userId } = decoded;
if (!userId) {
return callback(error, false, userId);
}
// 验证数字是否有效
if (!userId) {
return {isValid: false};
} else {
return {isValid: true};
}
};
module.exports = (server) => {
server.auth.strategy('jwt', 'jwt', {
key: env.JWT_SECRET,
validate,
});
server.auth.default('jwt');
};
然后编写 /restricted
接口测试验证,编写 routes/hello-hapi.js
:
const {jwtHeaderDefine} = require('../utils/router-helper');
module.exports = [
{
method: 'GET',
path: '/restricted',
options: {
handler: (request, h) => {
const response = h.response({
message: '您使用有效的JWT令牌来访问限制接口!'
});
response.header("Authorization", request.headers.authorization);
console.log('输出信息:', request.auth.credentials); // 控制台输出 { userId: 1}
return 'hello hapi';
},
auth: 'jwt',
description: '用于测试的用户 JWT 签发',
notes: '这是用于测试的用户 JWT 签发信息展示笔记',
tags: ['api', 'test'],
validate: {
...jwtHeaderDefine, // 增加需要 jwt auth 认证的接口 header 校验
}
},
},
];
重启项目后使用系统 PowerShell
或者 Bash
终端进行测试:
curl -v -H "Authorization: 控制台输出的Token值" http://localhost:3000/restricted
#例如:
curl -v -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6IkFudGhvbnkgVmFsaWQgVXNlciIsImlhdCI6MTU2Mzk1ODkwNn0.X-YfqA850TVQdV6KyGypEQSzJ_6f5A_-PTzAVM5yobk" http://localhost:3000/restricted
如果返回信息:
$ curl -v -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6IkFudGhvbnkgVmFsaWQgVXNlciIsImlhdCI6MTU2Mzk1ODkwNn0.X-YfqA850TVQ
dV6KyGypEQSzJ_6f5A_-PTzAVM5yobk" http://localhost:3000/restricted
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /restricted HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.58.0
> Accept: */*
> Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6IkFudGhvbnkgVmFsaWQgVXNlciIsImlhdCI6MTU2Mzk1ODkwNn0.X-YfqA850TVQdV6KyGypEQSzJ_6f5A_-PTzAVM5yobk
>
< HTTP/1.1 200 OK
< authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6IkFudGhvbnkgVmFsaWQgVXNlciIsImlhdCI6MTU2Mzk1ODkwNn0.X-YfqA850TVQdV6KyGypEQSzJ_6f5A_-PTzAVM5yobk
< content-type: application/json; charset=utf-8
< cache-control: no-cache
< content-length: 65
< accept-ranges: bytes
< Date: Wed, 24 Jul 2019 09:02:34 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
{"message":"您使用有效的JWT令牌来访问限制接口!"}
看到成功结果说明通过了。
JWT 项目运用
如果在项目实战使用大多数都是登录这边,当用户访问登录接口的时候,后端校验用户信息正确后,返回用户基本信息,例如:用户ID、用户权限等级、用户角色等级等等签发 JWT
返回给客户端,这样客户端访问其他接口,例如访问编辑用户信息的接口不需要传参用户ID,因为在 header
头有个 Authorization
字段包含基本信息,大大减少前后端判断工作量。
理解事务的使用场景
例如现在有个创建订单系统,包含 orders
表和 order_goods
表。
下面这个表是 orders
表描述:
字段 | 字段类型 | 字段类型 |
---|---|---|
id | integer | 订单的 ID,自增 |
user_id | integer | 用户的 ID |
payment_status | enum | 付款状态 |
下面这个表是 order_goods
表描述:
字段 | 字段类型 | 字段类型 |
---|---|---|
id | integer | 订单商品的 ID,自增 |
order_id | integer | 订单的 ID |
goods_id | integer | 商品的 ID |
single_price | float | 商品的价格 |
count | integer | 商品的数量 |
从表结构的设计关系来看,创建一次订单,依赖于先创建产生一条 orders
表的记录,获得一个 order_id
, 然后在 order_goods
表中通过 order_id
插入订单中的每一条商品记录,以最终完成一次完整的订单创建行为。中途若商品记录的插入遇到了失败,则一个订单记录的创建行为便是不完整的,orders
表中却产生了一条数据不完整的垃圾数据。在这样的场景下可以尝试引入事务操作。
数据库中的事务是指单个逻辑所包含的一系列数据操作,要么全部执行,要么全部不执行。在一个事务中,可能会包含开始(start)、提交(commit)、回滚(rollback)等操作,Sequelize
通过 Transaction 类来实现事务相关功能。以满足一些对操作过程的完整性比较高的使用场景。
Sequelize
支持两种使用事务的方法:
- 托管事务
- 非托管事务
托管事务基于 Promise
结果链进行自动提交或回滚。非托管事务则交由用户自行控制提交或回滚。
使用托管事务创建订单
handler: async (request, h) => {
await models.sequelize.transaction((t) => {
return models.orders.create(
{user_id: request.auth.credentials.userId}, // 从 JWT 获取用户id
{transaction: t},
).then((order) => {
const goodsList = [];
request.payload.goodsList.forEach((item) => {
goodsList.push(models.order_goods.create({
order_id: order.dataValues.id,
goods_id: item.goods_id,
// 此处单价的数值应该从商品表中反查出写入
goods_price: 4.9,
count: item.count,
}));
});
return Promise.all(goodsList);
});
}).then(() => {
// 事务已被提交
return 'success';
}).catch(() => {
// 事务已被回滚
return 'error';
});
}
无论是托管事务还是非托管事务,只要 sequelize.transaction
中抛出异常,sequelize.transaction
中所有关于数据库的操作都将被回滚。
更多功能请查看官方手册 Transactions 。
系统监控和记录
在上述文章把后端开发核心重要点描述差不多,大多数系统过程都是这样。当系统上线的时候需要了解系统的状况情况,这时候需要一些插件辅助来得知系统运行情况,hapi
提供的用于检索和打印日志的内置方法非常少,要获得功能更丰富的日志记录体验可以使用 Good 插件,Good
是一个 hapi
插件,用于监视和报告来自主机的各种 hapi
服务器事件以及 ops
信息。它侦听 hapi
服务器实例发出的事件,并将标准化事件推送到流集合中。Good
插件目前有这四个扩展功能: good-squeeze、good-console、good-file、good-http。
npm install @hapi/good --save
npm install @hapi/good-squeeze --save
npm install @hapi/good-console --save
配置插件 plugins/hapi-good.js
:
const hapiGood = require('@hapi/good');
const options = {
ops: {
interval: 1000
},
reporters: {
myConsoleReporter: [
{
module: '@hapi/good-squeeze',
name: 'Squeeze',
args: [{ log: '*', response: '*' }]
},
{
module: '@hapi/good-console'
},
'stdout'
]
}
};
module.exports = {
plugin: hapiGood,
options: options,
};
然后在 app.js
引入注册就可以了,在控制台可以看到系统日志信息,当然在 Hapi
文档插件列表还有更多插件,这里就不再详细描述,可以找个合适的体验下。
系统稳定性测试
测试框架,是运行测试的工具。通过它可以为 JavaScript
应用添加测试,从而保证代码的质量。现行的 Javascript
常用流行测试库有 Jasmine
,Mocha
, Karma
等,虽然框架的名称不同,但背后的核心套件却大同小异。
Lab
Lab 库支持 async/await
,尽可能保持测试引擎的足够简单,并包含了现代 Node.js
测试框架程序中需要的所有特性。提供了 describe
和 it
,以及生命周期的钩子等功能。
Code
Code
库用于提供 expect
断言的相关函数库,code-expect
与 mocha-expect
用法上几乎完全一致。
npm install --save-dev @hapi/lab
npm install --save-dev @hapi/code
然后在项目 test/unit.js
编写实例:
const { expect } = require('@hapi/code');
const { it } = exports.lab = require('@hapi/lab').script();
it('returns true when 1 + 1 equals 2', () => {
expect(1 + 1).to.equal(2);
});
运行终端命令:
$ lab ./test/unit.js
1 tests complete
Test duration: 8 ms
Leaks: No issues
测试接口
'use strict';
const Lab = require('@hapi/lab');
const { expect } = require('@hapi/code');
const { afterEach, beforeEach, describe, it } = exports.lab = Lab.script();
const { init } = require('../lib/server');
describe('GET /', () => {
let server;
beforeEach(async () => {
server = await init();
});
afterEach(async () => {
await server.stop();
});
it('responds with 200', async () => {
const res = await server.inject({
method: 'get',
url: '/'
});
expect(res.statusCode).to.equal(200);
});
});
总结
热重载
上述开发流程大致是这样,继续完善开发过程所遇到的问题,现在有个问题每当编写好文件,还得需要重启服务才能看到改动效果,要实现热加载很简单,可以安装 nodemon
插件完成需求:
npm install --save-dev nodemon
修改 package.json
字段:
"scripts": {
"dev": "nodemon --inspect ./app.js",
},
更多内容请参考文档:nodemon
已上传到 Github:Api-test