graphql+koa2 前端bff层

2024-05-30 13:06:43 浏览数 (3)

最近在做把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代理配置:

代码语言:javascript复制
    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

代码语言:javascript复制
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常规的中间件配置

代码语言:javascript复制
const getSchema = (...rst) => {
    const schema = makeExecutableSchema({
        typeDefs: typeDefs,
        resolvers: resolvers(...rst),
    });
    return schema;
}

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

代码语言:javascript复制
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内部:

代码语言:javascript复制
  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张这样子

代码语言:javascript复制
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这个查询就会这么写:

代码语言:javascript复制
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的参数值

代码语言:javascript复制
exportList(params: $params) {
                        id
                    }

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

代码语言:javascript复制
  type Query {
                exportList(params: QueryExportListParams): [ ExportItem ]
                exportDetail(id: String): ExportItem
            }

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

代码语言:javascript复制
exportList(params: $params) {
                        id
                    }

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

代码语言:javascript复制
exportList(params: $params) {
                        id
                        groupName
                    }

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

0 人点赞