前言
- 在上一篇文章中,已经搭建好了用 Koa框架来写后端的一些基础配置。并在文末提到了一些后续会接入的功能。- 支持 GraphQL
- 环境区分
- 全局路由 prefix
- 返回 JSON数据
- ...
 
- 支持 
- 本篇文章就针对上述提到的功能对配置进行完善。
后端配置完善
支持 GraphQL
- 在之前我们已经配置了 Koa/router,那为什么我还要继续加入GraphQL的配置呢?
- 首先我们要知道 GraphQL能做什么:- 精确获取所需数据 : 在 GraphQL中,客户端可以精确地指定需要从服务器获取的数据结构,而不必依赖于服务器端定义的固定端点。这意味着客户端可以请求精确匹配其需求的数据,避免了过度获取或不足的数据。
- 单一端点 :在 GraphQL中,通常只有一个端点用于所有数据查询。这消除了在RESTful API中多个端点可能导致的过度网络请求和端点的混乱。
- 强类型系统 :GraphQL使用强类型系统来定义数据模型,这使得客户端和服务器之间的通信更加明确和可靠。每个字段都有明确的类型,可以帮助开发人员在开发过程中更早地发现潜在的错误。
- 实时数据 :GraphQL支持实时数据查询,使得客户端可以订阅特定的数据变更,并在数据发生变化时实时接收更新。
- 自我描述性 :GraphQL具有自我描述性,客户端可以通过GraphQL的查询语言轻松地了解可用的数据结构和字段。
 
- 精确获取所需数据 : 在 
- 因为有这些特性,所以在很多场景下,我们可以使用 GraphQL接口来做,例如:- 接口中某些字段是前端不需要的,就不需要在接口中去写哪些需要返回、哪些不需要返回;
- 前端需要接口返回增加某个已有的字段,后端又需要改接口重发服务;
- 统一数据类型,前端明确知道后端返回的数据类型;
- 对其他接口可以内部聚合;
- ...。
 
- 使用 GraphQL- 
安装 GraphQL相关的依赖js// 3.13.0、16.8.1 npm install apollo-server-koa graphql
- 
修改 src/index.js文件js// src/index.js + const { ApolloServer, gql } = require('apollo-server-koa') const app = new Koa() ... const app = new Koa() app.use(bodyParser()) + // 定义 GraphQL schema + const typeDefs = gql` + type Query { + hello: String + } + ` + const resolvers = { // 解析器函数来实现 GraphQL schema + Query: { + hello: () => 'Hello world!', + }, + } + const server = new ApolloServer({ typeDefs, resolvers }) + async function startServers() { + await Promise.all([server.start()]) + server.applyMiddleware({ app, path: '/gql/hello' }) + } + startServers().then(() => { + const PORT = process.env.PORT || 3000 + app.listen(PORT, () => + console.log(`Server running at http://localhost:${PORT}`), + ) + })
- 
启动项目并使用 Postman用POST请求访问/gql/hello路径。请求的Body选择raw并选择格式为JSON,填写以下数据:js{ "query": " query{ hello }" } 
 
- 
模块化 GraphQL 相关的文件
- 后面可能会添加很多的 GraphQL接口,拆分GraphQL不同的模块到不同的文件。- 
新建 GraphQL相关目录- 
src目录下新建resolvers目录用于存放解析器函数
- 
src目录下新建typeDefs目录用于存放schema
- 
src目录下新建servers目录用于存放resolver、typeDefs对应server
- 
在 package.json路径别名中添加对应的配置js// package.json { ..., "_moduleAliases": { ..., + "@resolvers": "src/resolvers", + "@servers": "src/servers", + "@typeDefs": "src/typeDefs" }, ... }
 
