Graphql的全栈指南
这是GraphQL系列文章的第二篇,我们从GraphQl的介绍开始,然后实现了一个简单的NodeJs GraphQl服务,以了解Resolvers 、Schemas (typeDefs) 、Query 和Mutation 等概念。
一旦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。
添加absinthe 和absinthe_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宏,用于突变。 - 字段:包围对象中的一个字段,这里是
query和object。 - 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.ex 和prev/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
祝你学习愉快!😇