(这是一个系列文章:预计会有三期,第一期会以同构构建先后端应用为主,第二期会以 GraphQL 和 MySQL 为主,第三期会以 Docker 配合线上部署报警为主)javascript
做者: 赵玮龙html
重要声明: 今后再也不以 AMC 团队名称发布文章,缘由不详述,全部文章和后续文章将由我的维护,若是你对个人文章感兴趣,也请继续支持和关注,再次声明-我的仍是会保持更新和最新以及前沿技术的踩坑,不只仅局限于前端领域!前端
可能你也发现题目出现了1/2,由于若是介绍 GraphQL 和 MySQL 一块儿,容易忽略掉中间的不少细节过程,还有篇幅自己问题,我准备把他们拆开来讲,我仔细想了下,我先从前端的角度看 GraphQL 如何耦合到咱们的项目中,看看它能为咱们带来什么而且解决了什么问题(虽然拆开说,篇幅仍是很是长的,但愿各位感兴趣的同窗能够先点赞保存~~~慢慢看),再而后咱们看看 node 端如何从数据库层面支持 GraphQL,仍是保留学习的心态~ 虚心向你们学习而且给本身的学习过程留下一些印记。java
正片的分界线node
先来阐述下什么是GraphQLreact
A query language for your API GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.git
这是官网上对他的解释,一种专门为 API 设计的查询语言: 一种知足你已有数据查询需求的 runtime,没有冗余而且精确的查询你须要的数据,也能让 API 更易于维护和演进。github
我仔细回想平常中开发遇到的那些以为很麻烦的问题:chrome
固然基于上面的问题我也知道如今各个公司自己也有本身 BFF 方案,针对前端作必定的优化。确实解决了上面一些问题。可是再后退一步说若是就 RESTful 自己的问题来思考的化,其实 GraphQL 解决的就是 RESTful 自己解决不了的问题。 咱们都知道:数据库
REST -> Representational State Transfer Resources 意味着单一资源不管是一张图片一个文件等等对应惟一的资源定位符
那么问题其实就在这里,每每随着如今前端界面的复杂化,咱们须要的资源每每不是单一资源了。那么这种架构自己也确实会有它的短板。
对比之下咱们看看为何可能会须要GraphQL
先明确一个概念GraphQL是基于SDL -> Schema Definition Language 熟悉数据库的同窗可能对于schema概念比较熟悉,其实咱们也能够根据这个名称去思考它自己Graph(图),图的概念自己就是你的data树形结构。
咱们看一下官网首页的例子:
# 描述的数据schema:
type Project {
name: String
tagline: String
contributors: [User]
}
# 你的请求数据:
{
project(name: "GraphQL") {
tagline
}
}
# 你获得的数据:
{
"project": {
"tagline": "A query language for APIs"
}
}
复制代码
从上面的例子咱们思考下,若是每一个数据自己都定义 schema 好处有两点:
既然解决了声明 schema 和接口文档问题,那它能不能解决多个 IO 请求和复用一个资源定位uri定位全部资源的问题呢?
首先复用一个资源定位 uri 定位全部资源确定是没问题的,前面咱们提到过既然是你的请求数据结构决定返回数据结构。那么不管你发出什么样的请求都会有相同的映射,服务端是不须要根据uri知道你具体请求什么信息了,而是经过你请求的格式(图)来判断是时候祭出官方的资源了:
咱们仍是借用官网的例子来看下:
# 你的请求资源可能涵盖以前RESTful的许多个接口或者是一个特别大的json数据
# 你可能在怀疑那若是RESTful一个接口也能返回下面的数据岂不是也很完美,没错但是若是我跟你说我可能须要的homeWorld 里的数据是特定的name 和climate呢?咱们还须要去url上传参数,而且实际状况是后端每每以为这样的东西我返回给你所有,你本身去拿就好啦。
{
hero {
name
friends {
name
homeWorld {
name
climate
}
species {
name
lifespan
origin {
name
}
}
}
}
}
# 对应的schema
type Query {
hero: Character
}
type Character {
name: String
friends: [Character]
homeWorld: Planet
species: Species
}
type Planet {
name: String
climate: String
}
type Species {
name: String
lifespan: Int
origin: Planet
}
复制代码
针对于拿特定数据这个问题为了更好的 (data=>UI),我看到一篇文章说代替以前 redux 的使用经验特别的好推荐给你们。
我一直以为这个对话框特别的有说服力:
下面咱们来在咱们的项目中实践下GraphQL
咱们先不要一口吃个胖子,先来一步步的改造以前的 ssr 耦合 GraphQL 看看这东西是怎么玩的。咱们的目的是利用 mock 的数据打通先后端流程。
先介绍下咱们用到的工具,直接使用 GraphQL 会有一些难度,因此咱们采用 Apollo 提供的一些工具:
咱们会在后面的使用中提到他们的一部分使用方式,固然最好·最全的使用方式是阅读官方文档
既然咱们提到咱们再也不须要各类url去定义一个资源自己,意味着咱们只须要一个接口所有搞定(我并无删掉以前代码而是禁掉,方便你们观察区别):
// apollo模块替代redux
import { ApolloProvider, getDataFromTree } from 'react-apollo';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { SchemaLink } from 'apollo-link-schema';
import { ApolloClient } from 'apollo-client';
// apollo grahql操做模块
import { makeExecutableSchema } from 'graphql-tools';
import { graphqlKoa } from 'apollo-server-koa';
// redux
// const { Provider } = require('react-redux');
// const getStore = require('../common/store').default;
// api前缀
const apiPrefix = '/api';
// 引入schema
let typeDefs;
const pathName = './server/schema.graphql';
if (typeof pathName === 'string' && pathName.endsWith('graphql')) {
const schemaPath = path.resolve(pathName);
typeDefs = importSchema(schemaPath);
};
// resolvers
let links = [{
id: 'link-0',
url: 'www.howtographql.com',
description: 'Love GraphQL'
},
{
id: 'link-002',
url: 'www.howtographql.com',
description: 'Love GraphQL'
}];
let idCount = links.length;
const resolvers = {
Query: {
info: () => `respect all, fear none!`,
feed: () => links,
name: () => `赵玮龙`,
age: () => 29
},
Mutation: {
post: (root, args) => {
const link = {
id: `link-${idCount++}`,
description: args.description,
url: args.url,
}
links.push(link)
return link
},
deleteLink: (root, args) => {
return links.filter(item => item.id !== args.id)
}
}
}
// 生成schema
const schema = makeExecutableSchema({
typeDefs,
resolvers
})
// 路由
module.exports = function(app, options={}) {
// 页面router设置
app.get(`${staticPrefix}/*`, async (ctx, next) => {
// graphql接口设置
const client = new ApolloClient({
link: new SchemaLink({ schema }),
ssrMode: true,
connectToDevTools: true,
cache: new InMemoryCache(),
})
const helmet = Helmet.renderStatic();
const context = {};
options.title = helmet.title;
// restful api redux数据源
// const store = getStore();
// const promises = routes.map(
// route => {
// const match = matchPath(ctx.path, route);
// if (match) {
// let serverFetch = route.component.loadData
// return serverFetch(store.dispatch)
// }
// }
// )
// const serverStream = await Promise.all(promises)
// .then(
// () => {
// return ReactDOMServer.renderToNodeStream(
// <Provider store={store}>
// <StaticRouter
// location={ctx.url}
// context={context}
// >
// <App/>
// </StaticRouter>
// </Provider>
// );
// }
// );
// graphql提取数据而且渲染dom
const Html = (
<ApolloProvider client={client}> <StaticRouter location={ctx.url} context={context} > <App/> </StaticRouter> </ApolloProvider>
);
const serverStream = await getDataFromTree(Html).then(() => ReactDOMServer.renderToNodeStream(Html));
// console.log(serverStream.readable);
await streamToPromise(serverStream).then(
(data) => {
options.body = data.toString();
if (context.status === 301 && context.url) {
ctx.status = 301;
ctx.redirect(context.url);
return ;
}
// 把store.getState()替换成client.extract()
if (context.status === 404) {
ctx.status = 404;
ctx.body = renderFullPage(options, client.extract());
return ;
}
ctx.status = 200;
ctx.set({
'Content-Type': 'text/html; charset=utf-8'
});
ctx.body = renderFullPage(options, client.extract());
})
// console.log(serverStream instanceof Stream);
await next();
});
// api路由
// app.get(`${apiPrefix}/user/info`, async(ctx, next) => {
// ctx.body = {
// code: 10000,
// msg: '',
// data: {
// name: '赵玮龙',
// age: 29,
// }
// }
// await next();
// });
//
// app.get(`${apiPrefix}/home/info`, async(ctx, next) => {
// ctx.body = {
// code: 10000,
// msg: '',
// data: {
// title: '你要的网站',
// content: '那些年我想过的女孩~',
// }
// }
// await next();
// });
// 设置调试GraphQL-playground
app.all('/graphql/playground', koaPlayground({
endpoint: '/graphql',
})
);
// GraphQl api
app.all('/graphql', graphqlKoa({ schema }));
}
复制代码
先来看下路由方面咱们声明了两个路由:
咱们看到根据 ssr 自己的原理,咱们把 INITIAL_STATE 换成了GraphQL的数据,这正是咱们后面会说道的利用 GraphQL 代替 redux 的方案 聚焦下三个问题。
import { importSchema } from 'graphql-import';
let typeDefs;
const pathName = './server/schema.graphql';
if (typeof pathName === 'string' && pathName.endsWith('graphql')) {
const schemaPath = path.resolve(pathName);
typeDefs = importSchema(schemaPath);
};
复制代码
再而后看看 schema:
type Query {
info: String
"""
the list of Posts by this author
"""
# Link实例拿到root声明,每个field都须要到它上层申明
feed: [Link!]!
name: String!
age: Int!
}
type Link {
id: ID!
description: String!
url: String!
}
type Mutation {
post(url: String!, description: String!): Link!
deleteLink(id: ID!): [Link!]!
}
# interface Character {
# id: ID!
# name: String!
# role: Int!
# }
#
# type Master implements Character {
#
# }
复制代码
可是你会发现,根据你的请求是 query 或者 mutation 会有参数或者一些 resolvers 中互相共享的参数等,这就是这个函数自己的一些参数:
主要前三个参数会是常常用到的。
既然服务端定义好了数据,咱们能够经过以前的 /graphql/playground 访问数据看看可否获得想要的结果 咱们发现这里还有咱们以前定义的所有 schema 这个文档查询简直是太方便啦!
至于客户端代码,咱们还用 react 来耦合 graphql
import React from 'react';
import Helmet from 'react-helmet';
import PropTypes from 'prop-types';
// import { connect } from 'react-redux';
import { withRouter } from 'react-router'
import { ApolloConsumer, Query } from 'react-apollo';
import gql from 'graphql-tag';
// actions
// import {
// userInfoAction,
// homeInfoAction
// } from '../actions/userAction';
// selector
// import {
// selectQueryDataFromUser,
// } from '../reducers/entities'
//
// const mapStateToProps = (state, ownProps) => {
// const userInfo = selectQueryDataFromUser(state)
// return {
// ...userInfo,
// }
// };
//
// const mapDispatchToProps = {
// userInfoAction,
// homeInfoAction
// };
//
// @connect(mapStateToProps, mapDispatchToProps)
const GET_INFO_AUTH = gql` { info feed { id url description } name age } `
class Home extends React.Component {
// static loadData(dispatch) {
// return Promise.all([dispatch(userInfoAction()), dispatch(homeInfoAction())])
// }
static defaultProps = {
name: '',
age: null,
}
render() {
const {
name,
age,
} = this.props
return (
<React.Fragment>
<Helmet>
<title>主页</title>
</Helmet>
<h1>{name}</h1>
<h2>{age}</h2>
</React.Fragment>
);
}
}
export default withRouter(
() => (
<Query
query={GET_INFO_AUTH}
>
{
({ loading, error, data }) => {
if (loading) return "loading..."
if (error) return `Error! {error.message}`
return (
<Home
age={data.age}
name={data.name}
/>
)
}
}
</Query>
)
);
复制代码
这里有一个叫 Query 的高阶组件,固然也有 Mutation,具体你能够查阅官方文档。 咱们会发现这个高阶组件把 fetch 包裹起来暴露给咱们须要的 data, loading 之类的数据供咱们渲染 UI。
相关 redux 自己的概念和它解决了哪些问题,若是你看兴趣能够看这里,固然咱们这里探讨的是利用 GraphQL 去替代 redux。咱们从上面的结构化·精确请求能发现,若是咱们能直接请求须要 UI 渲染的数据,就会省去不少处理数据和 normalize 化的过程,可是还有一个主要的问题没有解决,就是除去 server data 之外,还有不少本地的 data 处理,好比按钮展现隐藏 boolean,或者说本地的 data 和 server data 关联的问题,这就是为何在 redux 中咱们会把他们放在一块儿管理,那么 GraphQL 若是能解决这个问题而且也有一个全局惟一相似于 store 同样的数据源,这样咱们就不须要 mobx·redux 之类的数据管理库了,很幸运的是 Apollo 确实帮咱们这么作了,下面咱们来介绍下这个功能。既然用到本地的数据,最合适的例子仍是你们熟悉的 TodoList(在 home 页添加这个):
// 咱们新建一个todoForm 的文件写咱们的todoList组件
import React from 'react';
import { graphql, compose } from 'react-apollo';
import { withState } from 'recompose';
import {
addTodoMutation,
clearTodoMutation,
todoQuery,
} from '../../client/queries';
const TodoForm = ({
currentTodos,
addTodoMutation,
clearTodoMutation,
inputText,
handleText,
}) => (
<div> <input value={inputText} onChange={(e) => handleText(e.target.value)} /> <ul> { currentTodos.map((item, index) => (<li key={index}>{item}</li>)) } </ul> <button onClick={() => { addTodoMutation({ variables: { item: inputText } }) handleText('') }}> Add </button> <button onClick={(e) => clearTodoMutation()} > clearAll </button> </div> ) const maptodoQueryProps = { props: ({ ownProps, data: { currentTodos = [] } }) => ({ ...ownProps, currentTodos, }), }; export default compose( graphql(todoQuery, maptodoQueryProps), graphql(addTodoMutation, { name: 'addTodoMutation' }), graphql(clearTodoMutation, { name: 'clearTodoMutation' }), withState('inputText', 'handleText', ''), )(TodoForm) // queries.js import gql from 'graphql-tag'; // 这里的@写法是directives,能够查看上面的官方文档 const todoQuery = gql` query GetTodo { currentTodos @client } `; const clearTodoMutation = gql` mutation ClearTodo { clearTodo @client } `; const addTodoMutation = gql` mutation addTodo($item: String) { addTodo(item: $item) @client } `; export { todoQuery, clearTodoMutation, addTodoMutation, } 复制代码
看下效果:
(右边的 chrome 插件是 apollo)
咱们先看下几个问题:
第一个问题:
不知道你们有没有在写react的时候,习惯 stateless components 的形式呢? 我我的比较偏心这种写法,固然啦它也有本身的不足,就是没有 state 和生命周期,可是人们确定不会放弃使用它们,甚至有人想的更加极致就是代码里暴露都是这种 FP 风格的写法,因而就有了recompose,若是你有兴趣能够研究它的文档使用下。这里不是此次的重点,咱们带过,其实为了实现你的 UI 层的抽离,好比把逻辑层抽离在 HOC 高阶组件里,好比上面你看到的 withState 就是一个高阶组件,声明的 state 和相应的 function,你可能会好奇问什么要这样写呢?
// 咱们设想下若是咱们采用 Mutation 和 Query 组件嵌套的模式避免不了出现下面的形式(是否是感受有点像回调地狱呢?):
<Mutation>
{
...
<Query>
{
...
}
</Query>
...
}
</Mutation>
// recompose也提供了组合多个高阶组件的模式 compose, 固然 apollo 也有(至关于a(b(c())))
compose(a, b, c)
// 这样的代码看起来会不会舒服不少呢?
复制代码
第二个问题:
maptodoQueryProps 是什么? 用过 react-redux 的同窗确定熟悉 mapStateToProps 和 mapDispatchToProps 这两个函数,这里没有 dispatch 的概念,可是做者也是深受以前这个库的影响,想把 mutation, query data 也经过这种模式有一个 props 的映射。固然这里不止是 props 一个 key 具体能够参考这里,因此实际上是把 props.data(query) 和 props.mutation(mutation) 分别按照本身对于 props 的需求映射到 UI 组件上(是否是很像 selector)。
第三个问题:
这里是咱们主要要解释的,你们必定好奇,这个 todoList 逻辑呢?咱们的reducer 去哪啦?
import {
todoQuery,
} from './queries';
const todoDefaults = {
currentTodos: []
};
const addTodoResolver = (_obj, { item }, { cache }) => {
const { currentTodos } = cache.readQuery({ query: todoQuery });
const updatedTodos = currentTodos.concat(item);
cache.writeQuery({
query: todoQuery,
data: { currentTodos: updatedTodos }
});
return null;
};
const clearTodoResolver = (_obj, _args, { cache }) => {
cache.writeQuery({
query: todoQuery,
data: todoDefaults
});
return null;
};
export {
addTodoResolver,
clearTodoResolver,
todoDefaults,
}
复制代码
还记得咱们前面说 apollo-server 里的 resolver 处理 schema 相应字段的逻辑吗?这里的概念基本相似,apollo 仍是利用 resolver 去处理字段级别的逻辑,你可能会问这不是 reducer 的概念,没错这里彻底不是 redux 的理念,而是对于 AST 语法树的一种处理而已(因此这里也没有强迫你去用 pure function 处理, 而且强调 reducer 的可组合拆分性,这是我以为很是难过的地方,它失去了 redux 核心理念,换来一堆我根本就不想学的 api 和参数,哎。这个 apollo 在我认为就是 api 太多,本人之因此一直很欣赏 react+redux 解决方案,就由于灵活度很高而且 api 不多,这种作法也算是抽离了逻辑层吧)
这里有4个api,这里有详细的文档,这4个 api 分别操做 query 和 fragment,可是就我我的而言真的没有 reducer 容易理解而且灵活性强,期待大家的见解!
咱们会随着项目深刻继续说一些 GraphQL 的概念和使用方法,也但愿感兴趣的你能够留言交流。这里面东西确实是不少,坑也不少,因此没有涉及到的地方,咱们之后仍是开个专题来讨论下 GraphQL 不少缓存策略包括 redies 使用以及如何鉴权的方案(项目后面会涉及到部分,可是并不全面,敬请期待!)
由于此次代码改动量比较大,我仍是把源码放在这里,但愿你们不要以为我耍流氓只说不放源码! (若是你喜欢的话给个 star 吧!)