前言
- 在上一篇文章中,已经搭建好了用
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.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' }) } ...
-
环境区分
- 当我们部署了第一个版本后,后续要迭代更新的话,我们开发中正常来说是不会直接连接正式环境的。除非一些特殊情况,例如排查线上问题、导出线上环境数据等。
- 所以区分连接不同环境还是必要的一个配置。
-
新建
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.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
-
修改
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
-
测试调整后的返回值
截止当前的目录结构:
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)
「点赞、收藏和评论」
❤️关注+点赞收藏+评论+分享❤️,手留余香,谢谢🙏大家。