Graphql的全栈指南

237 阅读5分钟

Graphql的全栈指南

这是GraphQL系列文章的第二篇,我们从GraphQl的介绍开始,然后实现了一个简单的NodeJs GraphQl服务,以了解ResolversSchemas (typeDefs)QueryMutation 等概念。

一旦GraphQl被开源,社区就开始用他们最喜欢的服务器端语言实现这些规范。

有了这个认识,我们将使用Phoenix框架在Elixir中实现同样的GraphQL服务。

Phoenix框架

Phoenix是一个网络框架,它实现了流行的服务器端模型视图控制器模式,它是用elixir编写的。你应该有使用Elixir和Phoenix的基本经验才能继续阅读。

Absinthe GraphQL

Absinthe GraphQL是我们将要使用的Elixir中的GraphQL实现。

Phoenix的设置

由于Phoenix是用Elixir编写的,第一步是安装Elixir,然后是Phoenix:

如果你很懒,像我一样🥱,没有安装这些东西,只要运行这些命令即可:

$ brew install asdf
$ brew install postgresql

$ KERL_CONFIGURE_OPTIONS="--without-javac --with-ssl=$(brew --prefix openssl)"

$ asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git
$ asdf install erlang 22.1
$ asdf global erlang 22.1

$ asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
$ asdf install elixir 1.9
$ asdf global elixir 1.9

# To check that we are on Elixir 1.5 and Erlang 18 or later, run:
$ elixir -v

$ mix local.hex
$ mix archive.install hex phx_new 1.4.16

假设安装已经完成,让我们进入我们的项目设置。

项目设置

创建一个新的Phoenix项目:

$ mix phx.new menucard --no-webpack

创建数据库并启动服务器:

$ cd menucard
$ mix ecto.migrate
$ mix phx.server
[info] Running MenuCardWeb.Endpoint with cowboy 2.7.0 at 0.0.0.0:4000 (http)
[info] Access MenuCardWeb.Endpoint at http://localhost:4000

Phoenix应用模板将postgres作为默认的数据库适配器,其用户为postgres ,密码为postgres

现在应用程序在iex 会话中运行,但不是交互式的(使用iex -S mix phx.server 来启动交互式会话)。停止应用程序并继续(按两次ctrl+c )。

Absinthe

如果你是GraphQL的新手,我建议你阅读前面的文章,或者通过官方的GraphQL文档或指南来更好地了解GraphQL规范。

Absinthe主要支持GraphQL支持的所有规格,我们现在要在Phoenix中创建应用程序。

让我们来设置Absinthe。

添加absintheabsinthe_plug 作为依赖关系,absinthe_plug 是为了在phoenix中使用absinthe,GraphiQL 接口:

# mix.exe

defp deps do
  [
    ..,
    {:absinthe, "~> 1.4"},
    {:absinthe_plug, "~> 1.4"}
  ]
end

然后运行:

$ mix deps.get

在Absinthe中我们还定义了Schema,Resolvers, 和Types 。我们可以把它们定义为不同的模块,也可以定义在一个文件中。

lib/menu_card_web/schema.ex 中创建模式为MenuCardWeb.Schema

我们的应用程序的一个简单模式可以是:

# lib/menu_card_web/schema.ex

defmodule MenuCardWeb.Schema do
  use Absinthe.Schema

  @desc "An item"
  object :item do
    field :id, :id
    field :name, :string
  end

  # Example data
  @menu_items %{
    "foo" => %{id: 1, name: "Pizza"},
    "bar" => %{id: 2, name: "Burger"},
    "foobar" => %{id: 3, name: "PizzaBurger"}
  }

  query do
    field :menu_item, :item do
      arg :id, non_null(:id)
      resolve fn %{id: item_id}, _ ->
        {:ok, @menu_items[item_id]}
      end
    end
  end

end

这是一个简单的模式,我们可以查询一个特定的项目。

我们正在使用一些宏和函数,这些宏和函数写在Absinthe.Schema

  • queryrootQuery对象宏,我们在这里将不同的查询定义为字段。也有平等的mutation 宏,用于突变。
  • 字段:包围对象中的一个字段,这里是queryobject
  • arg:包围字段的一个参数。
  • resolve:包围字段的解析函数。

我们也在定义一个对象类型:item ,使用另外两个内置的标量类型:id ,代表一个唯一的数字,:string 是明显的。

而我们正在使用类型:item ,用于查询:menu_item ,以返回一个具有这种类型的地图。

MenuCardWeb.Router 中添加这个,以访问Absinthe提供的GraphiQL 接口:

# lib/menu_card_web/router.ex

defmodule MenuCardWeb.Router do
  ...
  forward "/graphiql", Absinthe.Plug.GraphiQL, schema: MenuCardWeb.Schema
end

转到localhost:4000/graphiql ,并运行一个查询:

{
  menuItem(id: "bar") {
    name
  }
}

结果:

{
  "data": {
    "menuItem": {
      "name": "Burger"
    }
  }
}

用Ecto

让我们使用混合任务来为项目和它们的评论生成上下文、模式和迁移:

$ mix phx.gen.context Menu Item items name:string price:integer 

$ mix phx.gen.context Menu Review reviews comment:string author_id:integer item_id:references:items

这些将创建我们需要的迁移和列。这应该已经添加了lib/menu_card/menu/item.ex,lib/menu_card/menu/review.ex,lib/menu_card/menu.exprev/repo/migrations 内的迁移。

编辑review.ex ,让我们在创建评论时添加item_id

# lib/menu_card/menu/review.ex

