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 的步骤是:
- 先创建 Type
- 再创建 Schema
- 创建 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_type
和 author_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."}}}
后记:
几个思考:
- 与 ActiveRecord Serialiizer 相比,更灵活
- 验证:官方建议验证放在业务逻辑层,因此少量的 REST API 至少目前还是必不可少的。当然不差钱,可以用GraphQL::Pro
- 分页:我目前还没有测试,只是根据官方文档理解,无需再引入分页的 gem。因为 GraphQL 有相关的指令来进行分页。
- 异常处理:异常可以重写
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
达到一劳永逸的效果。
总体来说,推荐指数,依然五星,希望大家尝试一下。