走在JS上的全栈之路(二)(1/2)

(这是一个系列文章:预计会有三期,第一期会以同构构建先后端应用为主,第二期会以 GraphQL 和 MySQL 为主,第三期会以 Docker 配合线上部署报警为主)javascript

做者: 赵玮龙html

重要声明: 今后再也不以 AMC 团队名称发布文章,缘由不详述,全部文章和后续文章将由我的维护,若是你对个人文章感兴趣,也请继续支持和关注,再次声明-我的仍是会保持更新和最新以及前沿技术的踩坑,不只仅局限于前端领域!前端

可能你也发现题目出现了1/2,由于若是介绍 GraphQL 和 MySQL 一块儿,容易忽略掉中间的不少细节过程,还有篇幅自己问题,我准备把他们拆开来讲,我仔细想了下,我先从前端的角度看 GraphQL 如何耦合到咱们的项目中,看看它能为咱们带来什么而且解决了什么问题(虽然拆开说,篇幅仍是很是长的,但愿各位感兴趣的同窗能够先点赞保存~~~慢慢看),再而后咱们看看 node 端如何从数据库层面支持 GraphQL,仍是保留学习的心态~ 虚心向你们学习而且给本身的学习过程留下一些印记。java


正片的分界线node

什么是GraphQL,为何咱们会须要它

先来阐述下什么是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

  • 首先是每次开发需求开始状态都须要都 mock 的数据,根据后端提供的接口文档去生成本身的 mock 数据不管是公司已有的 mock server 工具仍是本身的 mock server 或者是第三的 mock server 这让开发变得繁琐,缘由是在开发中我须要不停的查询后端的接口文档制定本身的 mock 数据而且,联调中还要会由于字段等等与后端不一致再次更改。
  • 每次请求资源的时候,常常会遇到各类串行·并行请求,核心缘由多是由于后端领域服务或者是数据库层面的缘由,这样会致使咱们在获得咱们需求的资源过程变得很是复杂。
  • 因为上面的问题或者哪怕是单个接口咱们每每也须要把接口返回的数据 normalize 化,固然就算你不 normalize 也须要处理返回数据拿到你真正映射到 UI 的 data。(后端返回数据每每不是咱们真正想要的,或者说不是所有咱们都须要的。)
  • 根据 RESTful 请求也就意味着咱们须要不少接口,或者说是起不少接口名称定位资源而且资源定位未必准确,这样不只仅浪费IO次数也会产生不少其实不必的网络请求。

固然基于上面的问题我也知道如今各个公司自己也有本身 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 好处有两点:

  • 这看起来是否是更加像自然的接口文档
  • 每一个字段都有本身的 scalar(类型),这点对于js自己弱类型来讲是个极好的消息。

既然解决了声明 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

咱们先不要一口吃个胖子,先来一步步的改造以前的 ssr 耦合 GraphQL 看看这东西是怎么玩的。咱们的目的是利用 mock 的数据打通先后端流程。

先介绍下咱们用到的工具,直接使用 GraphQL 会有一些难度,因此咱们采用 Apollo 提供的一些工具:

  • graphql-tag
  • apollo-client
  • apollo-server-koa
  • graphql-tools
  • apollo-cache-inmemory
  • react-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 }));
}
复制代码

先来看下路由方面咱们声明了两个路由:

  • /graphql(用于请求数据的接口)
  • /graphql/playground(graphql实现请求界面)

咱们看到根据 ssr 自己的原理,咱们把 INITIAL_STATE 换成了GraphQL的数据,这正是咱们后面会说道的利用 GraphQL 代替 redux 的方案 聚焦下三个问题。

  • schema (随着业务的发展咱们会把 schema 分出去单独成文件,固然若是你的编辑器支持 graphql 语法,你固然更但愿以 .graphql 文件结尾而后拥有IDE的功能)
  • resolvers (随着数据库的加入咱们下篇文章会说如何作 ORM 映射,这里先是 mock 数据)
  • ApolloClient (替代掉 createStore)

schema 自己咱们但愿它写在单独的文件中,例如 .graphql 中,作到拆分逻辑,可是目前 node 还不支持这个结尾文件名,咱们用的第三方库,固然本身作也并不难,就是利用 fs 读出 utf8 编码的字符串就行。

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 {
#
# }
复制代码

resolvers 主要解决的是 schema 声明的字段处理方式,每一个字段都有本身的 function 这个自己不难理解

可是你会发现,根据你的请求是 query 或者 mutation 会有参数或者一些 resolvers 中互相共享的参数等,这就是这个函数自己的一些参数:

  • root 至关于当前字段的父级字段信息。
  • args: 字段自己的参数。
  • context: resolver之间自己的共享对象。
  • info: 你的 schema AST 语法树

主要前三个参数会是常常用到的。

既然服务端定义好了数据,咱们能够经过以前的 /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 作数据管理

相关 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)

咱们先看下几个问题:

  • compose 里一堆奇怪的东西是干吗的。
  • maptodoQueryProps 是什么
  • graphql() 是什么鬼。。

第一个问题:

不知道你们有没有在写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 吧!)

相关文章
相关标签/搜索