基于 egg.js 构建 graphql api 服务
登录快速注册
基于 egg.js 构建 graphql api 服务
Egg.js 简介:https://eggjs.org/zh-cn/index.html
生成项目
通过骨架快速初始化,该操作会生成一个极简版的示例,完整示例参见:eggjs/examples/hackernews-async-ts
代码语言:javascript复制$ mkdir egg && cd egg
$ npm init egg --type=ts
$ npm i
启动项目
代码语言:javascript复制$ npm run dev
$ open http://localhost:7001
安装插件
@switchdog/egg-graphql
插件机制是egg的一大特色,由于我们基于 TypeScript ,所以选择了支持 TS 的包。
代码语言:javascript复制npm i -s @switchdog/egg-graphql
开启插件
在 config/plugin.ts
下告诉 egg 开启哪些插件:
graphql: {
enable: true,
package: '@switchdog/egg-graphql',
},
配置插件
通常插件都会有一些配置项,在/config/config.default.ts
中配置即可:
config.graphql = {
router: '/graphql',
// 是否加载到 app 上,默认开启
app: true,
// 是否加载到 agent 上,默认关闭
agent: false,
// 是否加载开发者工具 graphiql, 默认开启。路由同 router 字段。使用浏览器打开该可见。
graphiql: true,
apolloServerOptions: {
tracing: true, // 设置为true时,以Apollo跟踪格式收集和公开跟踪数据
debug: true, // 一个布尔值,如果发生执行错误,它将打印其他调试日志记录
},
};
在中间件中开启 graphql
config.middleware = [ 'graphql' ];
配置完成之后,每个落到 /graphql
的请求都会触发 GraphQL Schema 的查询。
代码结构
graphql 目录下有 4 种代码,分别是:
common
全局类型定义
query
查询代码
mutation
更新操作代码
resolver
业务实现代码
.
├── graphql | graphql 代码
│ ├── common | 通用类型定义
│ │ ├── resolver.js | 合并所有全局类型定义
│ │ ├── scalars | 自定义类型定义
│ │ │ └── date.js | 日期类型实现
│ │ └── schema.graphql | schema 定义
│ ├── mutation | 所有的更新
│ │ └── schema.graphql | schema 定义
│ ├── query | 所有的查询
│ │ └── schema.graphql | schema 定义
│ └── user | 用户业务
│ ├── connector.js | 连接数据服务
│ ├── resolver.js | 类型实现
│ └── schema.graphql | schema 定义
CORS
CORS
,常被大家称之为跨域问题,准确的叫法是跨域资源共享**(CORS,Cross-origin resource sharing)**,是W3C标准,是一种机制,它使用额外的HTTP头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
安装 egg-cors
代码语言:javascript复制npm i egg-cors --save
开启 /config/plugin.ts
:
cors: {
enable: true,
package: 'egg-cors',
},
配置 /config/config.default.ts
:
config.cors = {
origin: '*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
};
CSRF
CSRF(Cross-site request forgery)跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。
使用 graphql
或 rest
端点时,实际上不必担心使用 CSRF
保护。对服务的请求应该是无状态的,并且不真正依赖Cookie或会话数据。
关闭 /config/config.default.ts
:
config.security = {
csrf: {
ignore: () => true,
},
};
实现一个简单的 GraphQL API
编写 hello 业务
schema.graphql
graphql 自带一组默认标量类型,包括 Int
,Float
,String
,Boolean
,ID
。在定义字段时需要注明类型,这也是 graphql 的特点之一,是支持强类型的。如果非空,就在类型后面跟上一个!号。graphql 还包括枚举类型,列表和自定义类型。
type Hello {
id: ID!
name: String!
}
connector
编写完 schema
之后,graphql 知道有哪些数据了,但他还需要知道 “如何去取”, connector
的角色就在于此。 connector
的职责就是 “取数”, 他既可以调用 rpc 接口取数,又可以调用内置的 orm 插件去取数,还可以直接调用 egg 的 service
。
export default class HelloConnector {
hellos() {
return [
{
id: 1,
name: 'Jack',
},
{
id: 2,
name: 'Lucy',
},
];
}
}
resolver
resolve.js是数据类型的具体实现,依赖connector.js完成。其实 resolver 非常简单,就是针对你暴露的查询接口,调用相应的connector去取数即可,如下:
代码语言:javascript复制export default {
Query: {
hellos(_root: any, {}, { connector }) {
return connector.hello.hellos();
},
},
};
定义 Query
新建一个 query
目录创建 schema.graphql
文件,也可以直接在各个模块下的 schema.graphql
文件中定义
type Query {
hellos: [Hello!]
}
[Hello!] 可以理解为 [{id: 1, name: 'jack'}, {id: 2, name: 'praise'}] Hello! 可以理解为 {id: 1, name: 'jack'}
项目启动
代码语言:javascript复制npm run dev
在浏览器中输入 http://127.0.0.1:7001/graphql
出现如下界面说明已经 graphql
服务已经成功运行。
完成一次查询
代码语言:javascript复制# Write your query or mutation here
{
hellos {
id
name
}
}
执行得到
代码语言:javascript复制{
"data": {
"hellos": [
{
"id": "1",
"name": "Jack"
},
{
"id": "2",
"name": "Lucy"
}
]
}
}
设置别名
代码语言:javascript复制# Write your query or mutation here
{
users: hellos {
id
name
}
}
得到
代码语言:javascript复制{
"data": {
"users": [
{
"id": "1",
"name": "Jack"
},
{
"id": "2",
"name": "Lucy"
}
]
}
}
请求流程
通过上方的例子我们可以看出客户端发送请求会被 graphql
解析,根据映射关系找到对应的 resolver
。路由将数据传递到对应的 resolver
,resolver
去调用对应的 connector
进行处理,connector
再调用 service
进行数据库处理。
从MongoDB查询数据
安装 egg-mongoose
代码语言:javascript复制yarn add egg-mongoose
配置
代码语言:javascript复制// config/plugin.ts
exports.mongoose = {
enable: true,
package: 'egg-mongoose',
};
// config/config.default.ts
exports.mongoose = {
client: {
url: 'mongodb://127.0.0.1/example',
options: {},
// mongoose global plugins, expected a function or an array of function and options
plugins: [createdPlugin, [updatedPlugin, pluginOptions]],
},
};
Schema
代码语言:javascript复制// app/model/user.ts
module.exports = (app: any) => {
const mongoose = app.mongoose;
const Schema = mongoose.Schema;
const UserSchema = new Schema({
open_id: String!,
info: {
nick: String!,
name: String!,
gender: Number!,
city: String!,
province: String!,
country: String!,
avatar: String!,
phone: String!
},
address: [
{
name: String!,
phone: String!,
addr: String!,
default: Boolean,
sign: String!
}
],
carts: [
{
gid: Number!,
num: Number!
}
],
favorite: {
goods: [String],
content: [String]
},
browse: [String],
createdAt: Number,
updatedAt: Number
});
return mongoose.model('User', UserSchema);
}
Controller
代码语言:javascript复制// app/model/user.ts
import { Controller } from 'egg';
export default class UserController extends Controller {
public async index() {
const { ctx } = this;
ctx.body = await ctx.model.User.find({});
}
}
Router
代码语言:javascript复制// app/router.ts
import { Application } from 'egg';
export default (app: Application) => {
const { controller, router } = app;
router.get('/user', controller.user.index);
};