Cap1 前言
续上篇 Node.js 后端开发笔记 文章时隔几个月没有折腾,趁现在有时间继续完善开发过程内容。开头先把上篇重点内容过一下,毕竟几个月内有些技术发生一些改变。
项目原型是 Node.js
作为后端支持服务器,hapi
作为后端Web服务
开发,并且有数据库支持和一些接口约定。在开发项目中要善于使用调试技能,找出项目的问题所在并且解决完善它。
Cap2 初始化项目
npm init # 初始化项目
npm install @hapi/hapi # 安装hapi
新建服务器(app.js):
'use strict';
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({
port: 3000,
host: 'localhost'
});
server.route([{
method: 'GET',
path: '/',
handler: (request, h) => {
return 'hello hapi'
}
}])
await server.start();
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();
然后在终端启动:node ./app.js
后,进入 http://localhost:3000
可以访问服务了。
Cap3 添加 es6 支持
由于之前客户端开发一直使用 es6
开发,所以后端项目也打算支持 es6
开发支持:
npm install --save-dev @babel/core @babel/node @babel/preset-env nodemon # 添加模块
在 package.json
中的 scripts
字段添加启动命令:
"scripts": {
"start": "nodemon --exec babel-node ./app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
新建.babelrc
:
{
"presets": [ "@babel/preset-env" ]
}
然后运行 npm run start
即可启动项目。
由于部分插件不支持 es6
编写,故放弃。
Cap4 优化项目结构
根据上一篇一个完整项目应该是:
├── config # 项目配置目录
| ├── index.js # 配置项目中的配置信息
├── models # 数据库 model
├── node_modules # node.js 的依赖目录
├── plugins # 插件目录
├── routes # 路由目录
│ ├── hello-world.js # 测试接口 hello-world
├── utils # 工具类相关目录
├── app.js # 项目入口文件
├── package.json # JS 项目工程依赖库
├── readme.md # 项目工程如何被使用的说明手册...
在 config/index.js
添加配置内容:
module.exports = {
port: 3000,
host: 'localhost'
}
在 routes/hello-hapi.js
添加路由模块:
module.exports = [{
method: 'GET',
path: '/',
handler: (request, h) => {
return 'hello hapi'
}
}]
重写 app.js
:
const Hapi = require('@hapi/hapi');
const config = require('./config');
const routesHelloHapi = require('./routes/hello-hapi');
const init = async () => {
const server = Hapi.server({
port: config.port,
host: config.host
});
server.route([...routesHelloHapi]);
await server.start();
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();
这样一个基础项目框架出现了,可以在需求上增长的业务需求的设计弹性。但是对于应用程序运行的环境来说,不同的环境往往有不同的配置。例如本地数据库密码和线上的数据库密码是不一致的,并且这些都是敏感数据不得上传到代码托管服务上,解决这个问题也很简单,先新建个 .gitignore
文件,内容下面再说,在新建个文件 .env.example
内容填写:
# .env.expamle
HOST = 127.0.0.1
PORT = 3000
然后复制出一份真实的 .env
文件,来供系统最终使用,填入真实可用的配置信息。并且把 .env
文件名写入 .gitignore
里面,这样不会被 git
上传到服务器上。同样可以使用这里维护规则:Node.gitignore。
可以通过 dotenv 插件读取 .env
里面的配置信息,具体配置如下:
npm install dotenv
修改 app.js
:
const Hapi = require('@hapi/hapi');
const dotenv = require('dotenv');
const config = require('./config');
const routesHelloHapi = require('./routes/hello-hapi');
dotenv.config('.env');
const init = async () => {
const server = Hapi.server({
port: process.env.PORT,
host: process.env.HOST
});
server.route([...routesHelloHapi]);
await server.start();
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();
再重启项目就可以了,这样环境参数的配置交由生产环境的运维人员处理,项目的敏感配置数据的安全性可以得到一定程度的保证。
Cap5 接口约定
接口文档:
使用 hapi-swagger 来达到接口描述文档:
npm install hapi-swagger --save
npm install @hapi/inert --save
npm install @hapi/vision --save
添加插件 plugins/hapi-swagger.js
:
const Inert = require('@hapi/inert');
const Vision = require('@hapi/vision');
const HapiSwagger = require('hapi-swagger');
const Pack = require('../package');
module.exports = [
Inert,
Vision,
{
plugin: HapiSwagger,
options: {
info: {
title: 'Test API Documentation',
version: Pack.version,
}
}
}
]
修改 app.js
,注册插件:
const Hapi = require('@hapi/hapi');
const dotenv = require('dotenv');
const config = require('./config');
// 引入路由
const routesHelloHapi = require('./routes/hello-hapi');
// 引入自定义的 hapi-swagger 插件配置
const pluginHapiSwagger = require('./plugins/hapi-swagger');
dotenv.config('.env');
const init = async () => {
const server = Hapi.server({
port: process.env.PORT,
host: process.env.HOST
});
// 注册插件
await server.register([
// 为系统使用 hapi-swagger
...pluginHapiSwagger,
]);
server.route([...routesHelloHapi]);
await server.start();
console.log('Server running on %s', server.info.uri);
};
process.on('unhandledRejection', (err) => {
console.log(err);
process.exit(1);
});
init();
然后访问 http://127.0.0.1:3000/documentation 看到相关接口文档信息。
接口校验:
Joi
是一种验证模块,与 hapi
一样出自沃尔玛实验室团队。Joi
的 API
因其丰富的功能,使得验证数据结构与数值的合规,变得格外容易。安装依赖包:
npm install @hapi/joi --save
这里实例就在下一段展示,更多内容请参考:https://github.com/hapijs/joi
Cap6 实践业务接口雏形
根据上面已存在的功能,做一个用户类的相关的接口,以及文档描述,编写路由配置(routes/users.js
):
const Joi = require('@hapi/joi');
const GROUP_NAME = 'users';
module.exports = [
{
method: 'GET',
path: `/${GROUP_NAME}`,
options: {
handler: async (request, h) => {
return 'yong';
},
description: '获取用户列表',
notes: '这是获取用户列表信息展示笔记',
tags: ['api', GROUP_NAME]
},
},
{
method: 'GET',
path: `/${GROUP_NAME}/{userId}/info`,
options: {
handler: async (request, h) => {
return '';
},
description: '获取某个用户基本信息',
notes: '注意事项笔记说明',
tags: ['api', GROUP_NAME],
validate: {
params: {
userId: Joi.number()
.required()
.description('用户id'),
}
}
},
},
]
新建事项清单路由配置(routes/todos.js
):
const Joi = require('@hapi/joi');
const GROUP_NAME = 'todos';
module.exports = [
{
method: 'GET',
path: `/${GROUP_NAME}`,
options: {
handler: async (request, h) => {
return '';
},
description: '获取事项清单列表',
notes: '这是获取事项清单列表信息展示笔记',
tags: ['api', GROUP_NAME]
},
},
{
method: 'GET',
path: `/${GROUP_NAME}/{todo}/info`,
options: {
handler: async (request, h) => {
return '';
},
description: '获取某个事项清单基本信息',
notes: '注意事项笔记说明',
tags: ['api', GROUP_NAME],
validate: {
params: {
userId: Joi.number()
.required()
.description('事项清单id'),
}
}
},
},
]
在app.js
里注册路由:
const routesUsers = require('./routes/users');
const routesTodos = require('./routes/todos');
//....
server.route([
...routesUsers,
...routesTodos
]);
//....
然后重启系统,在文档可以看到项目相关接口说明。
Cap7 数据设计
要实现前端数据展示业务功能,离不开数据库的支持。数据库的连接创建、表结构的初始化、起始数据的填充都是需要解决的基础技术问题。
设计数据表:
创建用户表:
字段 | 字段类型 | 字段说明 |
---|---|---|
id | integer | 用户 ID,自增 |
name | varchar(255) | 用户昵称 |
avatar | varchar(255) | 用户头像 |
created_at | datetime | 记录的创建时间 |
updated_at | datetime | 记录的更新时间 |
MySQL 与 Sequelize
本文数据库环境使用最新版本:
PS C:\Windows\system32> mysql.exe -V
D:\Development\MySQL\bin\mysql.exe Ver 8.0.17 for Win64 on x86_64 (MySQL Community Server - GPL)
MySQL
数据库是现在后端比较主流的数据库,Sequelize
则是 Node.js
生态中一款知名的基于 promise
数据库 ORM
插件,提供了大量常用数据库增删改查的函数式 API
,在实际开发中,大量减少书写冗长的基础数据库查询语句。
Sequelize
支持的数据库有:PostgreSQL
,MySQL
,MariaDB
,SQLite
和 MSSQL
。在使用不同的数据库时候,需要额外安装不同的对应数据库连接驱动,比如本文使用的 MySQL
,则依赖于插件 MySQL2
。
Sequelize-cli
Sequelize
插件的主要应用场景是实际应用开发过程中的代码逻辑层。与其相伴的还有一套 cli
工具,Sequelize-cli
,提供了一系列好用的终端指令来完成一些常用的琐碎任务。
npm install --save sequelize-cli
npm install --save sequelize
npm install --save mysql2
安装好相关依赖文件,进行 sequelize
初始化结构:
D:\Projects\NodeHapiDev>sequelize init
Sequelize CLI [Node: 10.16.0, CLI: 5.5.0, ORM: 5.10.2]
Created "config\config.json"
models folder at "D:\Projects\NodeHapiDev\models" already exists.
Successfully created migrations folder at "D:\Projects\NodeHapiDev\migrations".
Successfully created seeders folder at "D:\Projects\NodeHapiDev\seeders".
如果
sequelize init
执行错误尝试使用node_modules/.bin/sequelize init
config/config.json
:配置了开发、测试、生产三个默认的样板环境,里面的变量可以根据前面所说的.env
进行配置,但由于是json
文件不支持动态载入,可以改成config.js
来达到目的,sequelize-cli
也兼容脚本模式的config.js
的形式。models/index.js
:用于定义数据库表结构对应关系的模块目录,sequelize-cli
会在models
目录中自动生成一个index.js
,该模块会自动读取config/config.js
中的数据库连接配置,并且动态加载未来在models
目录中所增加的数据库表结构定义的模块,最终可以方便通过models.tableName.operations
的形式来展开一系列的数据库表操作行为。migrations/
:用于通过管理数据库表结构迁移的配置目录,初始化完成后目录中暂无内容。seeders/
:用于在数据库完成migrations
初始化后,填补一些打底数据的配置目录。初始化完成后目录中暂无内容。
sequelize db:create
根据前文配置了 config/config.js
中的数据库连接信息,分别有开发环境与生产环境两个。执行下面的命令,可以默认使用 development
下的配置,来创建项目数据库。增加例如 --env production
,则使用 config/config.js
中的 production
项配置,来完成数据库的创建。
sequelize db:create
# 通过 --env 参数,指定为生产环境创建项目数据库
# sequelize db:create --env production
migrate 数据迁移
sequelize migration:create
数据库被创建完成后,数据表的创建,初学者的时候或许会借助于诸如 navicat
之类的 GUI
工具,通过图形化界面的引导,来完成一张一张的数据库表结构定义。这样的编辑手法能达成目的,但是对于一个持续迭代,长期维护的数据库而言,表结构的调整,字段的新增,回退,缺乏一种可追溯的程序化迁移管理,则会陷入各种潜在人为操作过程中的风险。
Migration
迁移的功能的出现,正是为了解决上述人为操作所不可追溯的管理痛点。Migration
就像使用 Git / SVN
来管理源代码的更改一样,来跟踪数据库表结构的更改。 通过 migration
可以将现有的数据库表结构迁移到另一个表结构定义,反之亦然。这些表结构的转换,将保存在数据库的迁移文件定义中,它们描述了如何进入新状态以及如何还原更改以恢复旧状态。
下面新建个 users
表为例,使用 sequelize migration:create
来创建一个迁移文件 create-users-table
:
sequelize migration:create --name create-users-table
D:\Projects\NodeHapiDev>sequelize migration:create --name create-users-table
Sequelize CLI [Node: 10.16.0, CLI: 5.5.0, ORM: 5.10.2]
migrations folder at "D:\Projects\NodeHapiDev\migrations" already exists.
New migration was created at D:\Projects\NodeHapiDev\migrations\20190723082807-create-users-table.js .
在 migrations
的目录中,会新增出一个 xxxxxxxxx-create-users-table.js
的迁移文件,xxxxxxxxx
为迁移表文件创建的时间戳,用来备注标记表结构改变的时间顺序。自动生成的文件里,包涵有 up
与 down
两个空函数, up
用于定义表结构正向改变的细节,down
则用于定义表结构的回退逻辑。比如 up
中有 createTable
的建表行为,则 down
中配套有一个对应的 dropTable
删除表行为。
create-users-table
表定义如下:
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
/*
Add altering commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
return queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
name: {
type: Sequelize.STRING,
allowNull: false,
},
avatar: Sequelize.STRING,
created_at: Sequelize.DATE,
updated_at: Sequelize.DATE,
});
},
down: (queryInterface, Sequelize) => {
/*
Add reverting commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.dropTable('users');
*/
return queryInterface.dropTable('users');
}
};
sequelize db:migrate
sequelize db:migrate
的命令,可以最终将 migrations
目录下的迁移行为定义,按时间戳的顺序,逐个地执行迁移描述,最终完成数据库表结构的自动化创建。并且在数据库中会默认创建一个名为 SequelizeMeta
的表,用于记录在当前数据库上所运行的迁移历史版本。
sequelize db:migrate
sequelize db:migrate:undo
sequelize db:migrate:undo
则可以按照 down
方法中所定义的规则,回退一个数据库表结构迁移的状态。
sequelize db:migrate:undo
通过使用 sequelize db:migrate:undo:all
命令撤消所有迁移,可以恢复到初始状态。 还可以通过将其名称传递到 --to
选项中来恢复到特定的迁移。
sequelize db:migrate:undo:all --to xxxxxxxxx-create-shops-table.js
向表中追加字段
并非所有的迁移场景都是创建新表,随着业务的不断深入展开,表结构的字段新增,也是常见的需求。例如用户表添加个性别的字段,就得创建一个名叫 add-columns-to-users-table
的迁移迁移文件:
sequelize migration:create --name add-columns-to-users-table
文件内容如下:
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn(
'users',
'gender',
{
type: Sequelize.BOOL
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn('users', 'gender');
}
};
然后再执行 sequelize db:migrate
命令进行对数据库补丁。
seeders 种子数据填充
数据库表结构初始化完,想要向表中初始化一些基础数据可以使用 seeders
来完成,使用方式与数据库表结构迁移相似。
sequelize seed:create
以 users
表为例,为表中添加基础数据:
sequelize seed:create --name init-users
这个命令将会在 seeders
文件夹中创建一个种子文件。文件名看起来像是 xxxxxxxxx-init-users.js
,它遵循相同的 up/down
语义,如迁移文件。编写文件如下:
'use strict';
const timestamps = {
created_at: new Date(),
updated_at: new Date(),
};
module.exports = {
up: (queryInterface, Sequelize) => {
/*
Add altering commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.bulkInsert('People', [{
name: 'John Doe',
isBetaMember: false
}], {});
*/
return queryInterface.bulkInsert('users', [
{
name: '小王',
avatar: 'https://cdn.iiong.com/201810/myheader.jpg',
...timestamps
},
{
name: '小王',
avatar: 'https://cdn.iiong.com/201810/myheader.jpg',
...timestamps
},
{
name: '小王',
avatar: 'https://cdn.iiong.com/201810/myheader.jpg',
...timestamps
},
{
name: '小王',
avatar: 'https://cdn.iiong.com/201810/myheader.jpg',
...timestamps
},
], {});
},
down: (queryInterface, Sequelize) => {
/*
Add reverting commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.bulkDelete('People', null, {});
*/
return queryInterface.bulkDelete('users', null, {});
}
};
sequelize db:seed:all
与 db:migrate
相似,执行 sequelize db:seed:all
,将向数据库填充 seeders 目录中所有 up
方法所定义的数据。
sequelize db:seed:all
注意: seeders 的执行,不会将状态存储在 SequelizeMeta 表中。
当然也可以通过 --seed
来制定特定的 seed
配置来做填充:
sequelize db:seed --seed xxxxxxxxx-init-users.js
sequelize db:seed:undo
Seeders 所填充的数据,也与迁移的 db:migrate:undo
相仿,只是不会进入 SequelizeMeta
记录。两个可用的命令如下,很简单,不再赘述:
# 撤销所有的种子
sequelize db:seed:undo:all
# 撤销指定的种子
sequelize db:seed:undo --seed XXXXXXXXXXXXXX-demo-users.js
Cap8 总结
以上内容利用 sequelize-cli
完成了数据库的创建、迁移、填充。
已上传到 Github:Api-test