jbuilder是Rails開發(fā)者最常用的gem之一了德撬,自不必多說银酬,它可是 API 開發(fā)中的利器末贾,靈活的DSL語法與Rails和Ruby本身很相配揩晴。本文就要探知一下勋陪,Jbuilder的實現(xiàn)原理是什么,以便我們?nèi)蘸罅蚶迹由钊氲氖褂煤烷_發(fā)Rails API應(yīng)用诅愚。
結(jié)構(gòu)
首先打開Jbuilder的lib目錄,也就是主目錄劫映,我們會看到以下文件結(jié)構(gòu):
├── generators
├── jbuilder
└── jbuilder.rb
我們可以看到违孝,lib下由 一個 jbuilder.rb的入口文件和jbuilder,generators 兩個目錄構(gòu)成的泳赋。其中g(shù)enerators是注冊在Rails中的生成器雌桑,因為本文主要介紹的是jbuilder的工作原理,所以我們就把目光放在 jbuilder目錄中祖今。
├── jbuilder
│ ├── dependency_tracker.rb
│ ├── errors.rb
│ ├── jbuilder.rb
│ ├── jbuilder_template.rb
│ ├── key_formatter.rb
│ └── railtie.rb
└── jbuilder.rb
在Jbuilder 的實現(xiàn)中校坑,我們基本上可以將它的功能部分分為:Jbuilder模塊拣技,template模塊和dependency模塊。
下面我們就來依次介紹它們耍目。
Jbuilder
# lib/jbuilder/jbuiler.rb
Jbuilder = Class.new(begin
require 'active_support/proxy_object'
ActiveSupport::ProxyObject
rescue LoadError
require 'active_support/basic_object'
ActiveSupport::BasicObject
end)
# lib/jbuilder.rb
require 'jbuilder/jbuilder'
....
class Jbuilder
@@key_formatter = KeyFormatter.new
@@ignore_nil = false
.....
end
Jbulder類本身是繼承自 ActiveSupport::ProxyBasic類过咬,同時jbuilder使用了打開類的方式,去擴充現(xiàn)有jbuilder類的方法制妄。 繼承ProxyBasic的主要作用就是作為一個潔凈室,讓繼承自它的JbuilderTemplate對象可以通過 method_missnig 去處理非定義方法的調(diào)用泵三。我們這也就道出了Jbuilder及JbuilderTemplate都是使用set! 方法去代理所有未定義的方法耕捞。
alias_method :method_missing, :set!
private :method_missing
最后在set!方法會將數(shù)據(jù)保存在 @attributes 屬性中,之后的操作也都是這樣的步驟烫幕,直到在template_bundler中調(diào)用了jbuilder的target方法 將@attributes轉(zhuǎn)換成json數(shù)據(jù)俺抽。
set!方法最后會調(diào)用_write將鍵值對保存到@attributes屬性中,其中Key還會經(jīng)過@key_formatter進行格式化较曼。
# lib/jbuilder.rb
def _write(key, value)
@attributes = {} if _blank?
@attributes[_key(key)] = value
end
def _key(key)
@key_formatter.format(key) # 每次在調(diào)用json.key_format!是都會重新的實例化一個KeyFormatter磷斧。
end
在下面介紹的Template中你就會看到模板處理器最后會調(diào)用 json.target!方法,然后進行渲染捷犹。
# 將Hash 轉(zhuǎn)換為json字符串返回弛饭。
def target!
::MultiJson.dump(@attributes)
end
Template
Jbuilder本身就是一個Rails的Railtie,并且它在active_view加載完成后萍歉,注冊了 jbuilder 模板處理器 register_template_handler 侣颂,active_view中規(guī)定如果要注冊 模板的需要一個能夠響應(yīng)call方法的處理類,并且call方法要接受一個template對象枪孩,返回一個字符串對象憔晒,然后action_view會將返回的字符串進行eval運行。
# lib/jbuilder/jbuilder_template.rb
class JbuilderHandler
cattr_accessor :default_format
self.default_format = Mime::JSON
def self.call(template)
# this juggling is required to keep line numbers right in the error
%{__already_defined = defined?(json); json||=JbuilderTemplate.new(self); #{template.source}
json.target! unless (__already_defined && __already_defined != "method")}
end
end
call方法返回的字符串蔑舞,是用";"分隔的多條語句拒担,模板的代碼也被插入在其中。其中的json是從JbuilderTemplate類中初始化出來的攻询,這樣jbuilder中 json receiver 就是 JbuilderTemplate的實例了从撼。
JbuilderTemplate 也是繼承與Jbuilder類,它在其中擴充了Jbuilder的功能方法有:
- partial!
- array!
- cache!
- cache_if!
之所以將這幾個方法單獨放在JbuilderTemplate中蜕窿,是因為需要使用ViewContext對象的render方法谋逻,去渲染其他的模板。
#lib/jbuilder/jbuilder_template.rb
def _render_partial(options)
options[:locals].merge! json: self
@context.render options
end
在Railtie中 定義模板處理器
# lib/jbuilder/railtie.rb
...
initializer :jbuilder do |app|
ActiveSupport.on_load :action_view do
# 向View中注冊處理器
ActionView::Template.register_template_handler :jbuilder, JbuilderHandler
# 解決依賴問題
require 'jbuilder/dependency_tracker'
end
end
Dependency
jbuilder 注冊template桐经,同時也使用了毁兆,action view的 dependency_tracker 去管理template中對外依賴。
jbuilder/dependency_tracker.rb 類首先繼承自 ::ActionView::DependencyTracker 然后對其核心的dependencies 方法進行了重載阴挣,讓其支持jbuilder自己的規(guī)范方法气堕。
具體的實現(xiàn)就是,使用正則表達式去在template字符串中匹配,jbuilder自己指定的規(guī)則茎芭。
# lib/jbuilder/dependency_tracker.rb
# Matches:
# json.partial! "messages/message"
# json.partial!('messages/message')
#
DIRECT_RENDERS = /
\w+\.partial! # json.partial!
\(?\s* # optional parenthesis
(['"])([^'"]+)\1 # quoted value
/x
# Matches:
# json.partial! partial: "comments/comment"
# json.comments @post.comments, partial: "comments/comment", as: :comment
# json.array! @posts, partial: "posts/post", as: :post
# = render partial: "account"
#
INDIRECT_RENDERS = /
(?::partial\s*=>|partial:) # partial: or :partial =>
\s* # optional whitespace
(['"])([^'"]+)\1 # quoted value
/x
def dependencies
direct_dependencies + indirect_dependencies + explicit_dependencies
end
private
def direct_dependencies
source.scan(DIRECT_RENDERS).map(&:second)
end
def indirect_dependencies
source.scan(INDIRECT_RENDERS).map(&:second)
end
我們在Rails View中使用 render template 路徑中不帶擴展名就是因為揖膜,擴展名已經(jīng)注冊到register_tracker方法中了,所以在render 的時候梅桩,action view 會自動的在所以注冊的tracker中尋找匹配的文件壹粟。
其他
KeyFormatter
KeyFormatter非常簡單,就是將傳入的key按照上一次設(shè)置好的格式進行格式化宿百。它的具體實現(xiàn)方法就是趁仙。
在json對象上的key_format! 方法傳入的參數(shù),都會傳入到KeyFormatter的構(gòu)造方法中垦页。
# 傳入Proc對象
json.key_format! ->(key){ "_" + key }
# 或是 Symbol Hash
json.key_format! camelize: :lower
# lib/jbuilder/key_formatter.rb
class KeyFormatter
def initialize(*args)
@format = {}
@cache = {}
...
end
end
在經(jīng)過format方法判斷傳入的是Proc還是Symbol 雀费,Proc的的話就執(zhí)行它,Symbol就使用send方法調(diào)用痊焊,并將參數(shù)傳入盏袄。
并且還會將已經(jīng)格式化過的key緩存下來,避免了相同key多次調(diào)用的開銷薄啥。
def format(key)
@cache[key] ||= @format.inject(key.to_s) do |result, args|
func, args = args
if ::Proc === func
func.call result, *args
else
result.send func, *args
end
end
end
Errors
Jbuilder 僅定義了一個異常類辕羽,就是NullError 用于處理為空異常的。
#lib/jbuilder/errors.rb
class NullError < ::NoMethodError
def self.build(key)
message = "Failed to add #{key.to_s.inspect} property to null object"
new(message)
end
end
總結(jié)
Jbuilder使用上非常簡潔靈活的DSL結(jié)構(gòu)垄惧,其實核心就是通過 method_missing 來將數(shù)據(jù)存放在一個Hash中逛漫,最后再將中其轉(zhuǎn)換成JSON數(shù)據(jù),在配合一下Ruby元編程的技巧赘艳,比如:打開類酌毡,動態(tài)派發(fā)等。其設(shè)計上的方式還是很有借鑒意義的蕾管。不愧是Rails官方出品枷踏。