有了 BFF 再也不用求服务包接口了

198 阅读4分钟

虽然很多后端同学会调侃自己的工作是 CURD,但最让后端同学方案的工作可能是没完没了为前端包接口,某个接口已经为 App 写了,但 Web 上增删了一些字段,改了一下结构就要重写一个新接口,确实没什么技术含量,还要跟着走一遍需求流程

BFF(Backend for Frontend)服务于前端的后端,它为不同的前端(如移动应用、网页应用等)提供定制化的后端服务。这些服务会根据前端的需求进行数据的聚合、裁剪和转换,以适应不同设备和场景的特点,旨在解决前端与后端协作中的低效问题

BFF 技术选型

业界有非常多 BFF 的解决方案,因为 BFF 接口代码大部分时候由前端来写,所以很多团队选择了使用前端更熟悉的 Node.js 来实现 BFF,但实践下来根据团队现有的后端技术栈和语言选择 BFF 技术栈,往往能带来更多的优势和长期收益

虽然 BFF 主要服务于前端,但本质上都是调用后端服务能力,很多服务有可能面临与 Node.js 互调用的集成成本,选择与团队现有后端技术栈一致的语言,可以更方便地与现有系统、数据库、中间件等进行集成,简化数据传输和服务调用

同时如果只是简单使用 Node.js 来实现 BFF,仅仅是把封装 DO 转 VO 的工作转嫁给了前端,考虑到前端同学可能对后端服务能力知识的欠缺,并不会带来显著的效率提升

GraphQL 是 Facebook 开发的一种用于 API 的查询语言,以及用于响应这些查询的运行时环境。GraphQL 的核心理念是客户端驱动的数据获取, 这意味着客户端指定它需要什么样的数据结构,服务器则返回符合要求的数据,非常适合数据查询为主的 BFF 诉求

GraphQL 有 Java、Go、Python、PHP、Node.js、C#、Rust 等主流语言的实现,可以轻松集成到团队的后端技术栈里

GraphQL 核心概念

Schema 和类型系统

Schema 是 GraphQL 的核心,定义了 API 的结构。它包括类型定义、查询类型、变更类型和订阅类型。类型系统确保了数据的一致性,并为开发者提供了自描述的 API

  • 标量类型(Scalar Types) :基本的数据类型,如 Int, Float, String, Boolean, ID
  • 对象类型(Object Types) :具有一组字段的对象,如 User, Post
  • 枚举类型(Enum Types) :有限集合的值,如 Status
  • 输入类型(Input Types) :用于定义查询和变更输入的结构
  • 接口(Interfaces)联合类型(Unions) :用于描述不同类型之间的关系
type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post]
}

type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
}

type Query {
    users: [User]
    user(id: ID!): User
    posts: [Post]
    post(id: ID!): Post
}

type Mutation {
    createUser(name: String!, email: String!): User
    createPost(title: String!, content: String!, authorId: ID!): Post
}

查询

Query 用于读取或获取数据,客户端通过构造查询,指定需要的数据字段

query {
    user(id: "1") {
        name
        email
        posts {
            title
        }
    }
}

响应示例

{
  "data": {
    "user": {
      "name": "Alice",
      "email": "alice@example.com",
      "posts": [
        {
          "title": "GraphQL Introduction"
        },
        {
          "title": "Advanced GraphQL"
        }
      ]
    }
  }
}

变更

Mutation 用于创建、更新或删除数据,类似于 REST 的 POST、PUT、DELETE 操作

mutation {
    createUser(name: "Bob", email: "bob@example.com") {
        id
        name
    }
}

响应示例

{
  "data": {
    "createUser": {
      "id": "2",
      "name": "Bob"
    }
  }
}

订阅

Subscription 用于实时获取数据更新,类似于 WebSocket,客户端订阅某个事件,当该事件触发时服务器会推送更新的数据

subscription {
    postAdded {
        id
        title
        author {
            name
        }
    }
}

响应示例

{
  "data": {
    "postAdded": {
      "id": "3",
      "title": "Real-time GraphQL",
      "author": {
        "name": "Charlie"
      }
    }
  }
}

解析器

Resolver 是处理 GraphQL 查询和变更的函数,每个字段有一个对应的解析器函数,负责获取该字段的数据

Node.js 实现的 Apollo Server 解析器示例