- 
- 
拆分到不同文件 js// src/resolvers/hello.js const resolvers = { Query: { hello: () => 'Hello world!', }, } module.exports = resolvers // src/typeDefs/hello.js const { gql } = require('apollo-server-koa') const typeDefs = gql` type Query { hello: String } ` module.exports = typeDefs // src/servers/hello.js const { ApolloServer } = require('apollo-server-koa') const typeDefs = require('@typeDefs/user') const resolvers = require('@resolvers/user') const server = new ApolloServer({ typeDefs, resolvers }) module.exports = typeDefs
- 
修改 src/index.jsjs// src/index.js ... - const { ApolloServer, gql } = require('apollo-server-koa') + const gqlHelloServer = require('@servers/hello') async function startServers() { - await Promise.all([server.start()]) - server.applyMiddleware({ app, path: '/gql/hello' }) + await Promise.all([gqlHelloServer.start()]) + gqlHelloServer.applyMiddleware({ app, path: '/gql/user' }) } ...
 
- 
环境区分
- 当我们部署了第一个版本后,后续要迭代更新的话,我们开发中正常来说是不会直接连接正式环境的。除非一些特殊情况,例如排查线上问题、导出线上环境数据等。
- 所以区分连接不同环境还是必要的一个配置。
- 
新建 config相关目录- 根目录下新建 config目录用于存放不同环境的配置- config.dev.js:开发环境相关的配置
- config.test.js:测试环境相关的配置
- config.prod.js:线上环境相关的配置
- index.js:获取当前环境配置
 
 
- 根目录下新建 
- 
在 package.json中修改启动命令并添加路径别名- 注意:在 Windows环境下NODE_ENV=dev&&值和&&之间不要有空格,不然获取到的process.env.NODE_ENV后面会多一个空格。
 js// package.json { ..., "scripts": { - "start": "nodemon ./src/index.js", + "dev": "set NODE_ENV=dev&& nodemon ./src/index.js", + "test": "set NODE_ENV=test&& nodemon ./src/index.js", + "prod": "set NODE_ENV=prod&& nodemon ./src/index.js" }, "_moduleAliases": { + "@config": "config", ... }, ... }
