GraphQL 漸進學習 08-graphql-采用eggjs-服務端開發(fā)
軟件環(huán)境
- eggjs 2.2.1
請注意當前的環(huán)境,老版本的
egg可能配置有差異
目標
- 創(chuàng)建 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,
// 是否加載開發(fā)者工具 graphiql, 默認開啟。路由同 router 字段。使用瀏覽器打開該可見。
graphiql: true,
// graphQL 路由前的攔截器
onPreGraphQL: function* (ctx) {},
// 開發(fā)工具 graphiQL 路由前的攔截器,建議用于做權限操作(如只提供開發(fā)者使用)
onPreGraphiQL: function* (ctx) {},
}
2 egg-graphql 代碼結構
.
├── graphql | graphql 代碼
│ ├── common | 通用類型定義
│ │ ├── resolver.js | 合并所有全局類型定義
│ │ ├── scalars | 自定義類型定義
│ │ │ └── date.js | 日期類型實現(xiàn)
│ │ └── schema.graphql | schema 定義
│ ├── mutation | 所有的更新
│ │ └── schema.graphql | schema 定義
│ ├── query | 所有的查詢
│ │ └── schema.graphql | schema 定義
│ └── user | 用戶業(yè)務
│ ├── connector.js | 連接數(shù)據(jù)服務
│ ├── resolver.js | 類型實現(xiàn)
│ └── schema.graphql | schema 定義
- graphql 目錄下,有 4 種代碼
- 1
common全局類型定義 - 2
query查詢代碼 - 3
mutation更新操作代碼 - 4
業(yè)務實現(xiàn)代碼- 4.1
connector連接數(shù)據(jù)服務 - 4.2
resolver類型實現(xiàn) - 4.3
schema定義
- 4.1
- 1
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 業(yè)務
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)
},
}
}
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 編寫數(shù)據(jù)服務 jwt 授權
- 配置
config/config.default.js
// easy-mock 模擬數(shù)據(jù)地址
config.baseURL =
'https://www.easy-mock.com/mock/59801fd8a1d30433d84f198c/example'
// jwt
config.jwt = {
jwtSecret: 'shared-secret',
jwtExpire: '14 days',
WhiteList: ['UserLogin']
}
- 數(shù)據(jù)請求封裝
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
)
}
}
- 用戶數(shù)據(jù)服務
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'
}
開啟內(nèi)置
bodyParser服務
- 編寫
middleware/auth.js
const jwt = require('jsonwebtoken')
module.exports = options => {
return async function auth(ctx, next) {
// 開啟 GraphiQL IDE 調(diào)試時,所有的請求放過
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驗證將失效,令牌數(shù)據(jù)是寫在request.header[authorization],這個調(diào)試IDE不支持設置header
參考
1 文章
- Koa2 手冊
- eggjs 手冊
- graphql-js/Authentication and Express Middleware
- Egg/GraphQL
- facebook/GraphQL
- apollo-graphql
- GraphQL API v4
- github/authenticating-with-graphql