使用ruby開發高性能的API

近期項目中,有一個模塊須要提供API,剛開始使用node.js來開發,但是開發起來很費事,多是我不大習慣javascipt那種恐怖的callback吧,因此動點心思嘗試在ruby使用異步IO的框架來提供API。java

開發中個人目標有:
1. 兼容咱們如今代碼的model
2. 兼容咱們如今的jbuilder 模板
3. 大體上符合rails開發者習慣node

明確目標後,google別人已經實踐過的技巧,而後綜合使用。json

首先咱們看看咱們項目大概的gem:api

gem 'goliath', '~> 1.0.3'
gem 'grape', '~> 0.6.1'
gem 'mongoid', '~> 4.0.0.beta1'
gem 'jbuilder', '~> 2.0.5'
gem 'tilt', '<= 1.4.1'
gem 'tilt-jbuilder', '~> 0.5.3'
gem 'activesupport', '~> 4.0.4'

項目目錄規劃:ruby

  • api 用來放置 api的實現,即 grade 的代碼
  • config 用來放置配置文件

    • application.rb 配置文件
    • mongoid.yml mongoid的配置文件
  • lib 放置和項目無關的代碼

    • core_ext 根據須要拓展內置類
    • grape_ext 更具須要拓展grade
  • models model放置的目錄,基本上直接copy咱們原來的代碼
  • views

    • v1
    • root
    • **
  • Gemfile
  • Rakefile
  • tool
  • server.rb 項目入口

咱們先來看看如何實現server.rb 文件app

require_relative './config/application.rb'
require 'rubygems'
require 'bundler/setup'
Bundler.require

class Server < Goliath::API

  require_relative 'lib/init.rb'
  require_relative 'api/api.rb'

  def response(env)
    API.call env
  end
end

以後咱們須要進入lib/init.rb 寫入一些配置框架

Dir[File.dirname(__FILE__) + '/core_ext/*.rb'].each {|file| require file }
Dir[File.dirname(__FILE__) + '/grape_ext/*.rb'].each {|file| require file }

config_path = File.join(File.dirname(__FILE__), '../config/mongoid.yml')
ENV['MONGOID_ENV'] = ENV['GOLIATH_ENV'] || 'development'

Mongoid.load! config_path

I18n.enforce_available_locales = false

Dir[File.dirname(__FILE__) + '/../models/concerns/*.rb'].each {|file| require file }
Dir[File.dirname(__FILE__) + '/../models/*.rb'].each {|file| require file }

以後咱們添加lib/grape_ext/jbuilder.rb 爲grade 添加jbuilder 的支持,咱們查看了grape-jbuilder的源碼,而後根據須要修改的:less

module Grape
  module Formatter

    class Renderer
      def initialize(view_path, template)
        @view_path, @template = view_path, template
      end

      def render(scope, locals = {})
        engine = ::Tilt.new file, nil, view_path: view_path
        engine.render scope, locals
      end

      private

      attr_reader :view_path, :template

      def file
        File.join view_path, "#{template}.jbuilder"
      end
    end

    class Jbuilder

      attr_reader :env, :endpoint, :object

      def self.call(object, env)
        new(object, env).call
      end

      def initialize(object, env)
        @object, @env = object, env
        @endpoint     = env['api.endpoint']
      end

      def call
        return Grape::Formatter::Json.call(object, env) unless template?

        route_info = env['rack.routing_args'][:route_info]
        namespace = if route_info.route_namespace == '/'
                      'root'
                    else
                      route_info.route_namespace
                    end
        view_path = "#{AppConfig.view_path}/#{route_info.route_version}/#{namespace}/"
        Renderer.new(view_path, template).render(endpoint, {})
      end

      def template
        endpoint.options.fetch(:route_options, {})[:action]
      end

      def template?
        !!template
      end
    end
  end
end

以後在api/api.rb 寫咱們的api吧:)異步

class API < Grape::API
  format :json
  version 'v1'
  formatter :json, Grape::Formatter::Jbuilder

  helpers do
    def current_user
      @current_user ||= User.authorize!(env)
    end

    def authenticate!
      error!('401 Unauthorized', 401) unless current_user
    end
  end

  # 沒有namespace 就會指定到咱們的v1/root裏面去
  get '', action: :show do
    @user = {name: 'manjia'}
  end

  # 異步http例子
  get '/testhttp' do
    conn = Faraday::Connection.new(:url => 'http://127.0.0.1:3000') do |builder|
      builder.use Faraday::Adapter::EMSynchrony
    end
    conn.get "/"
  end

  resource :user do
    post '', action: :sign_up do
      @user = User.new params.permit_hash :name, :email, :password
      if @user.save
        @user.login
        @user
      else
        error!({error: @user.errors}, 402)
      end
    end
  end
end

而後在對應的目錄寫好jbuilder 文件便可,好比上面的:show 那麼就是在views/v1/root/show.jbuilder, 上面的:sign_up 那麼就是在views/v1/user/sign_up.jbuilderpost

ok, 這裏咱們加入一些task方便咱們的開發

require 'rubygems'
require 'bundler/setup'
Bundler.require

desc 'open an console'
task :console do
  require 'irb'
  require 'irb/completion'
  require_relative 'lib/init.rb'
  ARGV.clear
  IRB.start
end

task :server do
  exec 'ruby ./server.rb -sv'
end

到這裏咱們的API服務項目基本搭建完畢,目標也基本實現了。

相關文章
相關標籤/搜索