1.课程大纲 #

2.BFF架构演进 #

2.1 单体服务 #

2.2 微服务 #

2.3 BFF #

2.4 网关 #

2.5 集群化 #

3.创建用户微服务 #

3.1 微服务 #

3.2 RPC #

3.3 sofa-rpc-node #

3.4 Protocol Buffers #

3.5 Zookeeper #

3.5.1 简介 #

3.5.2 安装启动 #

zookeeper\conf\zoo.cfg

+dataDir=./data

3.6 启动服务 #

3.6.1 安装 #

npm install mysql2 sofa-rpc-node --save

3.6.2 user\package.json #

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"
    }
}

3.6.3 user\index.js #

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(`用户微服务发布成功`);
})();

3.6.4 client.js #

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);
})()

4.创建文章微服务 #

4.1 安装 #

npm install mysql2 sofa-rpc-node --save

4.2 article\package.json #

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"
    }
}

4.3 article\index.js #

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(`文章微服务发布成功`);
})();

4.4 client.js #

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);
})()

5.创建BFF #

5.1 安装 #

npm install koa koa-router koa-logger sofa-rpc-node lru-cache ioredis amqplib fs-extra --save

访问地址

http://localhost:3000/?userId=1

5.2 bff\package.json #

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"
    }
}

5.3 bff\index.js #

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');
});

5.4 rpc.js #

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;

5.5 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({
    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');
});

6.缓存 #

6.1 多级缓存 #

6.2 LRU #

6.3 redis #

6.4 使用缓存 #

6.4.1 bff\index.js #

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');
});

6.4.2 cache.js #

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;

7.消息队列 #

7.1 引入原因 #

7.2 RabbitMQ #

7.3 实现 #

7.3.2 bff\index.js #

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');
});

7.3.2 mq.js #

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;

7.3.3 bff\logger.js #

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');
    });
})();

8.Serverless #

8.1 BFF问题 #

8.2 Serverless #

8.3 Serverless的优势 #

8.4 Serverless的缺点 #

9.GraphQL #

9.1 Apollo Server #

9.2 GraphQL schema language #

9.3 resolvers #

9.4 ApolloServer示例 #

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")
}

9.5 Apollo Server 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}`)
    );
})()