(這是一個系列文章:預計會有三期,第一期會以同構構建先後端應用爲主,第二期會以 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 吧!)