GraphQL 渐进学习 08-graphql-采用eggjs-服务端开发

GraphQL 渐进学习 08-graphql-采用eggjs-服务端开发

软件环境

  • eggjs 2.2.1

请注意当前的环境,老版本的 egg 可能配置有差异

目标

  • 创建 graphql 服务
  • 用户登录授权
  • 用户访问鉴权

代码

步骤

1 使用 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) {},
}

2 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 种代码
    • 1 common 全局类型定义
    • 2 query 查询代码
    • 3 mutation 更新操作代码
    • 4 业务 实现代码
      • 4.1 connector 连接数据服务
      • 4.2 resolver 类型实现
      • 4.3 schema 定义

3 编写 common 全局类型

  • 1 common/schema.graphql
scalar Date
  • 2 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;
  },
});
  • 3 common/resolver.js
module.exports = {
  Date: require('./scalars/date'), // eslint-disable-line
};

egg node 下还是用 require ,如果语言偏好用 import 会损失转换性能,不推荐

4 编写 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

dataloaderfacebook 出品的数据请求缓存 解决 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)
    },
  }
}

5 编写 query 查询

  • query/schema.graphql
type Query {
  # 用户登录
  user(
    # 用户名
    username: String!,
    # 密码
    password: String!
    ): User
  # 用户列表
  users: [User!]
}

6 编写 mutation 更新

  • mutation/schema.graphql
type Mutation {

  # User
  # 删除用户
  removeUser (
    # 用户ID
    id: ID!): User
}

7 开启 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 关掉

8 编写数据服务 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

9 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

参考

1 文章

2 组件

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容