defmodule MenuCard.Menu.Review do
  use Ecto.Schema
  import Ecto.Changeset

  schema "reviews" do
    field(:comment, :string)
    field(:author_id, :integer)

    belongs_to(:item, MenuCard.Menu.Item)

    timestamps()
  end

  @doc false
  def changeset(review, attrs) do
    review
    |> cast(attrs, [:comment, :author_id, :item_id])
    |> validate_required([:comment, :author_id, :item_id])
  end
end

运行迁移程序:

$ mix ecto.migrate

创建和获取一个项目

让我们来写一个突变,我们需要在模式中创建一个项目的类型。删除模式中的旧代码,从空文件开始:

# lib/menu_card_web/schema.ex

defmodule MenuCardWeb.Schema do
  use Absinthe.Schema

  @desc "An item"
  object :item do
    field(:id, :id)
    field(:name, :string)
    field(:price, :integer)
    field(:reviews, list_of(:review))
  end

  @desc "Review for an item"
  object :review do
    field(:id, :id)
    field(:comment, :string)
    field(:author_id, :integer)
  end

  mutation do
    field :create_item, :item do
      arg(:name, non_null(:string))
      arg(:price, non_null(:integer))

      resolve(fn args, _ ->
        {:ok, MenuCard.Menu.create_item(args)}
      end)
    end
  end
end

在这里,唯一的区别是语言(Elixir),GraphQL规格的其他部分与之前的博客没有变化。

我想补充的几点是:absinthe中的解析函数和字段类型的约束与NodeJS版本不同。

  • 解析函数可以是一个3或2位数的函数。

    • 3 arity resolver

      item(id: 1){
        name
      }
      

      第一个参数将是父级,即,item(id: 1) 的解析值将是字段name 的父级。

      第二个参数将是为字段传递的args ,因此,对于字段item(id: 1) ,参数将是%{id: 1}

      第三个参数将是全局的context ,我们可以将其设置为一个插件。

    • 2 arity resolver:这里,第一个参数将是args ,第二个参数将是context

  • list_of(object_type/sclar-type):返回的值或传递的参数应该是一个列表。相当于[TypeName]

  • non_null(object_type/scalar-type):返回的值或参数应该是通过的,即不为空。等同于TypeName!

  • Resolver函数应该返回一个元组,第一个元素是:ok:error ,第二个元素应该是map。

现在,如果你运行

$ iex -S mix phx.server

你会看到一个错误,说应该有一个查询对象,它是我们定义的所有对象的根。一个突变也是一个rootMutation对象,但突变对象允许为空。

让我们添加一个查询并尝试一下。

mutation 对象之前添加这个:

query do
  field :item, :item do
    arg(:id, non_null(:id))

    resolve(fn args, _ ->
      {:ok, MenuCard.Menu.get_item(args)}
    end)
  end
end

注意:还有一种类型的对象是subscriptions 。我们将在另一章中广泛地看到它。

现在运行iex -S mix phx.server ,打开localhost:4000/graphiql ,使用GraphIQL 接口。在左边的文本区,写下查询,然后点击播放按钮或ctrl + enter ,运行查询。

查询:

mutation{
  createItem(name: "Rice pudding", price: 30){
    id
    name
    price
  }
}

结果:

{
  "data": {
    "createItem": {
      "id": "1",
      "name": "Rice pudding"
    }
  }
}

呼啦啦 🎉!

我们完成了基本的查询和变异。

加载关联

你可以看到,我们有reviews ,为每个item

@desc "An item"
object :item do
  field(:id, :id)
  field(:name, :string)
  field(:price, :integer)
  field(:reviews, list_of(review))
end

我们可以通过三种方式加载review 关联

  • 为它写一个单独的解析器

    object :item do
      field(:id, :id)
      field(:name, :string)
      field(:price, :integer)
    
      field(:reviews, list_of(:review)) do
        resolve(fn parent, _, _ ->
          {:ok, MenuCard.Menu.get_reviews_by_item(parent.id)}
        end)
      end
    end
    
  • :item 解析器本身中返回评论:

    # lib/menu_card/menu.ex
    
    defmodule MenuCard.Menu do
      ...
    
      def get_item(%{id: id}) do
        Repo.get!(Item, id)
        |> Repo.preload(:reviews)
      end
    
    end
    
  • Absinthe建议使用dataloader 来批量加载关联。

即使如此,我们还是坚持使用预加载。这不是一个最好的做法,而且有弊端,尽量使用dataloader进行prod。

在你的代码中添加使用preload 的函数。

为了得到项目的评论,首先,我们需要一种方法来创建它们:突变

schema 中添加一个突变:

# lib/menu_card_web/schema.ex

defmodule MenuCardWeb.Schema do

  ...

  mutation do

    ...

    field :do_review, :review do
      arg(:comment, non_null(:string))
      arg(:author_id, non_null(:id))
      arg(:item_id, non_null(:id))

      resolve(fn args, _ ->
        {:ok, MenuCard.Menu.create_review(args)}
      end)
    end
  end
end

重置并从新的数据库开始:

$ mix ecto.reset
$ iex -S mix phx.server

我们将创建一个菜单项,然后为其添加一个评论

mutation {
  createItem(name: "Rice pudding", price: 40) {
    name
    price
  }
}

结果:

{
  "data": {
    "createItem": {
      "name": "Rice pudding",
      "price": 40
    }
  }
}

用那个返回的ID,创建一个评论:

mutation {
  doReview(itemId: 1, authurId: 1, comment: "Yummmmy!") {
    comment
  }
}

结果:

{
  "data": {
    "doReview": {
      "comment": "asdad"
    }
  }
}

就这样,这就是你如何用phoenix框架创建一个HTTP GraphQL API。

这里是代码

如果你想做更多的事情,可以创建一个突变来删除和编辑项目和评论。

在下一篇文章中见。如何使用apollo-client ,在前端利用这些API?ReactJS

祝你学习愉快!😇