XML
,但前端需要JSON
Backend for Frontend
的缩写,指的是专门为前端应用设计的后端服务RPC
协议比HTTP
协议具有更低的延迟和更高的性能,所以用的更多RPC(Remote Procedure Call)
是远程过程调用的缩写,是一种通信协议,允许程序在不同的计算机上相互调用远程过程,就像调用本地过程一样sofa-rpc-node
是基于 Node.js 的一个 RPC 框架,支持多种协议Protocol Buffers
(简称 protobuf)是 Google 开发的一种数据序列化格式,可以将结构化数据序列化成二进制格式,并能够跨语言使用conf
目录中找到 zookeeper.properties
文件,修改相关配置bin\zkServer.cmd
即可启动 Zookeeperzookeeper\conf\zoo.cfg
+dataDir=./data
logger
是日志记录器,用于记录服务器运行时的日志信息registry
是一个注册中心,用于维护服务的注册信息,帮助服务节点和客户端找到对方。server
表示服务端。服务端是提供服务的节点,它会将自己所提供的服务注册到注册中心,并等待客户端的调用。服务端通常会实现具体的业务逻辑,并使用 RPC
或其他通信协议与客户端进行通信server
的 addService
方法接受两个参数:服务接口和服务实现。服务接口是一个对象,其中包含了服务的名称信息。服务实现是一个对象,其中包含了具体实现服务方法的函数start
方法,用于启动服务器publish
方法,用于向注册中心注册服务。这样,客户端就可以通过注册中心获取服务的地址和端口,并直接向服务器发起调用npm install mysql2 sofa-rpc-node --save
user\package.json
{
"name": "user",
"version": "1.0.0",
"description": "",
"main": "index.js",
+ "scripts": {
+ "dev": "nodemon index.js"
+ },
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"sofa-rpc-node": "^2.8.0"
}
}
user\index.js
const { server: { RpcServer }, registry: { ZookeeperRegistry } } = require('sofa-rpc-node');
const mysql = require('mysql2/promise');
let connection;
// 引入 console 模块
const logger = console;
// 创建 Zookeeper 注册中心实例,传入地址为 '127.0.0.1:2181'
const registry = new ZookeeperRegistry({
logger,
address: '127.0.0.1:2181',
connectTimeout: 1000 * 60 * 60 * 24,
});
// 创建 RPC 服务器实例,传入注册中心和端口号
const server = new RpcServer({
logger,
registry,
port: 10000
});
// 添加服务接口,实现 getUserInfo 方法
server.addService({
interfaceName: 'com.zhufeng.user'
}, {
async getUserInfo(userId) {
const [rows] = await connection.execute(`SELECT id,username,avatar,password,phone FROM user WHERE id=${userId} limit 1`);
return rows[0];
}
});
// 启动 RPC 服务器,并发布服务
(async function () {
connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'root',
database: 'bff'
});
await server.start();
await server.publish();
console.log(`用户微服务发布成功`);
})();
user\client.js
const { client: { RpcClient }, registry: { ZookeeperRegistry } } = require('sofa-rpc-node');
// 设置日志记录器
const logger = console;
// 创建 Zookeeper 注册中心
const registry = new ZookeeperRegistry({
logger,
address: '127.0.0.1:2181',
});
(async function () {
// 创建 RPC 客户端
const client = new RpcClient({ logger, registry });
// 创建 RPC 服务消费者
const userConsumer = client.createConsumer({
// 指定服务接口名称
interfaceName: 'com.zhufeng.user'
});
// 等待服务就绪
await userConsumer.ready();
// 调用服务方法
const result = await userConsumer.invoke('getUserInfo', [1], { responseTimeout: 3000 });
// 输出结果
console.log(result);
process.exit(0);
})()
npm install mysql2 sofa-rpc-node --save
article\package.json
{
"name": "user",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
+ "dev": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"mysql2": "^2.3.3",
"sofa-rpc-node": "^2.8.0"
}
}
article\index.js
const { server: { RpcServer }, registry: { ZookeeperRegistry } } = require('sofa-rpc-node');
const mysql = require('mysql2/promise');
let connection;
// 引入 console 模块
const logger = console;
// 创建 Zookeeper 注册中心实例,传入地址为 '127.0.0.1:2181'
const registry = new ZookeeperRegistry({
logger,
address: '127.0.0.1:2181',
connectTimeout: 1000 * 60 * 60 * 24,
});
// 创建 RPC 服务器实例,传入注册中心和端口号
const server = new RpcServer({
logger,
registry,
port: 20000
});
// 添加服务接口,实现 getPostCount 方法
server.addService({
interfaceName: 'com.zhufeng.post'
}, {
async getPostCount(userId) {
const [rows] = await connection.execute(`SELECT count(*) as postCount FROM post WHERE user_id=${userId} limit 1`);
return rows[0].postCount;
}
});
// 启动 RPC 服务器,并发布服务
(async function () {
connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'root',
database: 'bff'
});
await server.start();
await server.publish();
console.log(`文章微服务发布成功`);
})();
article\client.js
const { client: { RpcClient }, registry: { ZookeeperRegistry } } = require('sofa-rpc-node');
// 设置日志记录器
const logger = console;
// 创建 Zookeeper 注册中心
const registry = new ZookeeperRegistry({
logger,
address: '127.0.0.1:2181',
});
(async function () {
// 创建 RPC 客户端
const client = new RpcClient({ logger, registry });
// 创建 RPC 服务消费者
const consumer = client.createConsumer({
// 指定服务接口名称
interfaceName: 'com.zhufeng.post'
});
// 等待服务就绪
await consumer.ready();
// 调用服务方法
const result = await consumer.invoke('getPostCount', [1], { responseTimeout: 3000 });
// 输出结果
console.log(result);
process.exit(0);
})()
npm install koa koa-router koa-logger sofa-rpc-node lru-cache ioredis amqplib fs-extra --save
访问地址
http://localhost:3000/?userId=1
bff\package.json
{
"name": "bff",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
+ "dev": "nodemon index.js",
+ "start": "pm2 start index.js --name bff"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.14.1",
"koa-logger": "^3.2.1",
"koa-router": "^12.0.0",
"sofa-rpc-node": "^2.8.0"
}
}
bff\index.js
const Koa = require('koa');
const router = require('koa-router')();
const logger = require('koa-logger');
const rpcMiddleware = require('./middleware/rpc');
const app = new Koa();
app.use(logger());
app.use(rpcMiddleware({
//配置 rpc 中间件的参数,表示要调用的 rpc 接口名称
interfaceNames: [
'com.zhufeng.user',
'com.zhufeng.post'
]
}));
router.get('/', async ctx => {
const userId = ctx.query.userId;
const { rpcConsumers: { user, post } } = ctx;
const [userInfo, postCount] = await Promise.all([
user.invoke('getUserInfo', [userId]),
post.invoke('getPostCount', [userId])
]);
ctx.body = { userInfo, postCount }
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log('bff server is running at 3000');
});
bff\middleware\rpc.js
const { client: { RpcClient }, registry: { ZookeeperRegistry } } = require('sofa-rpc-node');
const rpcMiddleware = (options = {}) => {
return async function (ctx, next) {
const logger = options.logger || console;
//创建 ZookeeperRegistry 类的实例,用于管理服务发现和注册
const registry = new ZookeeperRegistry({
logger,
address: options.address || '127.0.0.1:2181',
});
//创建 RpcClient 类的实例,用于发送 rpc 请求
const client = new RpcClient({ logger, registry });
const interfaceNames = options.interfaceNames || [];
const rpcConsumers = {};
for (let i = 0; i < interfaceNames.length; i++) {
const interfaceName = interfaceNames[i];
//使用 RpcClient 的 createConsumer 方法创建 rpc 消费者
const consumer = client.createConsumer({
interfaceName,
});
//等待 rpc 消费者准备完毕
await consumer.ready();
rpcConsumers[interfaceName.split('.').pop()] = consumer;
}
ctx.rpcConsumers = rpcConsumers;
await next();
}
};
module.exports = rpcMiddleware;
数据处理
const Koa = require('koa');
const router = require('koa-router')();
const logger = require('koa-logger');
const rpcMiddleware = require('./middleware/rpc');
const app = new Koa();
app.use(logger());
app.use(rpcMiddleware({
interfaceNames: [
'com.zhufeng.user',
'com.zhufeng.post'
]
}));
router.get('/', async ctx => {
const userId = ctx.query.userId;
const { rpcConsumers: { user, post } } = ctx;
const [userInfo, postCount] = await Promise.all([
user.invoke('getUserInfo', [userId]),
post.invoke('getPostCount', [userId])
]);
+ // 裁剪数据
+ delete userInfo.password;
+ // 数据脱敏
+ userInfo.phone = userInfo.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
+ // 数据适配
+ userInfo.avatar = "http://www.zhufengpeixun.cn/"+userInfo.avatar,
ctx.body = { userInfo, postCount }
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log('bff server is running at 3000');
});
ioredis
是一个基于 Node.js 的 Redis 客户端,提供了对 Redis 命令的高度封装和支持bff\index.js
const Koa = require('koa');
const router = require('koa-router')();
const logger = require('koa-logger');
const rpcMiddleware = require('./middleware/rpc');
+const cacheMiddleware = require('./middleware/cache');
const app = new Koa();
app.use(logger());
app.use(rpcMiddleware({
interfaceNames: [
'com.zhufeng.user',
'com.zhufeng.post'
]
}));
+app.use(cacheMiddleware({}));
router.get('/profile', async ctx => {
const userId = ctx.query.userId;
const { rpcConsumers: { user, post } } = ctx;
+ const cacheKey = `${ctx.method}#${ctx.path}#${userId}`;
+ let cacheData = await ctx.cache.get(cacheKey);
+ if (cacheData) {
+ ctx.body = cacheData;
+ return;
+ }
const [userInfo, postCount] = await Promise.all([
user.invoke('getUserInfo', [userId]),
post.invoke('getPostCount', [userId])
]);
// 裁剪数据
delete userInfo.password;
// 数据脱敏
userInfo.phone = userInfo.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
// 数据适配
userInfo.avatar = "http://www.zhufengpeixun.cn/" + userInfo.avatar;
+ cacheData = { userInfo, postCount };
+ await ctx.cache.set(cacheKey, cacheData);// keys *
+ ctx.body = cacheData
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log('bff server is running at 3000');
});
bff\middleware\cache.js
const LRUCache = require('lru-cache');
const Redis = require('ioredis');
class CacheStore {
constructor() {
this.stores = [];
}
add(store) {
this.stores.push(store);
return this;
}
async get(key) {
for (const store of this.stores) {
const value = await store.get(key);
if (value !== undefined) {
return value;
}
}
}
async set(key, value) {
for (const store of this.stores) {
await store.set(key, value);
}
}
}
class MemoryStore {
constructor() {
this.cache = new LRUCache({
max: 100,
ttl: 1000 * 60 * 60 * 24
});
}
async get(key) {
return this.cache.get(key);
}
async set(key, value) {
this.cache.set(key, value);
}
}
class RedisStore {
constructor(options) {
this.client = new Redis(options);
}
async get(key) {
let value = await this.client.get(key);
return value ? JSON.parse(value) : undefined;
}
async set(key, value) {
await this.client.set(key, JSON.stringify(value));
}
}
const cacheMiddleware = (options = {}) => {
return async function (ctx, next) {
const cacheStore = new CacheStore();
cacheStore.add(new MemoryStore());
const redisStore = new RedisStore(options);
cacheStore.add(redisStore);
ctx.cache = cacheStore;
await next();
};
};
module.exports = cacheMiddleware;
RabbitMQ
是一个消息代理,它可以用来在消息生产者和消息消费者之间传递消息RabbitMQ
服务器RabbitMQ
服务器将消息保存到队列中RabbitMQ
服务器将消息删除bff\index.js
const Koa = require('koa');
const router = require('koa-router')();
const logger = require('koa-logger');
const rpcMiddleware = require('./middleware/rpc');
const cacheMiddleware = require('./middleware/cache');
+const mqMiddleware = require('./middleware/mq');
const app = new Koa();
app.use(logger());
app.use(rpcMiddleware({
interfaceNames: [
'com.zhufeng.user',
'com.zhufeng.post'
]
}));
app.use(cacheMiddleware({}));
+app.use(mqMiddleware({ url: 'amqp://localhost' }));
router.get('/profile', async ctx => {
const userId = ctx.query.userId;
+ ctx.channels.logger.sendToQueue('logger', Buffer.from(JSON.stringify({
+ method: ctx.method,
+ path: ctx.path,
+ userId
+ })));
const { rpcConsumers: { user, post } } = ctx;
const cacheKey = `${ctx.method}#${ctx.path}#${userId}`;
let cacheData = await ctx.cache.get(cacheKey);
if (cacheData) {
ctx.body = cacheData;
return;
}
const [userInfo, postCount] = await Promise.all([
user.invoke('getUserInfo', [userId]),
post.invoke('getPostCount', [userId])
]);
// 裁剪数据
delete userInfo.password;
// 数据脱敏
userInfo.phone = userInfo.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
// 数据适配
userInfo.avatar = "http://www.zhufengpeixun.cn/" + userInfo.avatar,
cacheData = { userInfo, postCount };
await ctx.cache.set(cacheKey, cacheData);// keys *
ctx.body = cacheData
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log('bff server is running at 3000');
});
bff\middleware\mq.js
const amqp = require('amqplib');
const mqMiddleware = (options = {}) => {
return async (ctx, next) => {
//使用 amqp.connect 方法连接 RabbitMQ 服务器
const rabbitMQClient = await amqp.connect(options.url || 'amqp://localhost');
//使用 rabbitMQClient 的 createChannel 方法创建 RabbitMQ 通道
const logger = await rabbitMQClient.createChannel();
//使用 logger 的 assertQueue 方法创建名为 "logger" 的队列,如果队列已经存在则不会重复创建
await logger.assertQueue('logger');
ctx.channels = {
logger
};
await next();
};
};
module.exports = mqMiddleware;
bff\logger.js
const amqplib = require('amqplib');
const fs = require('fs-extra');
const path = require('path');
(async () => {
const conn = await amqplib.connect('amqp://localhost');
const loggerChannel = await conn.createChannel();
await loggerChannel.assertQueue('logger');
loggerChannel.consume('logger', async (event) => {
const message = JSON.parse(event.content.toString());
await fs.appendFile(path.join(__dirname, 'logger.txt'), JSON.stringify(message) + '\n');
});
})();
GraphQL
是一种用于API的查询语言,它允许客户端向服务端请求特定的数据,而不是服务端将所有可能的数据全部返回。这样,客户端可以更精确地获取所需的数据,并且服务端可以更有效地满足请求GraphQL
可以让客户端自己定义所需的数据结构,可以灵活地获取所需的数据。这对于多端应用来说非常方便,因为每一个客户端可能有不同的数据需求,使用GraphQL可以让每个客户端自己定义所需的数据结构GraphQL
可以让BFF服务层从不同的数据源获取数据,并将它们组合起来返回给客户端。这对于在BFF架构中更好地组织数据是很有帮助的,因为你可以在BFF层中组合来自不同数据源的数据,而不用在客户端中再做一次组合Apollo Server
是一个用于构建GraphQL API的开源服务器框架。它支持多种编程语言,允许你使用同一种方式来访问不同的后端数据源,并且具有良好的扩展性Apollo Server
是一种实现GraphQL服务端的方法,它提供了一些工具和功能,帮助你更轻松地构建和部署GraphQL API。它还提供了一些额外的功能,如缓存、身份验证和模拟数据,帮助你更快速地开发和测试你的APIGraphQL schema language
是一种用来定义GraphQL API的语言。它可以用来描述API中可用的数据和操作,包括支持的查询、变更、订阅等功能GraphQL schema
由一系列的类型组成,每种类型都有一个名称和一些字段。每个字段都有一个名称和类型,并可能有一些额外的限制,比如是否是必填的或者有默认值resolvers
是负责解析每个字段的函数。在Apollo Server中,你可以使用resolvers对象来定义这些函数resolvers
对象是一个包含了所有解析器函数的对象。它的结构与你在schema中定义的类型的结构是一样的gql
函数是一个template tag
,你可以将它放在模板字符串的前面,然后在模板字符串中编写GraphQL schema language的代码,可以定义查询和变更操作const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type Query {
users: [User]
user(id: ID): User
}
type Mutation {
createUser(username: String, age: Int): User
updateUser(id: ID, username: String, age: Int): Boolean
deleteUser(id: ID): Boolean
}
type User {
id: ID
username: String
age: Int
}
`;
let users = [
{ id: "1", username: "zhangsan", age: 25 },
{ id: "2", username: "lisi", age: 30 },
];
const resolvers = {
Query: {
users: (obj, args, context, info) => {
return users;
},
user: (obj, args, context, info) => {
return users.find(user => user.id === args.id);
}
},
Mutation: {
createUser: (obj, args, context, info) => {
const newUser = { id: users.length + 1, username: args.username, age: args.age };
users.push(newUser);
return newUser;
},
updateUser: (obj, args, context, info) => {
const updatedUser = { id: args.id, username: args.username, age: args.age };
users = users.map(user => {
if (user.id === args.id) {
return updatedUser;
}
return user;
});
return true;
},
deleteUser: (obj, args, context, info) => {
users = users.filter(user => user.id !== args.id);
return true;
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});
query {
users {
id
username
age
}
}
query {
user(id: "1") {
id
username
age
}
}
mutation {
createUser(username: "wangwu", age: 35) {
id
username
age
}
}
mutation {
updateUser(id: "1", username: "zhangsan2", age: 26)
}
mutation {
deleteUser(id: "1")
}
GraphQL API
添加到 Koa 应用程序中。它使用 Apollo Server
来执行 GraphQL
服务器逻辑,并允许使用 Koa 的优秀特性(如路由和中间件)来构建应用程序const Koa = require('koa');
const { ApolloServer } = require('apollo-server-koa');
const typeDefs = `
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => 'Hello, world!',
},
};
(async function () {
const server = new ApolloServer({ typeDefs, resolvers });
await server.start()
const app = new Koa();
server.applyMiddleware({ app });
app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
);
})()