graphql+koa2 前端bff层

最近在做把graphql融入项目中。使用graphql的优势:

  1. 前端把握查询的主动权,可定义你需要查询的字段过滤冗余,另外减少两端的沟通
  2. 接手bff层前端可作为空间更大,包括做一些鉴权
  3. 请求合并更加便利(以前初始化多个请求需要一起返回都是使用promise.all,graphql可以更加便利的一次请求多个数据)
  4. (利于服务同学)他们可以专心开发微服务,不用再去管数据聚合这类事情
  5. 需要预定义类型,开发之前我们就能知道数据结构的基本样子

我们团队的后端使用的是restful规范。每次查询的时候可能多少都会出现冗余字段,要剔除这些冗余字段对于后端同学来说没有技术含量又耗时。另外后端同学对于bff层其实不怎么感冒,因为数据聚合对他们来说没什么含量,完全是对前端同学服务。所以我们完全可以引入查询来接手后端同学的bff层。又或者我们新增了字段需要查询新增的字段后端同学也需要更改。基于这些尝试引入node+graphql。graphql的查询优势在于前端可以主动控制字段的获取(只要这些字段是可以访问的)。集成graphql有两种方式。

  1. 后端同学直接集成 (java接口(restful或者graphql)-->前端)
  2. 前端增加中间服务层(java接口-->前端中间服务层nodejs(graphql)-->前端)

对于第一种方式,后端同学可能更改会更大,更改接口规范来迎合前端可能代价太大且后端同学可能也不太会高兴修改接口规范多出来的工作量。所以我们选了第二种,引入nodejs中间层作为请求的转发。
首先修改前端的代理前端代理到本地nodejs服务,直接使用weboack的proxy代理配置:

    proxy: {
        '/api': {
            target: 'http://localhost:8080/',
            changeOrigin: true,
        },
        '/local': {
            target: 'http://localhost:8080/',
            changeOrigin: true,
            pathRewrite: { '^/local': '' },
        },
    },

代理写了两个配置,带有'/api'前缀的直接代理到后端,带有'/local'的要在node中间层做处理。为什么要写要两个配置,因为不是所有的请求都需要使用graphql做处理,这一点在后面使用它的时候就会知道,它有优势当然也有劣势。引入你的项目要看它能发挥多大价值。
写了这两个配置之后,带有两个关键字的请求都讲代理到本地node服务的8080端口。接下来配置node中间层。

前端中间服务层的配置

中间服务层使用koa2搭建,当然你也可以使用express等等其他。graphql的集成就是用中间件koa-graphql

const Koa = require('koa');
const koaStatic = require('koa-static');
const views = require('koa-views');
const koaBody = require('koa-body');
const path = require('path');
const mount = require('koa-mount');
const { graphqlHTTP } = require('koa-graphql');
const { makeExecutableSchema } = require('graphql-tools');

const loggerMiddleware = require('./middleware/logger');
const errorHandler = require('./middleware/errorHandler');

const responseWrapperMiddleware = require('./middleware/responseWrapper');
// const decoratorRequest = require('./middleware/decoratorRequest');
const axiosRequest = require('./middleware/axiosRequest');
const accessToken = require('./middleware/accessToken');

const apiProxy = require('./middleware/apiProxy');

const typeDefs = require('./graphql/typeDefs');
const resolvers = require('./graphql/resolvers');

const router = require('./routes/_router');

const { APP_KEYS, API_HOST, APP_ID, APP_SECRET } = require('./config');

const port = process.env.PORT || 8080;
const distPath = path.join(__dirname, '/dist');

const getSchema = (...rst) => {
    const schema = makeExecutableSchema({
        typeDefs: typeDefs,
        resolvers: resolvers(...rst),
    });
    return schema;
};

const app = new Koa();

// logger配置
app.use(loggerMiddleware());

// 设置静态资源目录
app.use(
    koaStatic(path.resolve(__dirname, './dist'), {
        index: false,
        maxage: 60 * 60 * 24 * 365,
    }),
);

// 各环境下通用app配置

// cookie验证签名
app.keys = APP_KEYS;

//设置模板引擎ejs
app.use(
    views(distPath, {
        map: {
            html: 'ejs',
        },
    }),
);

// 异常处理
app.use(errorHandler);

// req.body
app.use(koaBody({ multipart: true }));

// 包装请求的返回
app.use(responseWrapperMiddleware());

// 请求
app.use(
    axiosRequest({
        baseURL: `${API_HOST}/audit`,
    }),
);

// 请求后端的accessToken
app.use(
    accessToken({
        appId: APP_ID,
        appSecret: APP_SECRET,
    }),
);

// 直接代理前端的/api请求转发给后端,内部统一做鉴权和参数设置
app.use(
    apiProxy({
        prefix: '/api',
    }),
);

// koa graphql中间件
app.use(
    mount(
        '/graphql',
        graphqlHTTP(async (
            request, 
            response, 
            ctx, 
            graphQLParams
        ) => {
            return ({
                schema: getSchema(request, response, ctx, graphQLParams),
                graphiql: true,
            });
        })
    ),
);

// 路由
app.use(router.routes());
app.use(router.allowedMethods());

app.listen(port, function() {
    console.log(
        `\n[${
            process.env.NODE_ENV === 'production' ? 'production' : 'development'
        }] app server listening on port: ${port}\n`,
    );
});

主要看看graphql的配置其他都是koa常规的中间件配置

const getSchema = (...rst) => {
    const schema = makeExecutableSchema({
        typeDefs: typeDefs,
        resolvers: resolvers(...rst),
    });
    return schema;
}

