请注意当前的环境,老版本的
egg
可能配置有差别
egg-graphql
npm i --save egg-graphql
/config/plugin.js
exports.graphql = { enable: true, package: 'egg-graphql' }
/config/config.default.js
// add your config here config.middleware = ['graphql'] // graphql config.graphql = { router: '/graphql', // 是否加载到 app 上,默认开启 app: true, // 是否加载到 agent 上,默认关闭 agent: false, // 是否加载开发者工具 graphiql, 默认开启。路由同 router 字段。使用浏览器打开该可见。 graphiql: true, // graphQL 路由前的拦截器 onPreGraphQL: function* (ctx) {}, // 开发工具 graphiQL 路由前的拦截器,建议用于作权限操做(如只提供开发者使用) onPreGraphiQL: function* (ctx) {}, }
egg-graphql
代码结构. ├── 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 定义
graphql 目录下,有 4 种代码vue
common
全局类型定义query
查询代码mutation
更新操做代码4 业务
实现代码node
connector
链接数据服务resolver
类型实现schema
定义common
全局类型common/schema.graphql
scalar Date
common/scalars/date.js
const { GraphQLScalarType } = require('graphql'); const { Kind } = require('graphql/language'); module.exports = new GraphQLScalarType({ name: 'Date', description: 'Date custom scalar type', parseValue(value) { return new Date(value); }, serialize(value) { return value.getTime(); }, parseLiteral(ast) { if (ast.kind === Kind.INT) { return parseInt(ast.value, 10); } return null; }, });
common/resolver.js
module.exports = { Date: require('./scalars/date'), // eslint-disable-line };
在egg node
下仍是用require
,若是语言偏好用import
会损失转换性能,不推荐
user
业务user/schema.graphql
# 用户 type User { # 流水号 id: ID! # 用户名 name: String! # token token: String }
user/connector.js
'use strict' const DataLoader = require('dataloader') class UserConnector { constructor(ctx) { this.ctx = ctx this.loader = new DataLoader(this.fetch.bind(this)) } fetch(id) { const user = this.ctx.service.user return new Promise(function(resolve, reject) { const users = user.findById(id) resolve(users) }) } fetchById(id) { return this.loader.load(id) } // 用户登陆 fetchByNamePassword(username, password) { let user = this.ctx.service.user.findByUsernamePassword(username, password) return user } // 用户列表 fetchAll() { let user = this.ctx.service.user.findAll() return user } // 用户删除 removeOne(id) { let user = this.ctx.service.user.removeUser(id) return user } } module.exports = UserConnector
dataloader
是N+1
问题
user/resolver.js
'use strict' module.exports = { Query: { user(root, {username, password}, ctx) { return ctx.connector.user.fetchByNamePassword(username, password) }, users(root, {}, ctx) { return ctx.connector.user.fetchAll() } }, Mutation: { removeUser(root, { id }, ctx) { return ctx.connector.user.removeOne(id) }, } }
query
查询query/schema.graphql
type Query { # 用户登陆 user( # 用户名 username: String!, # 密码 password: String! ): User # 用户列表 users: [User!] }
mutation
更新mutation/schema.graphql
type Mutation { # User # 删除用户 removeUser ( # 用户ID id: ID!): User }
cros
跨域访问config/plugin.js
exports.cors = { enable: true, package: 'egg-cors' }
config/config.default.js
// cors config.cors = { origin: '*', allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH' } // csrf config.security = { csrf: { ignore: () => true } }
做为API
服务,顺手把csrf
关掉
jwt
受权config/config.default.js
// easy-mock 模拟数据地址 config.baseURL = 'https://www.easy-mock.com/mock/59801fd8a1d30433d84f198c/example' // jwt config.jwt = { jwtSecret: 'shared-secret', jwtExpire: '14 days', WhiteList: ['UserLogin'] }
util/request.js
'use strict' const _options = { dataType: 'json', timeout: 30000 } module.exports = { createAPI: (_this, url, method, data) => { let options = { ..._options, method, data } return _this.ctx.curl( `${_this.config.baseURL}${url}`, options ) } }
service/user.js
const Service = require('egg').Service const {createAPI} = require('../util/request') const jwt = require('jsonwebtoken') class UserService extends Service { // 用户详情 async findById(id) { const result = await createAPI(this, '/user', 'get', { id }) return result.data } // 用户列表 async findAll() { const result = await createAPI(this, '/user/all', 'get', {}) return result.data } // 用户登陆、jwt token async findByUsernamePassword(username, password) { const result = await createAPI(this, '/user/login', 'post', { username, password }) let user = result.data user.token = jwt.sign({uid: user.id}, this.config.jwt.jwtSecret, { expiresIn: this.config.jwt.jwtExpire }) return user } // 用户删除 async removeUser(id) { const result = await createAPI(this, '/user', 'delete', { id }) return result.data } } module.exports = UserService
token
验证中间件config/config.default.js
config.middleware = ['auth', 'graphql'] config.bodyParser = { enable: true, jsonLimit: '10mb' }
开启内置
bodyParser
服务
middleware/auth.js
const jwt = require('jsonwebtoken') module.exports = options => { return async function auth(ctx, next) { // 开启 GraphiQL IDE 调试时,全部的请求放过 if (ctx.app.config.graphql.graphiql) { await next() return } const body = ctx.request.body if (body.operationName !== 'UserLogin') { let token = ctx.request.header['authorization'] if (token === undefined) { ctx.body = {message: '令牌为空,请登录获取!'} ctx.status = 401 return } token = token.replace(/^Bearer\s/, '') try { let decoded = jwt.verify(token, ctx.app.config.jwt.jwtSecret, { expiresIn: ctx.app.config.jwt.jwtExpire }) await next() } catch (err) { ctx.body = {message: '访问令牌鉴权无效,请从新登录获取!'} ctx.status = 401 } } else { await next() } } }
若是开启GraphiQL IDE
工具,token
验证将失效,令牌数据是写在request.header[authorization]
,这个调试IDE
不支持设置header