快速的利用 Express 框架实现一个 Rustfull 接口的后端 Server
附:基于 Nodejs 的服务器后端的框架用的比较多的是 Koa : github/kaojs/koa 但本文这里介绍的基于 Express 框架。
附录: 前端开发,和一般开发 (非官方说法) 有三个阶段:
- 基于代码 Code 的 log 打印阶段调试开发
- 基于 IDE,如(Vscode) 的代码调试,断点阶段。
- 基于测试用例 (TDD) 驱动开发的测试驱动开发阶段,针对前端的或 nodejs 的项目,推荐使用Facebook 开源的 Jest 包做单元测试和测试驱动。
REST 介绍
-
REST是Representational State Transfer的缩写。它是Web标准架构和HTTP协议。REST架构风格描述了Roy Fielding在博士论文中最初表达的六个限制,并将RESTful风格的基础定义为:
- 统一的界面
- 无状态
- 可缓存
- 客户端服务器
- 分层系统
- 按需编码
RESTful 的应用程序使用HTTP执行四种操作CRUD(C:create,R:read,U:update,D:delete)。Create和Update用来提交数据,get来读取数据,delete用来移除数据。RESTful由基本URL,URL,媒体类型等组成。
使用的工具
- Node.js
- MySqL
- Redis
- Ticker (定时器)
- Postman (测试接口)
构建工程
注:如果你的需求是基于 express 建立一个 Web 项目,建议可以使用官方的推荐框架生成器,通过应用生成器工具 express-generator 可以快速创建一个应用的骨架。具体可以参见:http://www.expressjs.com.cn/starter/generator.html
- 进入你的工程目录,打开你的终端并且进行以下步骤:
首先做下面的准备工作。
- npx license mit 通过license包下载对应的协议npx gitignore node使用gitignore包自动的从Github仓库中下载相关文件
- npx covgen使用covgen包生成一份贡献者契约,这会让你的项目更受贡献者的欢迎。
- 创建一个 pcxProject 文件夹
mkdir pcxProject
- 转入你刚刚创建的目录
cd pcxProject
- 创建package.json文件
npm init
-
Package.json文件告诉了npm必要的信息,让npm可以识别项目,同时处理项目的依赖。
-
npm初始化将会提示你输入一些必要信息,比如说app名称、描述、版本、作者、关键字以及你是否相应看见你喜欢的。
你将会看到类似于下面这样的结果:
- 输入yes,按下enter键来完成package.json文件的创建。
- 创建server.js文件
touch server.js
- 在这个server中,我们将会写下创建我们server的代码。创建一个api文件夹mkdir api
- 在这个api文件夹中,创建三个独立的models、routes、以及controllers文件夹
mkdir api/controllers api/models api/routes
- 在api/controller文件夹中创建 Controller.js文件,在routes文件中创建 Routes.js 文件,在models文件夹中创建 Model.js 文件
- 我们的文件目录应该像下面这样:
- 其中,config 文件下面主要是程序启动之前的初始化操作。如定义的初始化数据在 Express Server 启动之前的初始化操作,或者一些数据库连接的初始化。
Express Server 安装及开发
- 现在来安装express和nodmon,express用来创建server,nodmon帮助我们追踪我们应用程序的改变,它会监视文件的改变并且自动重启server。
npm install --save-dev nodemon
npm install express --save
- 一旦成功安装之后,你的package.json文件将会被修改,添加两个新的依赖:
- 打开 package.json 文件,并且把这个任务添加到文件中 。
"start" : "nodemon server.js"
- 打开 server.js 文件,启动 Express 可以采用如下的代码:
var express = require('express'),
app = express(),
port = process.env.PORT || 3000;
app.listen(port);
console.log('todo list RESTful API server started on: ' + port);
- 在你的终端中运行 npm run start,启动你的server,你将会看到如下 Log.
todo list RESTful API server started on: 3000
安装数据库
mongoDB
- 首先,我们要安装mongoose
yarn add mongoose
- Mongoose 是我们用来与 MongoDB 数据库交互的工具。安装之后,打开 Model.js 文件,输入以下代码并且保存:
'use strict';
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var TaskSchema = new Schema({
name: {
type: String,
Required: 'Kindly enter the name of the task'
},
Created_date: {
type: Date,
default: Date.now
},
status: {
type: [{
type: String,
enum: ['pending', 'ongoing', 'completed']
}],
default: ['pending']
}
});
module.exports = mongoose.model('Tasks', TaskSchema);
-
通过上面的代码,我们在我们的文件导入了mongoose,我们创建了一个模型,说明我们的数据集应该呈现的样子。
-
正如你所看到的,它指出了数据集(表)应该包含一个名称,它的类型应该是string,还包括了创建日期。它也包含了任务状态,每个任务被创建的时候都有默认值pending。
MySqL
- 这里使用 promise-mysql 可以避免写回调函数。
yarn add promise-mysql
- 在程序服务启动之前,在 config 文件加下面添加一个数据库初始化的操作,touch promisePool.js 数据库连接池。
const mysql= require("promise-mysql");
// For Mysql
const pool = mysql.createPool({
host: config.DBConfig.DBAddr,
port: config.DBConfig.Port,
user: config.DBConfig.UserName,
password: config.DBConfig.Password,
database: config.DBConfig.DbName,
connectionLimit: 10,
});
async function getPool() {
return pool;
}
async function getConnection() {
return (await pool).getConnection();
}
-
在使用的时候,直接可从连接池获取一个连接。
-
在 config 文件夹的另外一个文件 initServer.js 如其名字一样,提供了初始化的变量的一些工作和导出函数。
-
之外,在 model.js 中封装了要使用的函数,比如:导出 getAllAddr( )
// For Database
async function getAllAddr(tableName,coinType){
let connection = await getConnection();
try {
let sqlStr = `select * from ${tableName} where cointype='${coinType}';`
// var sqlStr = sqlString.format('SELECT * FROM walletaddress WHERE cointype= ?',[coinType])
console.log(sqlStr)
const result = await connection.query(sqlStr)
// console.log(result)
return result;
} finally {
connection.release();
}
}
- 其他,MySql 获取数据的的接口,如上即可。
Redis
-
Redis 这里采用 async-redis 异步的连接 redis 的库。
-
在 config/promisePool.js 中加入 Redis 的初始化函数。
const redis = require("async-redis");
// For redis
const ccIp = config.CCConfig.RedisAddr.split(':',2)[0]
const ccPort = config.CCConfig.RedisAddr.split(':',2)[1]
const client = redis.createClient(ccPort,ccIp,{auth_pass: config.CCConfig.Password})
async function ccInit() {
await client.on("error", function (err) {
console.log("Error " + err);
});
}
var redisDao = function() {};
redisDao.prototype.get = async function (key){
let resp = await client.get(key);
console.log('AfterGet:', resp)
return resp
}
redisDao.prototype.set = async function (key, value){
await client.set(key, value)
}
redisDao.prototype.del = async function (key){
await client.del(key, function(err, o) {
console.log("Del error: " + err);
})
}
redisDao.prototype.hgetall = async function(key) {
let resp = await client.hgetall(key);
// console.log('AfterGetAll:',resp)
return resp
}
redisDao.prototype.hmset = async function(Item, key, value) {
await client.hmset(Item,key,value)
}
redisDao.prototype.hdel = async function(Item, key) {
console.log("DelHash Item:%s,key:%s", Item, key)
await client.hdel(Item, key)
}
安装路由
-
路由决定了应用程序如何响应特定终端的客户端的请求,一个特定的URL(或path)以及特定的HTTP请求(GET、POST等等)。
-
每个路由都有不同的路由处理方法,当路由匹配时被执行。下面我们将会用不同的方法定义两个基本的路由(“tasks”以及“/tasks/taskId”)。
-
“/tasks”有POST和GET方法,“/tasks/taskId”有GET、PUT和DELETE方法。
-
正如你所看到的,我们需要controller,每个路由方法都能调用它对应的处理程序。controller 下面可以写业务相关的 Restfull api 的响应处理函数。
use strict';
module.exports = function(app) {
var todoList = require('../controllers/todoListController');
// todoList Routes
app.route('/tasks')
.get(todoList.list_all_tasks)
.post(todoList.create_a_task);
app.route('/tasks/:taskId')
.get(todoList.read_a_task)
.put(todoList.update_a_task)
.delete(todoList.delete_a_task);
};
安装控制器
-
打开todoListController.js文件,然后进行下一步:
-
在这个controller中,我们将会写出5个不同的方法:list_all_tasks, create_a_task,read_a_task,update_a_task, delete_a_task。我们将会到处每个方法。
-
每个方法会用到不同的mongoose方法,例如find、findById、findOneAndUpdate、save 以及 remove。
'use strict';
var mongoose = require('mongoose'),
Task = mongoose.model('Tasks');
exports.list_all_tasks = function(req, res) {
Task.find({}, function(err, task) {
if (err)
res.send(err);
res.json(task);
});
};
exports.create_a_task = function(req, res) {
var new_task = new Task(req.body);
new_task.save(function(err, task) {
if (err)
res.send(err);
res.json(task);
});
};
exports.read_a_task = function(req, res) {
Task.findById(req.params.taskId, function(err, task) {
if (err)
res.send(err);
res.json(task);
});
};
exports.update_a_task = function(req, res) {
Task.findOneAndUpdate(req.params.taskId, req.body, {new: true}, function(err, task) {
if (err)
res.send(err);
res.json(task);
});
};
exports.delete_a_task = function(req, res) {
Task.remove({
_id: req.params.taskId
}, function(err, task) {
if (err)
res.send(err);
res.json({ message: 'Task successfully deleted' });
});
};
schedule 定时器任务
-
这里采用 while(true) + sleep 实现定时器,此处有两个定时器,两个 while(true) 但是考虑到 nodejs 的单线程异步的特性,当遇到需要同步的函数或阻塞的操作或 IO 操作的时候,如网络请求的操作时,nodejs 便会切换到其他地方去执行。除非是,await promise 操作才会等待。
-
在scheduleTask 文件夹下面的 task.js 里面放入如下内容:
const schedule = require('./schedule')
async function startQueryBlock() {
try {
schedule.ParseBlockTicker()
schedule.HeartBeatTicker()
} catch (error) {
console.error(error);
}
}
module.exports = {
startQueryBlock
}
- 在具体的 Schedule 中完成特定的定时任务的操作。(如下)
const ParseBlockTicker = async () => {
while(true) {
await sleep(2000) // Todo ****
// console.log('scheduleCronstyle:' + new Date());
const block = await Server.ServerData.api.rpc.chain.getBlock();
const lastHeight = parseInt(block.block.header.blockNumber.words[0])
if(lastHeight <= Server.ServerData.currentHeight) {
continue
} else {
while(Server.ServerData.currentHeight < lastHeight) {
Server.ServerData.currentHeight ++ // Todo
if(Server.ServerData.currentHeight % 100 == 0) {
console.log("[PCX] update node block info: node height is %d,mgr height is %d",lastHeight, Server.ServerData.currentHeight)
}
while(true) {
err = await syncTo()
if( err != null) {
// if err happen, sleep 5 second retry
await sleep(5000);
console.log("[PCX] fail to parse block and will try again. height : %d, err : %s", Server.ServerData.currentHeight, err);
continue
}
break
}
await PromisePool.redisDao.set(Server.ServerData.PCX + 'HEIGHT', Server.ServerData.currentHeight)
}
}
}
}
const HeartBeatTicker = async () => {
while(true) {
await sleep(30000)
// console.log('heartBeatTicker:' + new Date());
let nodeStatus = new Server.RespParam.NodeStatusReq()
nodeStatus.msgid = Server.RespParam.GetMsgId()
nodeStatus.coinType = Server.ServerData.PCX
const block = await Server.ServerData.api.rpc.chain.getBlock();
if(!block.block) {
nodeStatus.message = Server.ServerData.PCX + Server.ServerData.NodeOfflineMessage
console.log("[PCX] TRON node error, Report it!")
let err = await reportSyncNodeStatus(nodeStatus)
if(err != null) {
console.log("reportNodeStatus failed")
} else {
console.log("reportNodeStatus successful.")
}
} else {
nodeStatus.message = Server.ServerData.PCX + Server.ServerData.NodeAliveMessage
}
}
}
整合
-
安装 bodyParser, 使用 bodyParser 在处理之前,先在中间层解析请求中的body,在req.body属性。它暴露了各种工厂来创建中间件。如果没有body来解析或者空对象({}),中间件将会使用解析的req.body属性来填充。
-
关于主程序 Server.js 中可根据也业务调整如下:
var ServerInit = require("./config/initServer")
var express = require('express');
bodyParser = require('body-parser');
const parseBlock = require('./parseBlock/parseBlock')
function startExpress() {
app = express(),
port = process.env.PORT || ServerInit.ServerData.HttpRestPort;
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
var routes = require('./api/routes/routes');
routes(app);
app.use(function(req, res) {
res.status(404).send({url: req.originalUrl + ' not found'})
});
app.listen(port);
console.log("Server has starting")
}
async function main() {
await ServerInit.InitServer();
startExpress();
// 启动定时任务
parseBlock.startScheduleTask();
}
main();
通过Postman测试
用 Postman 测试,略。