Ch1 Zero to “It Works!”
Gem的檔案結構
$ bundle gem rulers
create rulers/Gemfile
create rulers/Rakefile
create rulers/LICENSE.txt
create rulers/README.md
create rulers/.gitignore
create rulers/rulers.gemspec # !!
create rulers/lib/rulers.rb # !!
create rulers/lib/rulers/version.rb
Initializating git repo in src/rulers
rulers/rulers.gemspec
放的是gem的資訊與dependency
rulers/lib/rulers.rb
就是主程式
dependency分成development與runtime
# rulers.gemspec
gem.add_development_dependency "rspec"
gem.add_runtime_dependency "rest-client"
rack進入點 與 rack app回傳值的資料結構
# best_quotes/config.ru
run proc {
[200, {'Content-Type' => 'text/html'}, ["Hello, world!"]]
}
狀態值,header,資料
rack app 的 interface
# rulers/lib/rulers.rb
require "rulers/version"
module Rulers
class Application
def call(env)
[200, {'Content-Type' => 'text/html'}, ["Hello from Ruby on Rulers!"]]
end
end
end
要有call的method,吃的是來自rack的env
new是編譯期的args,call是runtime的args
# best_quotes/config.ru
require './config/application'
run BestQuotes::Application.new
Ch2 Your First Controller
# rulers/lib/rulers.rb
require "rulers/version"
require "rulers/routing"
module Rulers
class Application
def call(env)
klass, act = get_controller_and_action(env)
controller = klass.new(env)
text = controller.send(act)
[200, {'Content-Type' => 'text/html'}, [text]]
end
def get_controller_and_action(env)
_, cont, action, after = env["PATH_INFO"].split('/', 4)
cont = cont.capitalize # "People"
cont += "Controller" # "PeopleController"
[Object.const_get(cont), action]
end
end
class Controller
def initialize(env)
@env = env
end
def env
@env
end
end
end
觀察
get_controller_and_action
:: env -> (Class, Symbol)Controller
:: env -> obj(一堆action)
Ch3 Rails Automatic Loading
const_missing
class Object
def self.const_missing(c)
require "./bobo"
Bobo
end
end
Bobo.new.print_bobo
CamelCase and snake_case
# rulers/lib/rulers/util.rb
module Rulers
def self.to_underscore(string)
string.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
end
end
auto-load
# rulers/lib/rulers/dependencies.rb
class Object
def self.const_missing(c)
require Rulers.to_underscore(c.to_s)
Object.const_get(c)
end
end
# best_quotes/config/application.rb
require "rulers"
$LOAD_PATH << File.join(File.dirname(__FILE__),"..", "app","controllers") #require要的
# --> No more require here! <--
module BestQuotes
class Application < Rulers::Application
end
end
Ch4 Rendering Views
erb
# some_directory/erb_test.rb
require "erubis"
template = <<TEMPLATE
Hello! This is a template.
It has <%= whatever %>.
TEMPLATE
eruby = Erubis::Eruby.new(template)
puts eruby.src
puts "=========="
puts eruby.result(:whatever => "ponies!")
_buf = '';
_buf << 'Hello! This is a template. It has ';
_buf << ( whatever ).to_s; _buf << '.';
_buf.to_s
==========
Hello! This is a template.
It has ponies!.
render
# rulers/lib/rulers/controller.rb (excerpt)
def render(view_name, locals = {})
filename = File.join "app", "views","#{view_name}.html.erb"
template = File.read filename
eruby = Erubis::Eruby.new(template)
eruby.result locals.merge(:env => env)
end
locals是controller的資料 env是來自rack的資料
觀察
- parse -> proc having lots of string -> eval -> string
Ch5 Basic Models
skip
Ch6 Request, Response
Request
module Rulers
class Controller
def request
@request ||= Rack::Request.new(@env)
end
def params
request.params
end
end
end
從 env
得到 Request
再從中得到 params
Response
module Rulers
class Controller
#private
def response(text, status = 200, headers = {})
raise "Already responded!" if @response
a = [text].flatten
@response = Rack::Response.new(a, status, headers)
end
def render(view_name, locals = {})
filename = File.join "app", "views","#{view_name}.html.erb"
template = File.read filename
eruby = Erubis::Eruby.new(template)
eruby.result locals.merge(:env => env)
end
#public
def get_response # Only for Rulers
@response
end
def render_response(*args)
response(render(*args))
end
end
end
response
產生three tuple,就是rack app真正要回傳的東西
Ch7 The Littlest ORM
Migration 之後,DB做事之前
# best_quotes/mini_migration.rb
require "sqlite3"
conn = SQLite3::Database.new "test.db"
conn.execute <<SQL
create table my_table (
id INTEGER PRIMARY KEY,
posted INTEGER,
title VARCHAR(30),
body VARCHAR(32000));
SQL
Model
# rulers/lib/rulers/sqlite_model.rb
require "sqlite3"
require "rulers/util"
DB = SQLite3::Database.new "test.db"
module Rulers
module Model
class SQLite
def initialize(data = nil)
@hash = data
end
# table's meta data
def self.table
Rulers.to_underscore name
end
def self.schema
return @schema if @schema
@schema = {}
DB.table_info(table) do |row|
@schema[row["name"]] = row["type"]
end
@schema
end
# ruby val -> sql's ds in ruby string
def self.to_sql(val)
case val
when Numeric
val.to_s
when String
"'#{val}'"
else
raise "Can't change #{val.class} to SQL!"
end
end
# 產生新model for a row
def self.create(values)
values.delete "id"
keys = schema.keys - ["id"]
vals = keys.map do |key|
values[key] ? to_sql(values[key]) : "null"
end
DB.execute <<SQL
INSERT INTO #{table} (#{keys.join ","})
VALUES (#{vals.join ","});
SQL
data = Hash[keys.zip vals]
sql = "SELECT last_insert_rowid();"
data["id"] = DB.execute(sql)[0][0]
self.new data
end
def self.find(id)
row = DB.execute <<SQL
select #{schema.keys.join ","} from #{table}
where id = #{id};
SQL
data = Hash[schema.keys.zip row[0]]
self.new data
end
# operate on DB directly
def self.count
DB.execute(<<SQL)[0][0]
SELECT COUNT(*) FROM #{table}
SQL
end
def save!
unless @hash["id"]
self.class.create
return true
end
fields = @hash.map do |k, v|
"#{k}=#{self.class.to_sql(v)}"
end.join ","
DB.execute <<SQL
UPDATE #{self.class.table}
SET #{fields}
WHERE id = #{@hash["id"]}
SQL
true
end
def save
self.save! rescue false
end
# operate on model's hash (no DB business)
def [](name)
@hash[name.to_s]
end
def []=(name, value)
@hash[name.to_s] = value
end
end
end
end
method_missing & define_method
Model的accessor!!
class MyClass
["foo", "bar"].each do |method|
define_method(method) {
puts "Obj #{method}"
}
end
end
myobj = MyClass.new
myobj.foo
myobj.bar
When you call a method that doesnʼt exist on an object, its method called “method_missing” will be called to tell it so. If you override method_missing to check for column names, you can instead just return the column value from method_missing, which will return it from the original call.
Ch8 Rack Middleware
rack app的interface(type signature) & 如何疊app
# sample_dir/config.ru
class Canadianize
def initialize(app, arg = "")
@app = app # next rack app
@arg = arg # args
end
def call(env) # env -> [ status, headers, content ]
status, headers, content = @app.call(env)
content[0] += @arg + ", eh?"
[ status, headers, content ]
end
end
use Canadianize, ", simple"
run proc {
[200, {'Content-Type' => 'text/html'}, ["Hello, world"]]
}
map
# sample_dir/config.ru
require "rack/lobster"
use Rack::ContentType
map "/lobster" do
use Rack::ShowExceptions
run Rack::Lobster.new
end
map "/lobster/but_not" do
run proc {
[200, {}, ["Really not a lobster"]]
}
end
run proc {
[200, {}, ["Not a lobster"]]
}
If thereʼs could be two that match, the longer path is checked first.
Ch9 Real Routing
Controller Actions are Rack Apps
Before
module Rulers
class Application
def call(env)
if env['PATH_INFO'] == '/favicon.ico'
return [404, {'Content-Type' => 'text/html'}, []]
end
klass, act = get_controller_and_action(env)
controller = klass.new(env) # get env from the previous line, so this line should be eliminated
text = controller.send(act) # should not expose how to execute an action
if controller.get_response # this is controller(rack app)'s responsibility
st, hd, rs = controller.get_response.to_a
[st, hd, [rs.body].flatten]
else
[200, {'Content-Type' => 'text/html'}, [text]]
end
# rack_app = klass.action(act)
# rack_app.call(env)
end
end
end
After
# rulers/lib/rulers.rb
module Rulers
class Application
def call(env)
if env['PATH_INFO'] == '/favicon.ico'
return [404, {'Content-Type' => 'text/html'}, []]
end
klass, act = get_controller_and_action(env)
rack_app = klass.action(act)
rack_app.call(env)
end
end
end
# rulers/lib/rulers/controller.rb
module Rulers
class Controller
include Rulers::Model
def initialize(env)
@env = env
@routing_params = {} # Add this line!
end
def dispatch(action, routing_params = {})
@routing_params = routing_params
text = self.send(action)
if get_response
st, hd, rs = get_response.to_a
[st, hd, [rs].flatten]
else
[200, {'Content-Type' => 'text/html'}, [text].flatten]
end
end
def self.action(act, rp = {})
proc { |e| self.new(e).dispatch(act, rp) }
end
def params
request.params.merge @routing_params
end
end
end
Route 也就是 match
Usuge
# best_quotes/config.ru
require "./config/application"
app = BestQuotes::Application.new
use Rack::ContentType
app.route do
match "", "quotes#index"
match "sub-app", proc { [200, {}, ["Hello, sub-app!"]] }
# default routes
match ":controller/:id/:action"
match ":controller/:id", :default => { "action" => "show" }
match ":controller", :default => { "action" => "index" }
end
run app
match & routeObject
# rulers/lib/rulers/routing.rb
class RouteObject
def initialize
@rules = []
end
def match(url, *args)
options = {}
options = args.pop if args[-1].is_a?(Hash)
options[:default] ||= {}
dest = nil
dest = args.pop if args.size > 0
raise "Too many args!" if args.size > 0
parts = url.split("/")
parts.select! { |p| !p.empty? }
vars = []
regexp_parts = parts.map do |part|
if part[0] == ":"
vars << part[1..-1]
"([a-zA-Z0-9]+)"
elsif part[0] == "*"
vars << part[1..-1]
"(.*)"
else
part
end
end
regexp = regexp_parts.join("/")
@rules.push({
:regexp => Regexp.new("^/#{regexp}$"),
:vars => vars,
:dest => dest,
:options => options,
})
end
def check_url(url)
@rules.each do |r|
m = r[:regexp].match(url)
if m
options = r[:options]
params = options[:default].dup
r[:vars].each_with_index do |v, i|
params[v] = m.captures[i]
end
dest = nil
if r[:dest]
return get_dest(r[:dest], params)
else
controller = params["controller"]
action = params["action"]
return get_dest("#{controller}" +
"##{action}", params)
end
end
end
nil
end
def get_dest(dest, routing_params = {})
return dest if dest.respond_to?(:call)
if dest =~ /^([^#]+)#([^#]+)$/
name = $1.capitalize
cont = Object.const_get("#{name}Controller")
return cont.action($2, routing_params)
end
raise "No destination: #{dest.inspect}!"
end
end
# And now, the Application:
module Rulers
class Application
def route(&block)
@route_obj ||= RouteObject.new
@route_obj.instance_eval(&block)
end
def get_rack_app(env)
raise "No routes!" unless @route_obj
@route_obj.check_url env["PATH_INFO"]
end
end
end
Conclusion
from env to res
env -> router -> Controller -----------------------> (Real)Controller -> res
/ \ / \
| |
env req -> parmas
/ \ / \
| |
env router
/ \
|
env
基本上這本書的九個把Rails的核心精神都帶到了,這裡是縮減版的rails與上面的很像,不過某些部分多了一些包裝。
Appendix: Unobtrusive JavaScript
JavaScript 不再需要與 HTML 混在一起
原本是
<input type="text" name="date" onchange="validateDate()"/>
變成
<input type="text" name="date" id="date" />
window.addEventListener("DOMContentLoaded",function(event){
document.getElementById('date').addEventListener("change",validateDate);
});
先找出要的DOM,之後再JS中設定該DOM。
找出要的DOM,可透過id或class,甚至是DOM的traversal
原本但是找到之後,要怎麼儲存這個DOM的狀態??
原本都是在JS中來保存,但這樣就會變成DOM的狀態與程式邏輯的狀態會混在一起。
所以之後有了data attributes
!!