- 注意:在 
- 
在 src/index.js中引入js... + const config = require('@config') startServers().then(() => { const PORT = config.PORT || 3000 app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`), ) })
全局路由 prefix
- 
平常在调后端的接口时,发现接口的地址开头都是一样的,例如: js/api/user/info /api/manage/setInfo ...
- 
前面的 /api就是所有接口都有的,那我们在写接口的时候是要每个接口都去加/api的前缀吗?
- 
Koa中我们可以使用中间件给所有请求的path添加自定义的prefix,或者替换请求的path中约定的prefix,如下:js... const app = new Koa() + const prefixMiddleware = (prefix) => { + return async (ctx, next) => { + ctx.request.path = ctx.request.path.replace(prefix, '') + await next() + } + } + app.use(prefixMiddleware(process.env.GLOBAL_ROUTER_PREFIX)) app.use(bodyParser())
统一返回 JSON 数据
- 在写接口的时候,为了可读性以及方便前端处理等。需要将所有接口返回的格式统一。
- 
定义一些常用的状态返回类型,例如: 200、400、401、500等。js// src/constants/response.js const SUCCESS = { code: 200, data: null, message: 'success', } const PARAM_ERROR = { code: 400, data: null, message: 'param error', } const UNAUTHORIZED = { code: 401, data: null, message: 'Invalid token', } const SERVER_ERROR = { code: 500, data: null, message: 'server error', } module.exports = { SUCCESS, PARAM_ERROR, UNAUTHORIZED, SERVER_ERROR, }
- 
添加全局异常拦截统一处理并返回 js// src/index.js ... const app = new Koa() + app.use(async (ctx, next) => { + try { + await next() + } catch (err) { + ctx.status = err.code || 500 + ctx.body = err + } + }) ...
- 
调整目录结构,创建 middlewares目录存放自定义中间件js// src/middlewares/pathPrefix.js const pathPrefix = (prefix) => { return async (ctx, next) => { ctx.type = 'application/json' // 设置响应内容类型为 JSON ctx.request.path = ctx.request.path.replace(prefix, '') await next() } } module.exports = pathPrefix // src/middlewares/catchDeal.js const catchDeal = () => { return async (ctx, next) => { try { await next() } catch (err) { ctx.status = err.code || 500 // 设置响应状态码 ctx.body = err // 设置响应体 } } } module.exports = catchDeal // src/middlewares/index.js const catchDeal = require('./catchDeal') const pathPrefix = require('./pathPrefix') const middlewareInit = (app) => { app.use(catchDeal()) app.use(pathPrefix(process.env.GLOBAL_ROUTER_PREFIX)) } module.exports = middlewareInit
- 
修改 src/index.js文件js// src/index.js ... + const middlewareInit = require('@middlewares') const app = new Koa() + middlewareInit(app) - app.use(async (ctx, next) => { - try { - await next() - } catch (err) { - ctx.status = err.code || 500 - ctx.body = err - } - }) - const prefixMiddleware = (prefix) => { - return async (ctx, next) => { - ctx.request.path = ctx.request.path.replace(prefix, '') - await next() - } - } - app.use(prefixMiddleware(process.env.GLOBAL_ROUTER_PREFIX)) ...
- 
修改 src/utils/authenticate.jsjs// src/utils/authenticate.js const { verifyToken } = require('@utils/tokenOpt') + const { UNAUTHORIZED, PARAM_ERROR } = require('@constants/response') const authenticate = async (ctx, next) => { const { authorization } = ctx.headers if (!authorization) { - ctx.throw(401, 'Authentication token is required') + ctx.throw({ + ...PARAM_ERROR, + message: 'Authentication token is required', + }) } if (authorization && authorization.startsWith('Bearer ')) { try { ... } catch (error) { - ctx.throw(401, 'Invalid token') + ctx.throw(UNAUTHORIZED) } await next() } else { - ctx.throw(401, 'Invalid token') + ctx.throw(UNAUTHORIZED) } } module.exports = authenticate
- 
修改 src/routes/login.jsjs// src/routes/login.js const Router = require('koa-router') const { createToken } = require('@utils/tokenOpt') + const { SUCCESS, PARAM_ERROR } = require('@constants/response') ... router.post('/login', async (ctx) => { ... if (user) { const token = createToken(user) - ctx.body = token + ctx.body = { ...SUCCESS, data: token } } else { - ctx.status = 401 - ctx.body = { error: 'Invalid username or password' } + ctx.throw({ + ...PARAM_ERROR, + message: 'Invalid username or password', + }) } }) module.exports = router
- 
测试调整后的返回值      
截止当前的目录结构:
            
            
              js
              
              
            
          
          |- config
  |- config.dev.js
  |- config.test.js
  |- config.prod.js
  |- index.js
|- node_modules
|- src
  |- constants
    |- common.js
    |- response.js
  |- middlewares
    |- catchDeal.js
    |- pathPrefix.js
    |- index.js
  |- resolvers
    |- hello.js
  |- routes
    |- login.js
    |- user.js
  |- servers
    |- hello.js
  |- typeDefs
    |- hello.js
  |- index.js
  |- utils
    |- authenticate.js
    |- createOpt.js
|- .env
|- .eslint.js
|- .prettierrc
|- package-lock.json
|- package.json后续计划
- 数据库
- ...
最后
- 下一次更新我会拿单独的一篇文章来讲数据库相关的内容,敬请期待。
- 后端框架的源码我将在所有需要用到的配置都搭建好之后,放在我的 github 上。如果需要这个版本的,也可以私信我,私信可能回复不及时,还请理解。
- 有什么问题或想要在项目中增加什么配置欢迎大家评论区留言交流。
往期精彩
- 【0-1搭建网站】(一)Koa+koa/router+module-alias+eslint+prettier
- Node.js 版本管理工具 n 最全使用手册
- 磕磕绊绊的 4 年前端er,一次含泪总结
- 前端还不会 Nginx 吗?快来学起来
- 金九前端面试总结!
- 从0搭建Vite + Vue3 + Element-Plus + Vue-Router + ESLint + husky + lint-staged
- 「前端进阶」JavaScript手写方法/使用技巧自查
- 公众号打开小程序最佳解决方案(Vue)
「点赞、收藏和评论」
❤️关注+点赞收藏+评论+分享❤️,手留余香,谢谢🙏大家。