翻译|Redux和GraphQL入门


title: 翻译|Redux和GraphQL入门
date: 2017-04-11 23:15:32
categories: 翻译
tags: Redux


当GraphQL发布以来,非常清楚的显示出,他将会成为非常好的技术.社区都在耐心的等待技术评价.
但是你可能和我一样,发现文档比我们期待的更难理解.可能的原因是由于GraphQL和Relay的联合使用.

我也感觉到了你的痛苦.我的大脑都要融化掉了,我告诉我自己我将会尝试在其他的框架里是用它.我做到了!这一次我仅仅把关注点放在GraphQL自身,其他的地方保持尽可能的简单.

Sharing is Caring

这个教程的配置部分尽可能的简单,结合GraphQL和Redux.减少复杂的部分,所有的内容你可以直接从这里看到(指代码部分).

我们将使用Redux来代替Relay,在服务器上使用es5而不是es6/babel-node.所有的GraphQL的东西都保持尽可能的简单.

下面配置一下项目

项目文件配置

创建新文件件(graphql-app).
需要一个package.json.

 npm init

需要在服务器上安装一下模块:graphql-js,express-graphql,express,webpack和webpack-dev-server.

编写服务器的编码使用es5,避免编译过程.

创建sevsr.js文件,导入我们安装的模块
server.js

 var webpack = require(‘webpack’);
var WebpackDevServer = require(‘webpack-dev-server’);
var express = require(‘express’);
var graphqlHTTP = require(‘express-graphql’);
var graphql = require(‘graphql’);
//下面是有关graphql使用的配置,有对象和类型
var GraphQLSchema = graphql.GraphQLSchema;
var GraphQLObjectType = graphql.GraphQLObjectType;
var GraphQLString = graphql.GraphQLString;
var GraphQLInt = graphql.GraphQLInt;

你可以看到我们给graphQL的类型定义了变量,后面我们要使用这些变量.

接着我们为GraphQL创建可以获取的数据.这里使用Goldbergs的数据作为来源.

我们的数据

 var goldbergs = {
 1: {
   character: "Beverly Goldberg",
   actor: "Wendi McLendon-Covey",
   role: "matriarch",
   traits: "embarrassing, overprotective",
   id: 1
 },
 2: {
   character: "Murray Goldberg",
   actor: "Jeff Garlin",
   role: "patriarch",
   traits: "gruff, lazy",
   id: 2
 },
 3: {
   character: "Erica Goldberg",
   actor: "Hayley Orrantia",
   role: "oldest child",
   traits: "rebellious, nonchalant",
   id: 3
 },
 4: {
   character: "Barry Goldberg",
   actor: "Troy Gentile",
   role: "middle child",
   traits: "dim-witted, untalented",
   id: 4
 },
 5: {
   character: "Adam Goldberg",
   actor: "Sean Giambrone",
   role: "youngest child",
   traits: "geeky, pop-culture obsessed",
   id: 5
 },
 6: {
   character: "Albert 'Pops' Solomon",
   actor: "George Segal",
   role: "grandfather",
   traits: "goofy, laid back",
   id: 6
 }
}

GraophQL

GraphQL从简化的角度考虑,有一个类型系统构成-这是我们用来理解他的心理模型-我们将看到这里有三种”类型”.

  1. 模型的类型
  2. 查询的类型
  3. schema的类型

在实际的编码中,类型可能比这个简单,这里只是为了到入门的目的,所以比较简单

模型的类型

我们将创建一个”模型类型”,实际相当于实际的数据的镜像.

 var goldbergType = new GraphQLObjectType({
  name: "Goldberg",
  description: "Member of The Goldbergs",
  fields: {
   character: {
     type: GraphQLString,
     description: "Name of the character",
   },
   actor: {
     type: GraphQLString,
     description: "Actor playing the character",
   },
   role: {
     type: GraphQLString,
     description: "Family role"
   },
   traits: {
     type: GraphQLString,
     description: "Traits this Goldberg is known for"
   },
   id: {
     type: GraphQLInt,
     description: "ID of this Goldberg"
   }
 }
});

我们创建了一个GraphQLObjectType的对象实例,取名为”Goldberg”.
在“fields”下,每一个“type”表明一个期待的类型.例如 string(GraphQLString)最为演员角色的类型,int(GraphQLInt)作为Id的类型约束.

你可能也注意到了”description”字段,GraphQL自带说明文档.当我们结合express-graphql使用GraphiQL的时候可以在action中刚看到这个描述内容.

Query Type

“Query type”定义了我们怎么查询我们的数据

var queryType = new GraphQLObjectType({
  name: "query",
  description: "Goldberg query",
  fields: {
    goldberg: {
      type: goldbergType,
      args: {
        id: {
          type: GraphQLInt
        }
      },
      resolve: function(_, args){
        return getGoldberg(args.id)
      }
    }
  }
});

