sinatra [email protected]
DESCRIPTION
介紹Sinatra這套Ruby DSL Framework的基礎, 以及如何透過Sinatra輔助RailsTRANSCRIPT
About me• 鄧慕凡(Mu-Fan Teng)
• a.k.a: Ryudo
• http://twitter.com/ryudoawaru
• Ruby developer since 2007
• Founder of Tomlan Software Studio.
2
11年8月26日星期五
3
11年8月26日星期五
Agenda
• Sinatra Introduction
• Tutorial
• Rails Metal
• Production tips
4
11年8月26日星期五
Why Sinatra
5
11年8月26日星期五
Load lesser, run faster!Rails 3.1預設: 不含project本身約20個middleware
6
11年8月26日星期五
Load lesser, run faster!Sinatra:預設不超過6個
def build(*args, &bk) builder = Rack::Builder.new builder.use Rack::MethodOverride if method_override? builder.use ShowExceptions if show_exceptions? builder.use Rack::CommonLogger if logging? builder.use Rack::Head setup_sessions builder middleware.each { |c,a,b| builder.use(c, *a, &b) } builder.run new!(*args, &bk) builder end
7
11年8月26日星期五
Sinatra
• very micro web framework(少於1600行)
• pure Ruby/Rack
• DSL化設計
8
11年8月26日星期五
完全自訂性• 自己選擇的ORM
• 自己選擇template engine
• 可簡單可複雜的路由設定
9
11年8月26日星期五
適用場合
10
11年8月26日星期五
11
11年8月26日星期五
12
11年8月26日星期五
Sinatra適合• Tiny WEB App
• 聊天室
• Widget
• WEB API
13
11年8月26日星期五
Tutorial開始
14
11年8月26日星期五
Hello Sinatra!require 'rubygems'require 'sinatra'get '/' do 'Hello Sinatra!'end
15
11年8月26日星期五
流程
get ‘/’ROUTES
post ‘/users’
put ‘/users/:id’
get ‘/users/:id’
beforeBEFORE
before ‘/users’
after
AFTER
after ‘/users’
16
11年8月26日星期五
Named Route Params
Sinatra中的Route就如同Rails的Action + Route, 可透過路由表示式的設定將URI字串中符合批配樣式的內容(冒號開頭)化為特定的params hash成員.
17
require 'rubygems'require 'sinatra'
get '/hello/:name' do # => get '/hello/Rubyconf' # params[:name] => Rubyconf "Hello #{params[:name]}!"end
11年8月26日星期五
Splat Param Routeget '/say/*/to/*' do # /say/hello/to/world params[:splat] # => ["hello", "world"]end
get '/download/*.*' do # /download/path/to/file.xml params[:splat] # => ["path/to/file", "xml"]end
在路由表示式中的*號會以陣列成員的形式集中到特定的params[:splat]中.
18
11年8月26日星期五
Route matching with Regular Expressions
get %r{/posts/name-([\w]+)} do # => get /posts/name-abc, params[:captures][0] = 'abc' "Hello, #{params[:captures].first}!"end
get %r{/posts/([\w]+)} do |pid| # => put match content to block param(s) # => matches 「([\w]+)」 to 「pid」end
路由表示式也接受Regular Expression並可將match內容化為特定的params[:captures]陣列成員, 也可直接設定為Block區域變數
19
11年8月26日星期五
Route with block paramaters
get '/thread/:tid' do |tid| # => tid == params[:tid]end
get '/pictures/*.*' do |filename, ext| # => filename == first * # => ext == second * # => GET '/pictures/abc.gif' then filename = "abc" and ext = "gif" "filename = #{filename}, ext = #{ext}"end
get %r{/posts/([\w]+)} do |pid| # => put match content to block param(s) # => matches 「([\w]+)」 to 「pid」end
以上所介紹的路由表示式都可以將匹配的內容指派到區塊的區域變數中
20
11年8月26日星期五
Conditional route
get '/foo', :agent => /MSIE\s(\d.\d+)/ do "You're using IE version #{params[:agent][0]}" # => IE特製版end
get '/', :host_name => /^admin\./ do "Admin Area, Access denied!"end
也可以用user_agent或host_name來做路由啟發的條件; 注意的是agent可以是字串或正規表示
式, hostname只能是正規表示式.21
11年8月26日星期五
Conditions(2)get '/', :provides => 'html' do erb :indexend
get '/', :provides => ['rss', 'atom', 'xml'] do builder :feedend
provides的條件是看accept header而非path
22
11年8月26日星期五
RESTful Routesget '/posts' do # => Get some postsend
get '/posts/:id' do # => Get 1 postend
create '/posts' do # => create a postend
put '/posts/:id' # => Update a postend
delete '/posts/:id' # => Destroy a postend
23
11年8月26日星期五
Route Return Values
1. HTTP Status Code
2. Headers
3. Response Body(Responds to #each)
24
11年8月26日星期五
Http Streamingclass FlvStream #http://goo.gl/B8BdU
....def each ... end
endclass Application < Sinatra::Base # Catch everyting and serve as stream get %r((.*)) do |path| path = File.expand_path(STORAGE_PATH + path) status(401) && return unless path =~ Regexp.new(STORAGE_PATH) flv = FlvStream.new(path, params[:start].to_i) throw :response, [200, {'Content-Type' => 'application/x-flv', "Content-Length" => flv.length.to_s}, flv] endend
由於只要有each這個method就可以當做Response Body, 因此在App Server支援的前提下就可以做出Http Streaming
25
11年8月26日星期五
Template Engines
• 支援數不清的樣板引擎(erb/haml/sass/coffeescript/erubis/builder/markdown...)
• 支援inline template
• 透過Tilt可自訂樣板引擎
26
11年8月26日星期五
Template Eenginesget '/' do haml :index, :format => :html4 # overridden#render '/views/index.haml'end
get '/posts/:id.html' do @post = Post.find(params[:id]) erb "posts/show.html".to_sym, :layout => 'layouts/app.html'.to_sym#render '/views/posts/show.html.erb' with #layout '/views/layouts/app.html.erb'endget '/application.css' do sass :application#render '/views/application.sass'end
所有的view名稱都必需是symbol,不可以是String
27
11年8月26日星期五
Compass Integration
set :app_file, __FILE__set :root, File.dirname(__FILE__)set :views, "views"set :public, 'static'
configure do Compass.add_project_configuration(File.join(Sinatra::Application.root, 'config', 'compass.config'))endget '/stylesheets/:name.css' do content_type 'text/css', :charset => 'utf-8' sass(:"stylesheets/#{params[:name]}", Compass.sass_engine_options )end
28
11年8月26日星期五
Inline Templatesget '/' do erb :"root"end
template :root do<<"EOB"<p>Hello Sinatra!</p>
EOBend
29
11年8月26日星期五
Before filtersbefore '/rooms/:room_id' do puts "Before route「/rooms/*」 only" @room = Room.find(params[:room_id])endbefore do puts "Before all routes"endget '/' do ...endget '/rooms/:room_id' do puts "object variable 「@room」 is accessiable now!" "You are in room #{@room.name}"end
30
11年8月26日星期五
Before filters
• 和路由使用相同表示式• 沒有Rails的「prepend_before_filter」
• 先載入的先執行
31
11年8月26日星期五
Before filter orderbefore '/rooms/:room_id' do # 先執行 # Before route「/rooms/*」 only @room = Room.find(params[:room_id])endbefore do # 後執行 # Before all routesendget '/' do ...endget '/rooms/:room_id' do # object variable 「@room」 is accessiable now! "You are in room #{@room.name}"end
32
11年8月26日星期五
Session
enable :sessions # equal to 「use Rack::Session::Cookie」
post '/sessions' do @current_user = User.auth(params[:account], params[:passwd]) session[:uid] = @current_user.id if @current_userenddelete '/sessions' do session[:uid] = nil redirect '/'end
33
11年8月26日星期五
Cookiesget '/' do response.set_cookie('foo', :value => 'BAR') response.set_cookie("thing", { :value => "thing2", :domain => 'localhost', :path => '/', :expires => Time.today, :secure => true, :httponly => true })end
get '/readcookies' do cookies['thing'] # => thing2end
34
11年8月26日星期五
Helpers
helpers do def member2json(id) Member.find(id).attributes.to_json endendget '/members/:id.json' do member2json(params[:id])end
35
11年8月26日星期五
HelpersHelpers can use in:
1. filters
2. routes
3. templates
36
11年8月26日星期五
Halt & Pass
• Halt
• 相當於控制結構中的break, 阻斷後續執行強制回應.
• Pass
• 相當於控制結構中的next.
37
11年8月26日星期五
Halt Examplehelpers do def auth unless session[:uid] halt 404, 'You have not logged in yet.' end endend
before '/myprofile' do authend
get '/myprofile' do #如果session[:uid]為nil,則此route不會執行end
38
11年8月26日星期五
Pass Exampleget '/checkout' do pass if @current_member.vip? #一般客結帳處理 erb "normal_checkout".to_symendget '/checkout' do #VIP專用結帳處理 erb 'vip_checkout'.to_symend
39
11年8月26日星期五
body,status,headers
get '/' do status 200 headers "Allow" => "BREW, POST, GET, PROPFIND, WHEN" body 'Hello' # => 設定body body "#{body} Sinatra" # => 加料bodyend
40
11年8月26日星期五
url and redirect
get '/foo' do redirect to('/bar')end
url(別名to)可以生成包含了baseuri的url; redirect則同其名可進行http重定向並可附加訊息或狀態碼.
41
11年8月26日星期五
send_fileget '/attachments/:file' do send_file File.join('/var/www/attachments/', params[:file]) #可用選項有 # filename:檔名 # last_modified:顧名思義, 預設值為該檔案的mtime # type:內容類型,如果沒有會從文件擴展名猜測。 # disposition:Content-Disposition, 可能的包括: nil (默認), :attachment (下載附件) 和 :inline(瀏覽器內顯示) # length:Content-Length,預設為檔案sizeend
另⼀一個helper: attachment等於send_file的disposition為:attachement的狀況.
42
11年8月26日星期五
request物件
# 在 http://example.com/example 上運行的應用get '/foo' do request.body # 被客戶端設定的請求體(見下) request.scheme # "http" request.script_name # "/example", 即為SUB-URI request.path_info # "/foo" request.port # 80 request.request_method # "GET" request.query_string # "" 查詢參數 request.content_length # request.body的長度 request.media_type # request.body的媒體類型end
43
11年8月26日星期五
settings
#以下三行都是同一件事set :abc, 123set :abc => 123settings.abc = 123###可用區塊######set(:foo){|val| puts(val) }get '/' do settings.foo('Sinatra') #will puts "Sinatra" "setting abc = #{settings.abc}"end
Sinatra提供了多種方式讓你在Class Scope和Request Scope都能取用與設定資料或區塊, 其中有⼀一些預設的settings是有關系統運作與設定的.
44
11年8月26日星期五
重要的setting• public(public)
• 指定public目錄的位置
• views(views)
• 指定template/views目錄位置
• static(true)
• 是否由Sinatra處理靜態檔案, 設為false交給WEB伺服器會增強效能
• lock(false)
• 設定false開啟thread模式(單⼀一行程⼀一次處理多個requests)
• methid_override(視情況而定)
• 開始「_method」參數以使用get/post以外的http method
• show_exceptions(預設值與environment有關)
• 是否像rails⼀一樣顯示error stack
45
11年8月26日星期五
settings的特別用途set(:probability) { |value| condition { rand <= value } }
get '/win_a_car', :probability => 0.1 do "You won!"end
get '/win_a_car' do "Sorry, you lost."end
condition是⼀一個Sinatra內建的method,可以視傳入區塊的執行結果為true或false決定視否執行該route或pass掉.
46
11年8月26日星期五
Configure Blockconfigure do # 直接設定值 set :option, 'value' # 一次設定多個值 set :a => 1, :b => 2 # 等於設定該值為true enable :option # 同上 disable :option # 可用區塊 set(:css_dir) { File.join(views, 'css') }end
configure :production do # 可針對環境(RACK_ENV)做設定 LOGGER.level = Logger::WARNend
get '/' do settings.a? # => true settings.a # => 1end
類似Rails的environment.rb, 在行程啟動時執行⼀一次.47
11年8月26日星期五
錯誤處理1. not_found
2. 特定error class
3. http status code
4. 對應所有錯誤
48
11年8月26日星期五
error block
error do #透過env['sinatra.error'] 可取得錯誤物件 'Sorry there was a nasty error - ' + env['sinatra.error'].nameend
error 處理區塊在任何route或filter拋出錯誤的時候會被調用。 錯誤物件可以通過sinatra.error的env hash項目取得, 可以使用任何在錯誤發生前的filter或route中定義的instance variable及環境變數等
49
11年8月26日星期五
自定義error block
error MyCustomError do 'So what happened was...' + env['sinatra.error'].message #輸出為:「 So what happened was... something bad」end
get '/' do raise MyCustomError, 'something bad'end
如同Ruby的rescue區塊, error處理⼀一樣可以針對error class做定義;也可以在執行期故意啟發特定error class的錯誤並附加訊息.
50
11年8月26日星期五
自定義error block
error 403 do 'Access forbidden'end
get '/secret' do 403enderror 400..510 do 'Boom'end
針對特定的HTTP CODE設定錯誤處理區塊, 可以是代碼範圍
51
11年8月26日星期五
not_found block
not_found do 'This is nowhere to be found'end
not_found區塊等於 error 404區塊
52
11年8月26日星期五
Rack Middleware
和Rails⼀一樣, Sinatra也是基於Rack的middleware, 所以可以使用其它的Rack middleware.
require 'rubygems'require 'sinatra'
use Rack::Auth::Basic, "Restricted Area" do |username, password| [username, password] == ['admin', '12345']end
get '/' do "You are authorized!"end
53
11年8月26日星期五
模組化
為了建構可重用的組件,需要將你的Sinatra應用程式模組化以將程式化為⼀一個獨立的Rack Middleware.
require 'rubygems'require 'sinatra/base'#不可以require "sinatra" 以避免頂層Object Class被汙染class MyApp < Sinatra::Base set :sessions, true set :foo, 'bar' get '/' do 'Hello world!' endend
54
11年8月26日星期五
Multiple App in 1 process
利用Rack::Builder可將不同的Rack App掛在不同的uri下面.
#config.rurequire 'rubygems'require 'sinatra/base'class App1 < Sinatra::Base get '/' do 'I am App1' endendclass App2 < Sinatra::Base get '/' do 'I am App2' endendmap '/' do run App1endmap '/app2' do run App2end
55
11年8月26日星期五
何時需要模組化• 特定的Rack App Server(Passenger/Heroku等)
• 將你的Sinatra App當做⼀一個Middleware而非終點(endpoint), 例如:1. ⼀一次掛載多個Sinatra App在同⼀一個rackup
2. 在Rails內掛載Sinatra App
56
11年8月26日星期五
Rails Metal
#routes.rbTestMixin::Application.routes.draw do devise_for :users mount ApiApp, :at => '/api'# => 掛載 ApiApp在/api 下 root :to => "welcome#index"end#lib/api_app.rbclass ApiApp < Sinatra::Base get '/users/:id.json' do User.find(params[:id]).attributes.to_json endend
57
11年8月26日星期五
限制• 「可以」使用Rails的model
• 「不可以」使用Rails的helper/controller
• 「可能可以」使用Rails的views, 但極不建議
• not_found的錯誤是「由Rails端」處理
• 「不可以」直接使用綁定Rails(Railties)的組件
58
11年8月26日星期五
Devise Mix-in
#config/routes.rbTestMixin::Application.routes.draw do devise_for :users mount ApiApp, :at => '/api'# => 掛載 ApiApp在/api 下end#lib/api_app.rbclass ApiApp < Sinatra::Base get '/users/:id.json' do #等效 devise的 authenticate_user! request.env['warden'].authenticate!(:scope => 'user') User.find(params[:id]).attributes.to_json endend
由於Devise是建構在Warden(Rack Middleware)之上,雖然不能直接使用Devise的認證helper, 但可以用warden的方式來處理認證的問題.
59
11年8月26日星期五
Scopes and Binding
• Sinatra的變數定義域分成兩種1. Application/Class Scope
2. Request/Instance Scope
60
11年8月26日星期五
Scopes-範例class MyApp < Sinatra::Base # => Application/Class Scope configure do # => Application/Class Scope set :foo, 100 end self.foo # => 100 helpers do # => Application/Class Scope def foo # => Request scope for all routes end end get '/users/:id' do # => Request scope for "/users/:id" only settings.foo # => 100 end get '/' do # => Request scope for '/' only endend
Request scope可透過settings helper取得在Application scope定義的設定值
61
11年8月26日星期五
Application/Class scope1. 應用程式的Class Body
2. helpers/configure的區塊內3. 傳遞給set的區塊
62
11年8月26日星期五
Request/Instance scope1. session/request物件只在這裡有效2. 作用於:
2.1.routes區塊2.2.helper methods
2.3.view/templates
2.4.before/after filters
2.5.error block
2.6. 傳給settings的區塊中的condition區塊
63
11年8月26日星期五
實體變數的定義範圍class MyApp < Sinatra::Base before do # => Request scope for all routes @varall = 100 end before '/posts/:id' do @post = Post.find(params[:id]) end get '/posts/:id' do # => Request scope for 「/posts/:id」 @post.nil? # => false @varall # => 100 settings.get('/foo'){ # => Request scope for 「/foo」 only @varall # => 100 @post.nil? # => true } end get '/' do @varall # => 100 @post # => nil endend
就算是在route block中定義的另⼀一個route block, ⼀一樣不能共用實體變數, 在before filter中定義的實體變數會傳到下⼀一個符合條件的before filter與route block.
64
11年8月26日星期五
存在Class Body中的Request Scope
#####sinatra/base.rb##module Sinatra class Base class << self def host_name(pattern) condition { pattern === request.host } end end endend##等同於自行定義#############set(:host_name){|pattern| condition { pattern === request.host }}######################
get '/', :host_name => /^admin\./ do "Admin Area, Access denied!"end
傳遞給condition method的區塊內的scope是Request Scope, 所以可以使用request物件
65
11年8月26日星期五
Production Tips1. ORM
2. Useful Extensions
3. Paginators
4. boot.rb
66
11年8月26日星期五
ActiveRecord
require 'rubygems'require 'sinatra'require 'active_record'
#先建立連線ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => 'sinatra_application.sqlite3.db')#require或宣告classclass Post < ActiveRecord::Baseend
get '/' do @posts = Post.all() erb :index end
67
11年8月26日星期五
Mongoid
require 'rubygems'require 'sinatra'require 'mongoid'Mongoid.configure do |config| config.master = Mongo::Connection.new.db("godfather")endclass User include Mongoid::Document include Mongoid::Timestampsendget '/users/:id.json' do User.find(params[:id]).to_jsonend
68
11年8月26日星期五
Sinatra More• MarkupPlugin
• 設定form以及html tag的helper等
• RenderPlugin
• content_for/yield等
• WardenPlugin
• 綁定單⼀一Class做登入處理
• MailerPlugin
• 顧名思義, 但不是使用ActionMailer
• RoutingPlugin
• 做出有namespace以及restful的路由
• code generator
• 建立project框架
69
11年8月26日星期五
WillPaginate
• 3.0後版本已直接以extension型式支援Sinatra
• 2.3版本需要手動改寫renderer
• 由於Sinatra不像Rails有固定的url形式約束, 必要時還是要自己改寫renderer
70
11年8月26日星期五
Kaminari
• Model Paginate功能可正常使用
• View Helpers的部份無法使用
• 需依照ORM require正確的組件
71
11年8月26日星期五
boot.rbrequire 'bundler'Bundler.setupBundler.requireclass FreeChat3 < Sinatra::Base configure do #settings set :sessions, true #Middlewares use Rack::Flash #Sinatra Extensions register SinatraMore::MarkupPlugin #DB Connections ActiveRecord::Base.establish_connection(DB_CONFIG[RACK_ENV]) Mongoid.load! File.join(ROOT_DIR, '/config/mongoid.yml') #load Model/Controller/helper/Libs Dir.glob(File.join(ROOT_DIR, '/lib/*.rb')).each{|f| require f } Dir.glob(File.join(ROOT_DIR, '/app/models/*.rb')).each{|f| require f } Dir.glob(File.join(ROOT_DIR, '/app/helpers/*.rb')).each{|f| require f } Dir.glob(File.join(ROOT_DIR, '/app/controllers/*.rb')).each{|f| load f } end helpers do include ApplicationHelper include ActionView::Helpers::TextHelper end end
72
11年8月26日星期五
Q&A
73
11年8月26日星期五