const { ApolloServer, gql } = require('apollo-server');

// 定义 Schema
const typeDefs = gql`
    type User {
        id: ID!
        name: String!
        email: String!
        posts: [Post]
    }

    type Post {
        id: ID!
        title: String!
        content: String!
        author: User!
    }

    type Query {
        users: [User]
        user(id: ID!): User
        posts: [Post]
        post(id: ID!): Post
    }

    type Mutation {
        createUser(name: String!, email: String!): User
        createPost(title: String!, content: String!, authorId: ID!): Post
    }
`;

// 模拟数据
const users = [];
const posts = [];

// 定义解析器
const resolvers = {
  Query: {
    users: () => users,
    user: (_, { id }) => users.find(user => user.id === id),
    posts: () => posts,
    post: (_, { id }) => posts.find(post => post.id === id),
  },
  Mutation: {
    createUser: (_, { name, email }) => {
      const user = { id: `${users.length + 1}`, name, email };
      users.push(user);
      return user;
    },
    createPost: (_, { title, content, authorId }) => {
      const post = { id: `${posts.length + 1}`, title, content, authorId };
      posts.push(post);
      return post;
    }
  },
  User: {
    posts: (user) => posts.filter(post => post.authorId === user.id)
  },
  Post: {
    author: (post) => users.find(user => user.id === post.authorId)
  }
};

// 创建 Apollo Server 实例
const server = new ApolloServer({ typeDefs, resolvers });

// 启动服务器
server.listen().then(({ url }) => {
  console.log(`服务器已启动,访问地址:${url}`);
});

使用 Node.js 实现 GraphQL

为了方便理解,我们使用 apollo server 写一个简单的 BFF

文件结构

graphql-example/
├── schema.js
├── resolvers.js
├── index.js
├── package.json
└── README.md

package.json

{
  "name": "graphql-example",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "apollo-server": "^3.13.0",
    "graphql": "^16.9.0"
  }
}

schema.js

const { gql } = require('apollo-server');

const typeDefs = gql`
  type Product {
    id: ID!
    name: String!
    price: Float!
  }

  type Query {
    products: [Product]
    product(id: ID!): Product
  }

  type Mutation {
    addProduct(name: String!, price: Float!): Product
    updateProduct(id: ID!, name: String, price: Float): Product
  }
`;

module.exports = typeDefs;

resolvers.js

// 使用 mock 数据
let products = [
  { id: '1', name: 'Laptop', price: 999.99 },
  { id: '2', name: 'Phone', price: 499.99 },
];

const resolvers = {
  Query: {
    products: () => products,
    product: (_, { id }) => products.find(product => product.id === id),
  },
  Mutation: {
    addProduct: (_, { name, price }) => {
      const newProduct = { id: `${products.length + 1}`, name, price };
      products.push(newProduct);
      return newProduct;
    },
    updateProduct: (_, { id, name, price }) => {
      const product = products.find(product => product.id === id);
      if (!product) {
        throw new Error('Product not found');
      }
      if (name !== undefined) product.name = name;
      if (price !== undefined) product.price = price;
      return product;
    },
  },
};

module.exports = resolvers;

index.js

const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');

// 创建 Apollo Server 实例
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// 启动服务器
server.listen(4000).then(({ url }) => {
  console.log(`服务器已启动,访问地址:${url}`);
});

启动与测试

$ npm install
$ npm start

这时候会看到提示

服务器已启动,访问地址:http://localhost:4000/

在浏览器中打开 http://localhost:4000/,将看到 Apollo Server 提供的 Apollo Studio Explorer

使用 fetch 发送请求

GraphQL 请求通常通过 HTTP POST 方法发送,其 Body 是一个 JSON 对象,包含以下字段:

  • query:必需的,包含要执行的 GraphQL 查询或变更
  • variables:可选的,包含查询或变更中使用的变量
  • operationName:可选的,指定要执行的具体操作名称,特别是在同一请求中包含多个操作时
{
  "query": "GraphQL查询或变更字符串",
  "variables": { "变量名": "变量值", ... },
  "operationName": "操作名称"
}

使用 fetch 发送请求

const query = `
  query GetProduct($id: ID!) {
    product(id: $id) {
      id
      name
      price
    }
  }
`;

const variables = { id: "2" };

fetch('http://localhost:4000/', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query, variables }),
}).then(res => res.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));