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 種代碼vue

    • 1 common 全局類型定義
    • 2 query 查詢代碼
    • 3 mutation 更新操做代碼
    • 4 業務 實現代碼node

      • 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 組件

相關文章
相關標籤/搜索