主要是生成graphql需要的schema。typeDefs是graphql的类型定义,使用的是schema来约束类型,resolvers就是解释器也就是你定义的类型需要怎么处理。
比如:
你的typeDefs类型自定是这样子(它是一个字符串):

const typeDefs = `
            type ExportItem {
                applicantStatus: String
                approving: [ String ]
                approvingMulitPassType: String
                auditFlowId: String
                bizName: String
                createdAt: Int
                createdBy: Int
                createdByName: String
                deleted: Boolean
                finishTime: Int
                groupId: String
                groupName: String
                id: String
                showApplyId: String
                templateId: String
                templateName: String
                updatedAt: Int
                updatedBy: Int
                updatedByName: String
                auditFlowForwardType: String
                uiConfig: String
                templateDesc: String
            }

            input QueryExportListParams {
                pageIndex: Int
                pageSize: Int
                finishedTimeBegin: Int
                finishedTimeEnd: Int
                showApplyId: String
                auditFlowId: String
                bizName: String
                initiatorEmployeeId: Int
                status: String
            }

            type Query {
                exportList(params: QueryExportListParams): [ ExportItem ]
                exportDetail(id: String): ExportItem
            }
        `

除开Query是graphql内部关键字,其他都是我们定义的。Query是graphql中的顶层类型,除开Query我们常用的还有Mutation。graphql规定所有的查询定义都要放在Query中,那么修改操作比如,我们要做增加,修改这些操作就放在mutation中。其实就算把所有的操作都放在query中或者mutation中解析也会通过,但是作为规范query中写查询,mutation中写操作也许更更好。那上面的定义是什么意思先分析一下,先看Query内部:

  type Query {
                exportList(params: QueryExportListParams): [ ExportItem ]
                exportDetail(id: String): ExportItem
            }

代表我们定义了两个查询名字叫exportList, exportDetail。exportDetail接受一个名字叫params的参数,params的类型是QueryExportListParams,返回一个数组数组里面的数据项类型是ExportItem。exportDetail接受一个id的参数id类型是字符串,返回的数据类型是ExportItem。ExportItem是我们自己定义的数据类型。QueryExportListParams是自己定义的参数类型,参数是输入类型必须要使用input关键字定义。那么这里定义了类型实现在哪里,实现就在resolvers中,每个类型定义在resolver中都必须有解析器一一对应。
所以resolvers张这样子

const resolvers = {
        Query: {
            exportList: async (_, { params }) => {
                const res = await ctx.axios({
                    url: '/data/export/all',
                    method: 'get',
                    params,
                    headers
                });
                return res.data;
            },
            exportDetail: async (_, { id }) => {
                const res = await ctx.axios({
                    url: `/applicant/byId/${id}`,
                    method: 'get',
                    headers
                });
                return res.data;
            }
        }
    };

解析器中就有类型定义的实现exportList,exportDetail。在解析器中,他们的数据来源可以是任何地方,有可能是数据库,也可能是其他接口。我们这里是做中间层转发。所以直接使用axios转发到后端了。那么类型定义的参数就在这里获取使用。
配置好后启动中间层服务,graphql查询生效之后会开启一个/graphql的路径接口,如果我们要使用graphql查询就请求/graphql这个路径。比如我们在前端请求graphql这个查询就会这么写:

post('/graphql', {
                query: `query ExportList($params: QueryExportListParams){
                    exportList(params: $params) {
                        id
                    }
                }`,
                variables: {
                    params: {
                        finishedTimeBegin: finishedTime
                            ? +moment(finishedTime[0]).startOf('day')
                            : void 0,
                        finishedTimeEnd: finishedTime
                            ? +moment(finishedTime[1]).endOf('day')
                            : void 0,
                        ...rst,
                    }
                }
            })

QueryExportListParams就是我们在中间层定义的参数类型,variables.params是我们传递给resolvers的参数值

exportList(params: $params) {
                        id
                    }

这表达我们查询的返回数据中之返回带有id的列表,返回的是列表是因为我们在类型定义的时候已经定义这个查询需要返回列表:

  type Query {
                exportList(params: QueryExportListParams): [ ExportItem ]
                exportDetail(id: String): ExportItem
            }

这里我们已经定义了exportList的返回类型是一个列表,类标的类型是ExportItem,所以我们不需要再告诉查询是不是取列表,返回类型都是事先定义好的,我们需要做的是控制返回字段,只要ExportItem这个类型包含的字段我们都可以定义取或者是不取,比如我们上面

exportList(params: $params) {
                        id
                    }

这就是表示我们只取id这个字段那么返回的数据中只会有id这个字段,如果我们还需要其他字段比如我们还需要groupName这个字段,就可以这样写

exportList(params: $params) {
                        id
                        groupName
                    }

只要是在我们定义的ExportItem这个类型之中我们都可以控制它取或者不取,如果你查询的参数在服务端的graphql中未定义就会出错。graphql的查询中另外一个比较好的地方在于指令,指令的加入会让bff层更加有做为(放在下一次讲)

46 声望
2 粉丝
0 条评论
推荐阅读
redux 源码解读
redux作为react生态圈中最常见的状态库,从源码来探究一下redux是如何做状态管理的。redux源码并不复杂,了解源码之前,我们先看看我们会怎么使用redux。

H_H_code阅读 761

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木60阅读 6k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.2k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木39阅读 7.1k评论 6

CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(>^ω^&lt...

XboxYan42阅读 2.8k评论 14

封面图
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan34阅读 2.2k评论 2

封面图
从零搭建 Node.js 企业级 Web 服务器(二):校验
校验就是对输入条件的约束,避免无效的输入引起异常。Web 系统的用户输入主要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接收表单的接口也要做足校验行为,通过前后端共同控制输...

乌柏木32阅读 6.1k评论 9

46 声望
2 粉丝
宣传栏