Rails 在 Rails 项目里使用 Graphql

rocLv · 2017年02月05日 · 最后由 rhybee 回复于 2017年03月10日 · 7478 次阅读
本帖已被管理员设置为精华贴

本文严重参考了此文,此前未注明,请大家谅解。

Rails 一直走在技术的前沿,从最早引入 scss、coffee 等就可以说明,Rails 一直在努力引入前端的前沿的技术。

过去的一年,前端涌现出各种框架,而 Rails 社区紧随其后有了相应的解决方案,比如 react-rails、react_on_rails 等等。

本文主要介绍了一下如何在 Rails 项目中使用 GraphQL,并抛出一些关于用 GraphQL 取代 API 的一些最佳实践,未必正确,权当抛砖引玉。欢迎拍砖。

关于GraphQL的介绍大家可以参看这里。这里主要介绍怎么集成到 Rails 项目中。

我们先创建一个 Rails 项目

$ rails new MyBlogApp
.
.
.

$ cd MyBlogApp

创建 Rails GraphQL API

首先,我们把rack-cors gem 添加到 Gemfile 中。

gem 'rack-cors', :require => 'rack/cors'

然后把以下内容添加至config/application.rb:

# config/application.rb

module MyBlogApp
  class Application < Rails::Application

    # ...

    config.middleware.insert_before 0, "Rack::Cors" do
      allow do
        origins '*'
        resource '*', :headers => :any, :methods => [:get, :post, :options]
      end
    end

  end
end

注意:在生产环境这样配置是相当危险的,具体配置请参考官方文档

然后我们生成一个 Blog model

$ rails generate model Blog title:string content:string author_id:integer

然后再添加一个 Author 的 model

rails generate model Author name:string

Blog 属于 Author:

# app/models/blog.rb

class Blog < ActiveRecord::Base
  belongs_to :author
end

Author 有许多 Blogs

# app/models/author.rb

class Author < ApplicationRecord
  has_many :blogs
end

然后,运行:

$ rails db:migrate

到这里为止,我们的 App 的基本内容已经告一段落了。 接下来,我们就开始创建 GraphQL API。

首先,在 Gemfile 里添加GraphQL gem:

gem 'GraphQL'

然后运行$ bundle install, 安装 gem。

我们创建 GraphQL API 的步骤是:

  1. 先创建 Type
  2. 再创建 Schema
  3. 创建 query

首先,我们在app目录下创建graph目录:

$ mkdir -p app/graph
$ mkdir -p app/graph/types

然后在config/application.rb文件里添加以下路径:

# config.application.rb
config.autoload_paths << Rails.root.join('app', 'graph')
config.autoload_paths << Rails.root.join('app', 'graph', 'types')

让我们先创建一个Query Type:

# app/graph/types/query_type.rb

QueryType = GraphQL::ObjectType.defile do
    name "Query"
    description 'The query root for this schema'

    field :blog do
       type BlogType
       argument :id, !types.ID
       resolve -> (obj, args, context) {
          Blog.find args[:id]
       }
    end
end

作为这次查询的根,我们定义了一个字段 blog,它会根据 id 返回一个 Blog 记录。

然后我们继续定义blog_typeauthor_type

# app/graph/types/blog_type.rb

BlogType = GraphQL::ObjectType.define do
   name 'Blog'
   description 'A Blog'
   field :title, types.String
   field :content, types.String
   field :author do
      type AuthorType
      resolve -> (obj, args, context) {
          obj.author
      }
   end
end
# app/graph/types/author_type.rb
AuthorType = GraphQL::ObjectType.define do
   name 'Author'
   description 'Author of Blogs'
   field :name, types.String
end

然后让我们在创建 GraphQL Schema:

# app/graph/blog_schema.rb
BlogSchema = GraphQL::Schema.define do
  query QueryType
end

现在剩下的就是将我们的 GraphQL Schema 的端点暴露出来,最好使用 POST 的方式: 别忘了修改此处:

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  # Use :null_session here
  protect_from_forgery with: :null_session
end

接下来,我们先生成一个QueriesController:

$ rails generate controller Queries create
# app/controllers/queries_controller.rb

class QueriesController < ApplicationController
  def create
    query_string = params[:query]
    query_variables = params[:variables] || {}
    result = BlogSchema.execute(query_string, variables: query_variables)
    render json: result
  end
end

在 config/routes.rb 里添加以下路由:

resources :queries, via: [:post, :options]

That‘s it!

现在我们可以在终端下测试:

$ rails server

打开另一个终端:

$ rails console
> Blog.create title: "Intro to GraphQL", content: "Something something something. Blah blah blah. Etc etc etc."

> exit

$ curl -XPOST -d 'query={ blog(id: 1) { title content }}' http://localhost:3000/queries

返回:

{"data":{"blog":{"title":"Intro to GraphQL","content":"Something something something. Blah blah blah. Etc etc etc."}}}

后记:

几个思考:

  1. 与 ActiveRecord Serialiizer 相比,更灵活
  2. 验证:官方建议验证放在业务逻辑层,因此少量的 REST API 至少目前还是必不可少的。当然不差钱,可以用GraphQL::Pro
  3. 分页:我目前还没有测试,只是根据官方文档理解,无需再引入分页的 gem。因为 GraphQL 有相关的指令来进行分页。
  4. 异常处理:异常可以重写resolve方法,如: 定义个一个RescueFrom类放置到app/classes目录下面:
class RescueFrom
  def initialize error_superclass, resolve_func
    @error_superclass = error_superclass
    @resolve_func = resolve_func
  end

  def call obj, args, ctx
    @resolve_func.call obj, args, ctx
  rescue @error_superclass => err
    puts err.message
  end
end

然后重写blog_type.rb:

BlogType = GraphQL::ObjectType.define do
  name "Blog"
  description "A Blog"
  field :title, types.String
  field :content, types.String
  field :author do
    type AuthorType
    resolve RescueFrom.new(ActiveRecord::RecordNotFound,  -> (obj, args, ctx) {
      obj.author
    })
  end
end

但是,这种解决方法并不优雅,不能像在类似BaseController::ActionController这样的基类里使用rescue_from达到一劳永逸的效果。

总体来说,推荐指数,依然五星,希望大家尝试一下。

可以理解为是一个增强版的 API view 层吗? 复杂的业务逻辑应该写在哪里? 有用在生产环境吗?感觉怎么样?

这个不能比较,有点像给前端写的 ORM

huacnlee 将本帖设为了精华贴。 02月06日 10:15

你们有用到 Relay 吗?

需要 登录 后方可回复, 如果你还没有账号请 注册新账号