当前位置: 首页 > Web前端 > HTML

graphql+koa2前端bff层

时间:2023-03-29 10:50:09 HTML

目前正在致力于将graphql集成到项目中。使用graphql的好处:前端掌握了查询的主动权,可以定义自己需要查询的字段过滤冗余,减少两端通信,接管前端bff层。多个请求需要使用promise.all一起返回,graphql可以更方便的一次请求多个数据)(有利于服务同学)他们可以专心开发微服务,不用担心数据聚合等事情需要预定义的类型。在开发之前,我们可以知道数据结构的基本外观。我们团队的后端使用的是restful规范。每次查询都可能或多或少的出现冗余字段,而后端同学要剔除这些冗余字段又费时又费时。另外,后端同学其实对bff层不是很感兴趣,因为dataaggregation给他们的内容很少,完全是给前端同学看的。所以我们可以引入查询来接管后端同学的bff层。或者我们新增了一个字段,需要查询新增的字段。后端同学也需要改变。基于这些尝试引入node+graphql。graphql的查询优势在于前端可以主动控制字段的获取(只要这些字段是可访问的)。有两种方法可以集成graphql。后端同学直接集成(java接口(restful或graphql)-->前端)前端加一个中间服务层(java接口-->前端中间服务层nodejs(graphql)-->前端)。对于第一种方式,后端同学可能变化更大,改接口规范去迎合前端可能成本太高,后端同学可能不乐意修改接口规范的额外工作量.所以我们选择了第二种方案,引入nodejs中间层作为请求转发。首先修改前端代理为本地nodejs服务,直接使用weboack的proxy代理配置:proxy:{'/api':{target:'http://localhost:8080/',changeOrigin:true,},'/local':{target:'http://localhost:8080/',changeOrigin:true,pathRewrite:{'^/local':''},},},proxy写了两个以'/api'为前缀的配置直接到后端,带'/local'在节点中间层处理。为什么要写两个配置,因为并不是所有的请求都需要graphql来处理,这个以后用到的时候就知道了,当然也有优缺点。介绍一下你的项目,看看它能发挥多大的价值。写完这两个配置后,带有两个关键字的请求会被代理到本地节点服务的8080端口。接下来配置节点中间层。前端中间服务层的配置中间服务层是使用koa2搭建的,当然你也可以使用express等。graphql的集合就是用中间件koa-graphqlconstKoa=require('koa');constkoaStatic=require('koa-static');constviews=require('koa-views');constkoaBody=require('koa-body');constpath=require('path');constmount=require('koa-mount');const{graphqlHTTP}=require('koa-graphql');const{makeExecutableSchema}=require('graphql-tools');constloggerMiddleware=require('./middleware/logger');consterrorHandler=require('./middleware/errorHandler');constresponseWrapperMiddleware=require('./middleware/responseWrapper');//constdecoratorRequest=require('./middleware/decoratorRequest');constaxiosRequest=require('./middleware/axiosRequest');constaccessToken=require('./middleware/accessToken');constapiProxy=require('./middleware/apiProxy');consttypeDefs=require('./graphql/typeDefs');constresolvers=require('./graphql/resolvers');constrouter=require('./routes/_router');const{APP_KEYS,API_HOST,APP_ID,APP_SECRET}=require('./conf搞笑');constport=process.env.PORT||8080;constdistPath=path.join(__dirname,'/dist');constgetSchema=(...rst)=>{constschema=makeExecutableSchema({typeDefs:typeDefs,resolvers:resolvers(...rst),});返回架构;};constapp=newKoa();//logger配置app.use(loggerMiddleware());//设置静态资源目录app.use(koaStatic(path.resolve(__dirname,'./dist'),{index:false,maxage:60*60*24*365,}),);//各环境通用app配置//cookie验证签名app.keys=APP_KEYS;//设置模板引擎ejsapp.use(views(distPath,{map:{html:'ejs',},}));//异常处理app.use(errorHandler);//req.bodyapp.use(koaBody({multipart:true}));//返回包装请求app.use(responseWrapperMiddleware());//请求app.use(axiosRequest({baseURL:`${API_HOST}/audit`,}));//请求accessTokenapp.use(accessToken({appId:APP_ID,appSecret:APP_SECRET,}));//直接代理前端/api请求到后端,内部统一认证和参数设置app.use(apiProxy({prefix:'/api',}),);//koagraphql中间件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'}]应用服务器监听端口:${port}\n`,);});主要看graphql的配置其他都是koa常规的中间件配置returnschema;}主要是生成graphql需要的schematypeDefs,也就是graphql的类型定义,使用schema来约束类型,resolvers是解释器,也就是如何处理你定义的类型。例如:您的typeDefs类型自定义看起来像这样(它是一个字符串):consttypeDefs=`typeExportItem{applicantStatus:Stringapproving:[String]approvingMulitPassType:StringauditFlowId:StringbizName:StringcreatedAt:IntcreatedBy:IntcreatedByName:Stringdeleted:BooleanfinishTime:IntgroupId:StringgroupName:Stringid:StringshowApplyId:StringtemplateId:StringtemplateName:StringupdatedAt:IntupdatedBy:IntupdatedByName:StringauditFlowForwardType:StringuiConfig:StringtemplateInportDesc:String}inputList参数查询{IntpageSize:诠释finishedTimeBegin:IntfinishedTimeEnd:IntshowApplyId:StringauditFlowId:StringbizName:StringinitiatorEmployeeId:Intstatus:String}typeQuery{exportList(params:QueryExportListParams):[ExportItem]exportDetail(id:String):ExportItemy}`除了qlQuer关键字,其他由我们定义。Query是graphql中的顶级类型。除了Query,我们还经常用到Mutation。graphql规定所有查询定义必须放在Query中,所以修改操作,比如我们要添加,修改这些操作,放在mutation中。事实上,即使所有操作都放在查询或变异中,解析仍然会通过,但作为标准查询来编写查询,将操作写在变异中可能会更好。上面的定义是什么意思?我们先来分析一下,先看看Query里面:typeQuery{exportList(params:QueryExportListParams):[ExportItem]exportDetail(id:String):ExportItem}表示我们定义了两个查询名称exportList和exportDetail。exportDetail接受一个名为params的参数,params的类型为QueryExportListParams,返回一个数组,数组中的数据项类型为ExportItem。exportDetail接受一个id,其参数id类型为字符串,返回的数据类型为ExportItem。ExportItem是我们自己定义的数据类型。QueryExportListParams是自己定义的参数类型,参数是输入类型,必须使用input关键字定义。然后它定义了类型的实现位置,实现在解析器中。每个类型定义都必须与解析器中的解析器一一对应。所以解析器看起来像这样,参数,标题});返回资源数据;},exportDetail:async(_,{id})=>{constres=awaitctx.axios({url:`/applicant/byId/${id}`,method:'get',headers});返回资源数据;}}};解析器中有exportList和exportDetail的类型定义实现。在解析器中,它们的数据源可以在任何地方,可能是数据库,也可能是其他接口。我们这里是做中间层转发的。于是直接用axios转发给后端。然后这里获取并使用类型定义的参数。配置完成后,启动中间层服务。graphql查询生效后,会打开一个/graphql的路径界面。如果我们要使用graphql查询,我们会请求/graphql的路径。比如我们在前端请求graphql查询,我们会这样写:post('/graphql',{query:`queryExportList($params:QueryExportListParams){exportList(params:$params){id}}`,变量:{参数:{finishedTimeBegin:finishedTime?+moment(finishedTime[0]).startOf('day'):void0,finishedTimeEnd:finishedTime?+moment(finishedTime[1]).endOf('day'):void0,...rst,}}})QueryExportListParams是我们在中间层定义的参数类型,variables.params是我们传递给resolvers的参数值exportList(params:$params){id}这个表示在我们查询的返回数据中返回带id的列表返回一个列表,因为我们在定义类型的时候定义了查询需要返回一个列表:typeQuery{exportList(params:QueryExportListParams):[ExportItem]exportDetail(id:String):ExportItem}这里我们定义了exportList的返回类型是一个列表,类标签的类型是ExportItem,所以我们不需要区分是否查询就是取一个列表,返回类型是预先定义好的,我们要做的就是控制返回字段。只要是ExportItem类型中包含的字段,我们就可以定义是否带。比如我们上面的exportList(params:$params){id}这意味着我们只取id字段,返回的数据中就只有id字段。如果我们需要其他字段比如groupName字段,我们可以这样写exportList(params:$params){idgroupName}只要定义在我们的ExportItem类型中,我们就可以控制取不取。如果你查询的参数在服务器的graphql中没有定义,就会报错。graphql查询的另一个重要部分是指令。指令的加入会让bff层有更多的事情要做(放在下讲)