【0-1搭建网站】(二)GraphQL+环境区分+path添加prefix+统一返回数据格式

前言

  • 上一篇文章中,已经搭建好了用 Koa 框架来写后端的一些基础配置。并在文末提到了一些后续会接入的功能。
    1. 支持 GraphQL
    2. 环境区分
    3. 全局路由 prefix
    4. 返回 JSON 数据
    5. ...
  • 本篇文章就针对上述提到的功能对配置进行完善。

后端配置完善

支持 GraphQL

  • 在之前我们已经配置了 Koa/router,那为什么我还要继续加入 GraphQL 的配置呢?
  • 首先我们要知道 GraphQL 能做什么:
    1. 精确获取所需数据 : 在 GraphQL 中,客户端可以精确地指定需要从服务器获取的数据结构,而不必依赖于服务器端定义的固定端点。这意味着客户端可以请求精确匹配其需求的数据,避免了过度获取或不足的数据。
    2. 单一端点 :在 GraphQL 中,通常只有一个端点用于所有数据查询。这消除了在 RESTful API 中多个端点可能导致的过度网络请求和端点的混乱。
    3. 强类型系统GraphQL 使用强类型系统来定义数据模型,这使得客户端和服务器之间的通信更加明确和可靠。每个字段都有明确的类型,可以帮助开发人员在开发过程中更早地发现潜在的错误。
    4. 实时数据GraphQL 支持实时数据查询,使得客户端可以订阅特定的数据变更,并在数据发生变化时实时接收更新。
    5. 自我描述性GraphQL 具有自我描述性,客户端可以通过 GraphQL 的查询语言轻松地了解可用的数据结构和字段。
  • 因为有这些特性,所以在很多场景下,我们可以使用 GraphQL 接口来做,例如:
    1. 接口中某些字段是前端不需要的,就不需要在接口中去写哪些需要返回、哪些不需要返回;
    2. 前端需要接口返回增加某个已有的字段,后端又需要改接口重发服务;
    3. 统一数据类型,前端明确知道后端返回的数据类型;
    4. 对其他接口可以内部聚合;
    5. ...。
  • 使用 GraphQL
    1. 安装 GraphQL 相关的依赖

      js 复制代码
      // 3.13.0、16.8.1
      npm install apollo-server-koa graphql
    2. 修改 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}`),
      +   )
      + })
    3. 启动项目并使用 PostmanPOST 请求访问 /gql/hello 路径。请求的 Body 选择 raw 并选择格式为 JSON,填写以下数据:

      js 复制代码
      {
        "query": " query{ hello }"
      }

模块化 GraphQL 相关的文件

  • 后面可能会添加很多的 GraphQL 接口,拆分 GraphQL 不同的模块到不同的文件。
    1. 新建 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"
          },
          ...
        }
    2. 拆分到不同文件

      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
    3. 修改 src/index.js

      js 复制代码
      // 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' })
      }
      ...

环境区分

  • 当我们部署了第一个版本后,后续要迭代更新的话,我们开发中正常来说是不会直接连接正式环境的。除非一些特殊情况,例如排查线上问题、导出线上环境数据等。
  • 所以区分连接不同环境还是必要的一个配置。
  1. 新建 config 相关目录

    • 根目录下新建 config 目录用于存放不同环境的配置
      • config.dev.js:开发环境相关的配置
      • config.test.js:测试环境相关的配置
      • config.prod.js:线上环境相关的配置
      • index.js:获取当前环境配置
  2. 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",
        ...
      },
      ...
    }
  3. 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 数据

  • 在写接口的时候,为了可读性以及方便前端处理等。需要将所有接口返回的格式统一。
  1. 定义一些常用的状态返回类型,例如: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,
    }
  2. 添加全局异常拦截统一处理并返回

    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
    +   }
    + })
    ...
  3. 调整目录结构,创建 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
  4. 修改 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))
    ...
  5. 修改 src/utils/authenticate.js

    js 复制代码
    // 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
  6. 修改 src/routes/login.js

    js 复制代码
    // 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
  7. 测试调整后的返回值

截止当前的目录结构:

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 上。如果需要这个版本的,也可以私信我,私信可能回复不及时,还请理解。
  • 有什么问题或想要在项目中增加什么配置欢迎大家评论区留言交流。

往期精彩

「点赞、收藏和评论」

❤️关注+点赞收藏+评论+分享❤️,手留余香,谢谢🙏大家。

相关推荐
Q_192849990624 分钟前
基于Spring Boot的个人健康管理系统
java·spring boot·后端
liutaiyi824 分钟前
Redis可视化工具 RDM mac安装使用
redis·后端·macos
Q_192849990631 分钟前
基于Springcloud的智能社区服务系统
后端·spring·spring cloud
xiaocaibao77734 分钟前
Java语言的网络编程
开发语言·后端·golang
会说法语的猪2 小时前
springboot实现图片上传、下载功能
java·spring boot·后端
凡人的AI工具箱2 小时前
每天40分玩转Django:实操多语言博客
人工智能·后端·python·django·sqlite
Cachel wood2 小时前
Django REST framework (DRF)中的api_view和APIView权限控制
javascript·vue.js·后端·python·ui·django·前端框架
m0_748234082 小时前
Spring Boot教程之三十一:入门 Web
前端·spring boot·后端
想成为高手4992 小时前
国产之光--仓颉编程语言的实战案例分析
后端
编码浪子3 小时前
构建一个rust生产应用读书笔记7-确认邮件2
开发语言·后端·rust