“query type”也是GraphQLObjectType的实例.只是用于不同的目的.
我们创建goldberg这个查询字段,设定的类型是goldbergType.在args(参数)下我们可以看到新的goldberg字段,它将接受id作为参数.

但我们解析查询的时候,我们返回gegGoldberg()函数的调用返回值

 function getGoldberg(id) {
 return goldbergs[id]
}

从查询中的id从data中返回其中一个Goldberg.

Schema type

最终”schema type”把类型放到一起.

为schema提供服务

我们可以使用express和graphqlHTTP 中间件来提供schma服务.

 var graphQLServer = express();
graphQLServer.use('/', graphqlHTTP({ schema: schema, graphiql: true }));
graphQLServer.listen(8080);
console.log("The GraphQL Server is running.")
node server

浏览器打开http://localhost:8080/.可以看到GraphiQL IDE工作了.
如果我们执行了查询

 { 
 goldberg(id: 2) { 
   id,
   character
 }
}

返回的结果是

 {
 "data": {
   "goldberg": {
     "id": 2,
     "character": "Murray Goldberg"
   }
  }
}

再做一些其他查询也非常的有意思.

提示:在屏幕的顶部右边,有一个按钮,标签为”Docs”,如果我们点击按钮,可以看到之前在”description”中添加的字段内容.可以探索一下文档.

为app提供服务

为了在我们app的前端使用GraphQL,需要安装babel,babel-loader以及一组babel-presets的约定.

 npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react

创建文件.babelrc,这个文件告诉babel,我们的预先设定.

 {
 "presets": ["es2015", "stage-0", "react"]
}

创建一个新的index.js文件.目前还没有内容.

创建新的文件夹static,在文件夹中添加index.html文件.

 <div id="example"></div>
<script src="/static/bundle.js"></script>
<h3>hello world</h3>

现在我们的项目结构看起来像这样

graphql-app
| -- index.js
| -- server.js
| -- package.json
| -- .babelrc
| -- static
   | -- index.hml

在server.js文件中,我们需要配置webpack,借助babel打包项目的js文件.

在graphQLServer.listen(8080)下

 var compiler = webpack({
  entry: "./index.js",
  output: {
    path: __dirname,
    filename: "bundle.js",
    publicPath: "/static/"
  },
  module: {
    loaders: [
      { test: /\.js$/, 
        exclude: /node_modules/, 
        loader: "babel-loader"
      }
    ]
  }
});

Webpack 将会接受index.js文件,编译一个est的版本到/static/bundle.js文件.

接下来我们创建一个新的WebpackDevServer 来提供bundled的项目.

 var app = new WebpackDevServer(compiler, {
 contentBase: "/public/",
 proxy: {"/graphql": `http://localhost:${8080}`},
 publicPath: "/static/",
 stats: {colors: true}
});
app.use("/", express.static("static"));
app.listen(3000);
console.log("The App Server is running.")

proxy字段添加了我们已经创建的GraphQL服务到我们的app server,这可以使我们直接在app内部进行查询,不会有跨域问题.

启动一下

noder server

浏览器打开http://localhost:3000,我们会看到”hello world”的消息.
再到http://localhost:3000/graphql.

React和Redux

为了添加react和react-redux,app需要额外的组件:React,Redux,React-Redux,Redux-thunk和Immutable.

npm install --save react react-dom redux react-redux redux-thunk immutable

因为我们使用babel配置了webpack,我们可以在前端使用es6

从static/index.html文件中删除掉”hello world”,使用React添加新的信息.

 import React from "react";
import ReactDOM from "react-dom";
const Main = React.createClass({
  render: () => {
    return (
      <div>
        <p>hello react!</p>
      </div>
    )
  }
});
ReactDOM.render(
 <Main />,
 document.getElementById("example")
);

重新启动localhost:300,可以看到信息.

Reducer

添加新的文件夹,取名”app”最为子文件夹

 | -- app
   | -- actions
   | -- components
   | -- reducers

在reducerS 文件夹中创建reducer.js的文件,里面将执行我们的reducer函数.

我们会使用利用Immuatable模块为state服务,以便我们形成好的习惯.

 import Immutable from "immutable";
const immutableState = Immutable.Map({
  fetching: false,
  data: Immutable.Map({})
})

我们的state有两个字段-一个让我们知道是否在查询/等待响应的中间阶段,另一个包含着返回的响应数据.

下一步我么把ImmutableState添加到reducer 函数中

 export const queryReducer = (state = immutableState, action) => {
  switch (action.type) {
    case "STARTING_REQUEST":
      return state.set("fetching", true);
    case "FINISHED_REQUEST":
      return state.set("fetching", false)
             .set("data", Immutable.Map(action.response.data.goldberg));
    default:
      return state
  }
}

当我们在执行“STARING_REQUEST” action的时候,分发的动作改变”fecthing”的state 为true,表示在获取数据中.

当执行“FINISHED_REQUEST” action的时候,分发的工作改变 “feching”的state为false,data的state设定为我们的响应数据.

Store

