GraphQL is an open-source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data.[2] GraphQL was developed internally by Facebook in 2012 before being publicly released in 2015. GraphQL - Wikipedia
简单翻译一下: GraphQL是一个由Facebook在2012年的内部项目孵化并且于2015年正式发布的一个文档型API
GraphQL里面的所有操作归为两类, 一个是query
, 一个是mutation
Query可以简单理解为 rest
的get
请求, 就是一个获取资源的请求. 都需要一段schema来进行描述你想要的数据. 比如这里我们定义了一个方法, 方法是一个query类型的, 刚刚介绍过了GraphQL是一个描述型的API, 那么我们也可以描述一下它.
通过类型是一个 必填的String
的 locale
变量获取地址信息, 返回的数据有 country, province, cities, 其中 cities是由 city, districts构成的.
String 是GraphQL的类型之一
query Address($locale: String!) {
address(locale: $locale) {
country
province
cities {
city
districts
}
}
}
复制代码
我们在前端收到的数据大概是这样
{
"address": {
"country": "China"
"province": "JiangSu"
"cities": [{
"city":"NanJing"
"districts": "XiCheng"
}]
}
}
复制代码
上面这个例子, 在另一个页面也许我只需要country
信息就可以了, 那么我的schema可以写成
query Address($locale: String!) {
address(locale: $locale) {
country
}
}
复制代码
得到的数据会是
{
"address": {
"country": "China"
}
}
复制代码
我在A页面需要country信息, 在B页面需要 country和province信息, 在C页面再多给我返回个cities 以前遇到这种需求, 后端至少得写3个API用来返回,当然前端也得写3个请求去接收, 要么就是直接返回所有数据, 让前端在每个页面都去调用拿到所有数据(在这里就是 country+province+cities), 然后再在不同页面去展示不同的内容就可以了.
但是这样带来了几个坏处:
使用GraphQL就可以避免上述问题, 甚至你也不需要写3个schema, 善用GraphQL Fragments 和 GraphQL Directives可以帮你解决重复问题.
那么GraphQL对前端来说有没有弊端或者麻烦的地方呢? 当然是有的, 这里我们说两个问题.
如果使用过GraphQL的就会知道, 它默认使用的是POST
请求, 好处就是, 不论你schema多大, 都可以发送给后端. 但是不足的地方就在于, 没有办法使用http cache, HTTP 缓存 - HTTP | MDN /虽然 HTTP 缓存不是必须的,但重用缓存的资源通常是必要的。然而常见的 HTTP 缓存只能存储 GET
响应,对于其他类型的响应则无能为力。/ 当然, 我们可以将默认的请求类型改为GET
, 但是当schema过大的时候 ,就会出问题了.
我们在请求的时候, 可以从http请求的Headers
里面看到我们的query
, 里面有完整的schema,
那么有没有解决这两点的办法呢? 我这里提供一种解决思路, 就是 persisted query
A persisted query is an ID or hash that can be sent to the server instead of the entire GraphQL query string. Automatic persisted queries - Apollo Server - Apollo GraphQL Docs
简单翻译一下就是, 一个短dash代替一个超长的graphql schema
已经有很多合适的前/后端框架来使用, 我这里说一个前端框架 GitHub - apollographql/apollo-link-persisted-queries: Persisted Query support with Apollo Link
它里面已经有介绍如何使用, 以及工作原理了: How it works
刚刚我们介绍了, 如何在使用过程中生成. 但是如何预生成呢? 也就是, 在前端部署的过程中或者是在访问页面之前就已经生成好.
当然, 还是要问为什么要这么做. 简单来说, 还是为了更好的优化, 试想一下, 如果我已经可以将一个大量访问的schema的变动提前缓存起来, 并且准备好这份数据, 当前端访问的时候, 我直接将这份缓存好的数据扔给前端, 而不是再在后台重新查询拼接, 效率是不是会提高很多呢? 这样的设想完成起来, 需要解决一个最主要的问题, 后端如何在前端没有访问的时候提前预知schema?
我们这里采用的是, 在前端部署的过程中通过已有schema在node运行生成一段querystring, 通过hash后发给后端, 后端将这段query持久化起来
具体的做法是:
__typename
(这一步可能不需要, 因为如果你的请求设置了不带__typename, 就没必要了)贴上我的实现代码, 方便直接使用
// parseSchemaToJson
const { resolve, dirname } = require('path')
const { readFileSync } = require('fs')
const { parse: graphqlParse, print: graphqlPrint, Source, visit } = require('graphql')
const TYPENAME_FIELD = {
kind: 'Field',
name: {
kind: 'Name',
value: '__typename',
},
};
function isField(selection) {
return selection.kind === 'Field'
}
function addTypenameToDocument(doc) {
return visit(doc, {
SelectionSet: {
enter(node, _key, parent) {
// Don't add __typename to OperationDefinitions.
if (parent && parent.kind === 'OperationDefinition') {
return
}
// No changes if no selections.
const { selections } = node
if (!selections) {
return
}
// If selections already have a __typename, or are part of an
// introspection query, do nothing.
const skip = selections.some((selection) => {
return (
isField(selection) &&
(selection.name.value === '__typename' || selection.name.value.lastIndexOf('__', 0) === 0)
)
})
if (skip) {
return
}
// If this SelectionSet is @export-ed as an input variable, it should
// not have a __typename field (see issue #4691).
const field = parent
if (isField(field) && field.directives && field.directives.some((d) => d.name.value === 'export')) {
return
}
// Create and return a new SelectionSet with a __typename Field.
return {
...node,
selections: [...selections, TYPENAME_FIELD],
}
},
},
})
}
module.exports = function loadGql(filePath) {
if (!filePath) return null
try {
const source = readFileSync(filePath, 'utf8')
if (!source) return null
const document = loadSource(source, filePath)
return graphqlPrint(addTypenameToDocument(document))
} catch (err) {
console.log(err)
return null
}
}
function loadSource(source, filePath) {
let document = graphqlParse(new Source(source, 'GraphQL/file'))
document = extractImports(source, document, filePath)
return document
}
function extractImports(source, document, filePath) {
const lines = source.split(/(\r\n|\r|\n)/)
const imports = []
lines.forEach((line) => {
// Find lines that match syntax with `#import "<file>"`
if (line[0] !== '#') {
return
}
const comment = line.slice(1).split(' ')
if (comment[0] !== 'import') {
return
}
const filePathMatch = comment[1] && comment[1].match(/^[\"\'](.+)[\"\']/)
if (!filePathMatch || !filePathMatch.length) {
throw new Error('#import statement must specify a quoted file path')
}
const itemPath = resolve(dirname(filePath), filePathMatch[1])
imports.push(itemPath)
})
const contents = imports.map((path) => [readFileSync(path, 'utf8'), path])
const nodes = contents.map(([content, fileContext]) => {
return loadSource(content, fileContext)
})
const fragmentDefinitions = nodes.reduce((defs, node) => {
defs.push(...node.definitions)
return defs
}, [])
return visit(document, {
enter(node, key, parent, path, ancestors) {
if (node.kind === 'Document') {
return {
definitions: [...fragmentDefinitions, ...node.definitions],
kind: 'Document',
}
}
return node
},
})
}
复制代码
const crypto = require('crypto')
const loadGql = require('./parseSchemaToJson')
const SECRET_KEY = 'TRYITYOURSELF'
const queryStrings = loadGql('yourGraphqlFile.graphql')
const sha256Hash = crypto
.createHmac('sha256', SECRET_KEY)
.update(queryStrings)
.digest('hex')
// Then send to backend
复制代码
上面的parseSchemaToJson
里面是处理的fragment的递归的情况. 比如你的fragment里面还有fragment构成的部分, 如果你只有一层fragment构成, 那么可以精简一部分代码, 参考这里apollo-client/transform.ts at master · apollographql/apollo-client · GitHub, 但是使用上述代码也是没有问题的.