这一节笔者拿微信公众号开发为例,带大家搭建一套简单系统,由api网关发起调用,请求到远端通用微服务。这里作者默认大家已经搭建好rabbitMQ服务,系统中已经安装sequelize-auto 以及supervisor。
api-rest(先扔一个git地址,这里是源码:https://github.com/burning0xb/api-rest.git)
项目主要分为这几个部分
1、项目入口
require('babel-core/register')
require('./server');
用babel实现es6风格的书写
2、服务入口
const app = new Koa();
// 创建基础路由
const baseRouter = new BaseRouter();
// 使用session(整合redis)
app.use(session({
key: 'burning:session',
store: new RedisStore(),
maxAge: config.maxAge
}));
// 加载中间件依次为body体格式化,日志与跨域
app.use(bodyParser());
app.use(Logger());
app.use(convert(cors()));
// 加载基础路由中登录过滤
app.use(async (ctx, next) => {
if (baseRouter.requireLogin(ctx)) {
await next();
}
});
// 加载所有路由
app
.use(router.routes())
.use(router.allowedMethods());
// 服务启动
app.listen(config.port, () => {
logger.info(`server is running port ${config.port}`);
});
3、定时获取微信access_token
const rule = new schedule.RecurrenceRule();
rule.minute = [0, 20, 40]; // 任务规则
schedule.scheduleJob(rule, () => {
getAccessToken().then((res) => {
console.log(res);
global.wechatToken = res.access_token;
})
});
这里采用node-schedule模块,实现linux式的crontab任务。
4、路由入口
// 定义路由前缀
const router = koaRouter({
prefix: '/api'
});
// 初始化rabbitMQ 客户端后再去加载所有的路由
new Client().then((res) => {
logger.info('rabbitMQ is ready');
global.MQ = res.RabbitSend;
}).then(() => {
for (let _router in routers) {
if (_router !== '') {
routers[_router](router, upload);
console.log(`${_router} 加载成功 `);
}
}
const userHandler = new UserHandler(global.MQ);
rollUserList(userHandler);
});
// 这里去加载一个新的定时任务(每晚去更新关注用户的数据)
async function rollUserList(userHandler) {
schedule.scheduleJob('0 0 1 * * *', async () => {
const userList = await wechatApi.getUserList();
if (userList.data) {
console.log(`关注用户总数 ${userList.total} 人 开始更新用户信息`);
userList.data.openid.map(async (openid) => {
await util.sleep(1);
const userInfo = await wechatApi.getUserInfo(openid);
// 这里是重点,调用远端的服务去持久化用户信息
userHandler.saveWechatUser(userInfo);
})
}
});
}
5、路由文件(举一个例子)
import { UserHandler } from '../controller';
function userRouter(router, upload) {
// 给controller绑定MQ对象
const userHandler = new UserHandler(global.MQ);
// get请求
router.get('/user/getUserList', async (ctx, next) => {
const user = await userHandler.getUserList(ctx);
// respnose返回
ctx.body = user;
})
}
export default userRouter;
6、控制器(举一个例子)
import BaseHandler from './BaseHandler';
export default class UserHandler extends BaseHandler {
constructor(server) {
super();
// 这里初始化MQ对象
this.server = server;
}
/**
* [getUserList description]
* @param {[type]} ctx [description]
* @return {Promise} [description]
*/
async getUserList(ctx) {
// 获取解析过的请求体
const body = ctx.request.body;
// 构造远端服务请求体
const content = {
class: 'user', // 远端服务的类名
func: 'getUserList', // 远端服务的方法
content: {} // 消息内容
};
// 创建新的远端服务发送对象
const server = this.initServer(this.server);
// 发送并取得返回结果
const res = await server.send(content);
return res;
}
}
BaseHandler中有构造新的发送对象的方法
initServer(server) {
return server(global.ch, global.ok);
}
7、rabbitMQ
这里重点说一下消息队列的客户端
import amqp from 'amqplib/callback_api';
import RabbitSend from './RabbitSend';
import config from '../config.json';
export default class Client {
constructor() {
// 创建mq连接,on_connect为回调函数
return new Promise((resolve, reject) => {
amqp.connect('amqp://' + config.rabbitMq_user + ':' + config.rabbitMq_password + '@' + config.rabbitMq_host + ':' + config.rabbitMq_port, this.on_connect.bind(this, resolve));
}).catch((err) => {
console.log(err);
});
}
// 最后会返回一个new 对象,也就是说每init一次就会new一次
init(ch, ok) {
const server = new RabbitSend(ch, ok)
return server;
}
// 失败处理函数
bail(err) {
console.error(err);
}
init_client(resolve, RabbitSend) {
resolve({
RabbitSend: RabbitSend
});
}
on_connect(resolve, err, conn) {
if (err !== null) return this.bail(err);
// 创建信道
conn.createChannel((err, ch) => {
if (err !== null) return this.bail(err);
// 通道创建成功后我们通过通道对象的assertQueue方法来监听空队列,并设置durable持久化为true。
ch.assertQueue('', { exclusive: true }, (err, ok) => {
if (err !== null) return this.bail(err);
global.ch = ch;
global.ok = ok;
this.init_client(resolve, (ch, ok) => { return this.init(ch, ok); });
});
});
}
}
import config from '../config.json';
import uuid from 'node-uuid';
// 这里就是将要去发送消息的对象
export default class RabbitSend {
constructor(ch, ok) {
this.ch = ch;
this.ok = ok;
this.ramdom = Date.now();
}
mabeAnswer(msg) {
// 如果返回的消息ID再发送的消息队列中,就去处理
if (global.msgQueue.includes(msg.properties.correlationId)) {
console.log(msg.content.toString());
const index = global.msgQueue.indexOf(msg.properties.correlationId);
global.msgQueue.splice(index, 1);
// resove返回的消息
global.resolveRabbit[msg.properties.correlationId].resolve({
finalRes: JSON.parse(msg.content.toString())
});
// 从待处理队列中删除
delete global.resolveRabbit[msg.properties.correlationId];
} else {
// 如果指定消息的promise对象还存在那么就移除否则直接输出没有对应的MQ
if (global.resolveRabbit[msg.properties.correlationId]) {
global.resolveRabbit[msg.properties.correlationId].reject({
err: 'Unexpected message'
});
delete global.resolveRabbit[msg.properties.correlationId];
} else {
console.log('未找到对应的MQ');
}
}
}
// 当控制器去掉用send的时候会触发到这里
send(content, type) {
console.log(' [x] Requesting is ', content);
let queue = config.MQ_QUEUE_COMMON;
// let queue = config.MQ_QUEUE_COMMON_TEST;
// 根据type去区分要调用的queue,默认为config.MQ_QUEUE_COMMON
switch (type) {
case 'log':
queue = config.MQ_QUEUE_LOG;
break;
case 'pay':
queue = config.MQ_QUEUE_PAY;
break;
default:
queue = config.MQ_QUEUE_COMMON;
// queue = config.MQ_QUEUE_COMMON_TEST;
break;
}
// 返回一个带结果的promise对象
return new Promise((resolve, reject) => {
// 这里去声明消息的ID
const correlationId = uuid();
// 将此ID压入消息队列中
global.msgQueue.push(correlationId);
// 标识当前的promise对象
global.resolveRabbit[correlationId] = {
resolve: resolve,
reject: reject
};
// 创建消费者监听指定queue,noAck: true不做应答
this.ch.consume(this.ok.queue, (msg) => {
// 返回的结果处理函数
this.mabeAnswer(msg);
}, { noAck: true });
// 发送到指定queue,指明应答的queue以及消息ID
this.ch.sendToQueue(queue, new Buffer(JSON.stringify(content)), {
replyTo: this.ok.queue,
correlationId: correlationId
});
}).catch((err) => {
console.log(err);
});
}
}
这里的global可以用内存模块去管理,笔者就简单的存在原始内存对象中,还有一点说明,启动项目前要修改一下rabbitmq配置,在config.json中:
{
"port": 8888,
"rabbitMq_host": "主机IP",
"rabbitMq_port": "端口",
"rabbitMq_user": "用户名",
"rabbitMq_password": "密码",
"MQ_QUEUE_COMMON": "js_server",
"MQ_QUEUE_COMMON_TEST": "server_test",
"MQ_QUEUE_LOG": "log",
"MQ_QUEUE_PAY": "pay",
"MQ_QUEUE_PAY_TEST": "pay_test",
"maxAge": 1800000,
"redis_maxAge": 1800
}
其余的代码笔者相信有一定js基础的同学都能看懂,这里就不做赘述。下面继续搭建远端服务,也就是我们的消费者。
common-service (git地址:https://github.com/burning0xb/common-service.git )
这里的common-service就是我们所说的微服务,整合了sequelize orm框架,以下是对该服务的解读。
1、入口文件
import amqp from 'amqplib/callback_api';
import { logger, logger_date } from './src/log4j';
import config from './config';
import route from './route';
logger.info('server started');
function bail(err, conn) {
logger.error(err);
}
function on_connect(err, conn) {
if (err !== null)
return bail(err);
process.once('SIGINT', () => {
conn.close();
});
var q = config.rabbitMq_queue.logic01
/*
测试mq
*/
// var q = config.rabbitMq_queue.logic02
// 创建信道
conn.createChannel((err, ch) => {
logger_date.info('rabbitMQ createChannel');
// 监听指定的queue
ch.assertQueue(q, {durable: true});
// 设置公平调度,这里是指mq不会向一个繁忙的队列推送超过1条消息。
ch.prefetch(1);
// 创建消费者监听Q,reply为接收处理函数, noAck: false做出应答
ch.consume(q, reply, {
noAck: false
}, (err) => {
if (err !== null)
return bail(err, conn);
logger.info(' [x] Awaiting RPC requests');
});
function reply(msg) {
logger.info('request content is ' + msg.content.toString());
// 接收的消息内容
const request = JSON.parse(msg.content.toString());
// 这里定义返回函数
const cb = (response) => {
ch.sendToQueue(msg.properties.replyTo, new Buffer(JSON.stringify(response)), { correlationId: msg.properties.correlationId });
ch.ack(msg);
};
try {
// 查找api网关发送的消息中指定的方法
const func = request.class && request.func ? route[request.class][request.func] : null;
if (func) {
// 调用指定方法
func(cb, request.content);
} else {
cb({
err: 'method not allowed'
});
}
} catch(err) {
console.log(err);
cb({
code: 500,
err: 'server error'
});
}
}
});
}
// 创建连接
amqp.connect('amqp://' + config.rabbitMq_user + ':' + config.rabbitMq_password + '@' + config.rabbitMq_host + ':' + config.rabbitMq_port, on_connect);
logger_date.info('rabbitMQ connect success');
logger.warn('don`t kill this process');
2、路由
import { User } from './src/server/user';
const user = new User();
const route = {
user
};
export default route;
3、远端服务方法(举例)
import { AttentionUser } from '../../model';
import dbStorage from '../../config/dbStorage';
import moment from 'moment';
import autobind from 'autobind-decorator' // 绑定this
import { logger } from '../../log4j';
@autobind
export default class User {
constructor() {
}
/**
* [getUserList 获取用户信息]
* @param {Function} cb [description]
* @param {[type]} info [description]
* @return {Promise} [description]
*/
async getUserList(cb, info) {
logger.warn(`this is moment format ${moment().format('YYYY-MM-DD hh:mm:ss')}`);
// 用orm模型去分页查询数据
const attentionUser = await AttentionUser.findAndCount({
limit: 10,
offset: 0
});
cb({ code: '00000', attentionUser });
}
/**
* [unsubscribe 取消关注]
* @method unsubscribe
* @param {Function} cb [description]
* @param {[type]} info [description]
* @return {Promise} [description]
*/
async unsubscribe(cb, info) {
// 开启事务
const t = await dbStorage.transaction();
try {
const res = await AttentionUser.update({
IS_DISPLAY: 'N',
UPDATE_TIME: new Date()
}, {
where: {
OPENID: info.openid
}
}, { transaction: t });
// 提交事务
t.commit();
cb({ code: '00000', res })
} catch (err) {
// 回滚事务
t.rollback();
console.log(err);
cb({ code: '00001', err: err });
}
}
}
3、sequelize
这里涉及到sequelize这个框架,笔者不在此去详细讲解,请给位移步官网自行查询 http://docs.sequelizejs.com/manual/tutorial/querying.html#operators
要说明的是,在package.json中笔者写了一个命令行去自动生成orm的model,大家只要修改对应的数据,再 npm run build 就可以生成实体模型。
"build": "sequelize-auto -o ./entity_model -d 数据库 -h 主机IP -u 用户名 -p 端口 -x 密码 -e mysql -a ./src/model/config.json"
这里的数据库配置也需要修改,在src/config/seqCong.json 中
{
"database": "数据库",
"username": "用户名",
"password": "密码",
"host": "主机",
"port": 3306
}
至此,我们完成了api网关到一个简单服务的通信,在今后的课程中会逐步搭建各类微服务,比如支付服务,物流服务,订单服务,还有其他一些框架的搭建。如果各位看官对笔者的文章感兴趣的话,希望关注下笔者的个人公众号,大家一起交流探讨,如有写的不对的地方,还希望各位指正,笔者深感荣幸,最后感谢大家的阅读。