返回到index.js文件,我们想在reducer之外创建store,store接入到我们的主组件.我们需要借助redux和react-redux的助手函数来把刚刚创建的reducer导入store.

还需要使用redux-thunk 中间件来协助后面的数据请求动过.

import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import { queryReducer } from "./app/reducers/reducers.js";
import thunkMiddleware from "redux-thunk";

首先我们应用redux-thunk中间件

 const createStoreWithMiddleware = applyMiddleware(
  thunkMiddleware
)(createStore)

然后在Redux Provider中包装我们的主组件,传递queryReducer到createStoreWithMiddleware.

 ReactDOM.render(
  <Provider store={createStoreWithMiddleware(queryReducer)}>
    <Main />
  </Provider>,
  document.getElementById("example")
);

完成了!创建了store.

Actions

在actions文件夹中创建新文件actions.js

我们需要创建两个action来分发动作到我们的reducer,其中之一为“STARTING_REQUEST”,另一个为”FINISHED_REQUES”

const startingRequest = () => {
  return {
    type: "STARTING_REQUEST"
  }
}
const finishedRequest = (response) => {
  return {
    type: "FINISHED_REQUEST",
    response: response
  }
}

在store中之前应用的中间件redux-thunk是一件非常伟大的事情,当一个action返回一个函数,这个函数可以使用dispatch来注入到reducer.(译注:对于一部操作,返回响应值以后,可以在发起一个dispatch来通知reducer对state做出改变).

在一个新的getGraph action中,使用了两次dispatch()

export const getGraph = (payload) => {
  return dispatch => {
    dispatch(startingRequest());
    return new Promise(function(resolve, reject) {
      let request=new XMLHttpRequest();
      request.open("POST", "/graphql", true);
      request.setRequestHeader("Content-Type",
                               "application/graphql");
      request.send(payload);
      request.onreadystatechange = () => {
        if (request.readyState === 4) {
          resolve(request.responseText)
        }
      }
    }).then(response =>
            dispatch(finishedRequest(JSON.parse(response))))
  }
}

当getGraph()函数调用的时候,我们dispatch startingRequest(),表示开始一个新的查询.然后开始一个异步的请求(提示:”header”中有application/graphql的类型).当我们的查询完成的时候,我们dispatch finishedRequest() action,提供我们查询的结果.

Component

在”component”文件夹中,我们创建一个新的文件, Query.js文件

我们需要导入react,几个助手函数,还有刚刚创建的getGraph函数.

import React from ‘react’;
import { connect } from ‘react-redux’;
import { getGraph } from ‘../actions/actions.js’;

目前我们创建了空的出查询组件

let Query = React.createClass({
  render() {
    return (
      <div>
      </div>
    )
  }
});

我们要在组件中挂载我们的store和dispatch方法,方式是通过创建container组件和react-redux connect()函数

const mapStateToProps = (state) => {
  return {
    store: state
  }
};
export const QueryContainer = connect(
 mapStateToProps
)(Query);

在我们的Query组件中,我们需要接入componentDidMount 生命周期函数,从而可以在组件挂载的时候获取数据.

let Query = React.createClass({
  componentDidMount() {
    this.props.dispatch(
      getGraph("{goldberg(id: 2) {id, character, actor}}")
    );
  }
})

然后我们要添加组件来用于填充获取的响应的数据,一个提交额外查询的按钮.我们想知道在数据查询过程中的状态,并且显示在页面中.

let Query = React.createClass({
  componentDidMount() {
    this.props.dispatch(
      getGraph("{goldberg(id: 2) {id, character, actor}}")
    );
  },
  render() {
    let dispatch = this.props.dispatch;
    let fetchInProgress = String(this.props.store.get('fetching'));
    let queryText;
    let goldberg = this.props.store.get('data').toObject();
    return (
      <div>
        <p>Fetch in progress: {fetchInProgress}</p>
        <h3>{ goldberg.character }</h3>
        <p>{ goldberg.actor }</p>
        <p>{ goldberg.role }</p>
        <p>{ goldberg.traits }</p>
        <input ref={node => {queryText = node}}></input>
        <button onClick={() => {
          dispatch(getGraph(queryText.value))}
        }>
          query
        </button>
      </div>
    )
  }
});

上面这一步做完以后,最后一件事情就是把QueryContainer组件添加到我们的主组件.

index.js

 import { QueryContainer } from “./app/components/Query.js”;

使用QueryConatiner组件替代”hello react”组件

 const Main = () => {
  return (
    <div>
      <QueryContainer />
    </div>
  )
};

完成!现在运行编制好的GraphQL查询就可以获得核心内容.试着查询:{gold-berg(id:4)}{id,charactar,actor,traits},看看可以获得什么结果.

感谢

感谢阅读,我希望这篇文章能对你有帮助.你可以在这里查看源代码.现在我们使用Redux和GraphQL构建了非常好的app.

另外感谢Dan Abramov指出教程中的一个错误.

Resources

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容