《rails实战圣经》

Updated at: June 23, 2017

ihower

软件框架(Software framework)是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品。

非常多的Web框架都实践一个叫做MVC的软件架构设计模式,将软件分成三个部分:

  • Model物件包装了资料与商业逻辑,例如操作数据库
  • View表示使用者接口,显示及编辑表单,可内嵌Ruby程式的HTML
  • Controller负责将资料送进送出Model,处理从外界(也就是浏览器)来的HTTP Request请求,与Model互动后输出View(也就是HTML)

ORM(Object-relational mapping)可以用物件导向语法来操作关联式数据库,容易使用、撰码十分有效率,不需要撰写繁琐的SQL语法,也增加了程式码维护性。

# SQL
SELECT * FROM orders, users WHERE orders.user_id = users.id AND orders.status = "Paid" LIMIT 5 ORDER BY orders.created_at;

# Rails
Order.where(:status => "paid").includes(:user).limit(5).order("created_at")

指定任意URL对应到任一个Controller的动作,跟档案位置是无关的

附带了非常多开发Web会用到的函式库,例如Template、Email、Session、快取、JavaScript/Ajax、测试等

==================================================

  • 不要重复自己(DRY: Don’t Repeat Yourself)

    撰写出重复的程式码是件坏事

  • 惯例胜于设定(Convention Over Configuration)

    Rails会默认各种好的设定跟惯例,而不是要求你设定每一个细节到设定档中。

  • REST是网站应用程式的最佳模式

    使用Resources和标准的HTTP verbs(动词)来组织你的应用程式是最快的方式(我们会在路径一章详细介绍这个强大的设计)

CakePHP、Grails、TurboGears、Pylons、web2py、catalyst …

市面上流行成熟的CMS系统多为PHP写成,例如Drupal、WordPress等。当然也有用Ruby写的,例如Radiant。如果这些架站系统刚好符合你的需求,那就不一定需要Rails。

在硬件资源有限的行动装置及嵌入式系统上,仍是静态语言的天下

Ruby也是目前做Domain-specific language(DSL),特别是Internal DSL最为成功的程式语言。透过DSL,程式不但可以拥有非常好的可读性,也可以大幅增加生产力。成功的DSL函式库例如有:Rake建构工具、RSpec测试工具、Chef服务器设定工具、Cucumber验收测试等。

JRuby与Ruby最大的差异在于一些需要编译的RubyGem套件:有些因为效能要求而用C语言撰写的RubyGem在JRuby上不一定能够安装使用。

Rails支援的数据库包括SQLite3、MySQL、Postgres、IBM DB2、Oracle和SQL Server等。除了安装数据库软件,我们也需要安装搭配的Ruby函式库(称作Adapter或Driver)

Ruby Toolbox Ruby Gemsorg

# .gemrc
gem: --no-ri --no-rdoc
    or
gem install rails --no-ri --no-rdoc

--skip-test-unit

Gemfile 设定Rails应用程式会使用哪些Gems套件
README
Rakefile 用来加载可以被命令行执行的一些Rake任务
app/ 放Controllers、Models和Views
config/ 应用程式设定、路由规则、数据库设定等等
config.ru 用来启动Rack服务器的设定
db/
doc/ 你的文件
lib/ 放一些自定的Module和类别档案
log/
public/ 唯一可以在网络上看到的目录, 是你的图档、JavaScript、CSS和其他静态档案摆放的地方
bin/ 放rails这个指令和放其他的script指令
test/ 单元测试、fixtures及整合测试等
tmp/
vendor/ 第三方(gem..)外挂的目录

Ubuntu上默认没有任何JavaScript直译器可以给Rails使用。你可以装Node.js或是安装therubyracer这个Ruby套件来获得JavaScript直译器。

get "welcome/say_hello" => "welcome#say"
get "welcome" => "welcome#index"

root :to => "welcome#index"

YAML是一种可读性高,用来表达设定资料的资料格式。它严格要求缩排(建议为两个空白),且冒号后面必须有一个空格。一般我们会预期YAML的值解析出来是字串,因此如果内容是数字或多行文字时,建议加上引号以避免字串解析错误。例如 password: “123456”。如果没有加上引号,这串数字会被解析成Fixnum物件而不是字串String,后续可能造成型别判断错误。

请注意Model的名称是用单数person,而Controller照RESTful惯例是用复数people

单独用ruby -w去执行发生错误的程式,例如ruby -w app/controller/welcome_controller,这会打开Ruby的警告模式来获得更准确的语法错误讯息。

==================================================

ActiveRecord的资料验证(Validation)功能,可以帮助我们检查资料的正确性, errors.full_messages

外卡路由 match ‘:controller(/:action(/:id(.:format)))’, :via => :all

<p><%= link_to 'Back to index', :controller => 'events', :action => 'index' %></p>

<%= form_for @event, :url => { :controller => 'events', :action => 'update', :id => @event } do |f| %>
    <%= f.label :name, "Name" %>
    <%= f.text_field :name %>

    <%= f.label :description, "Description" %>
    <%= f.text_area :description %>

    <%= f.submit "Update" %>
<% end %>

def update
    @event = Event.find(params[:id])
    @event.update(event_params)

    redirect_to :action => :show, :id => @event
end

ActiveModel::ForbiddenAttributesError in EventsController#create的错误讯息,这是因为Rails会检查使用者传进来的参数必须经过一个过滤的安全步骤,这个机制叫做Strong Parameters

def create
    @event = Event.new(event_params)
    @event.save

    redirect_to :action => :index
end

private

def event_params
    params.require(:event).permit(:name, :description)
end

透过require和permit将params这个Hash过滤出params[:event][:name]和params[:event][:description]

Layout可以用来包裹样板,让不同样板共享相同的HTML开头和结尾部分。当Rails要显示一个样板给浏览器时,它会将样板的HTML放到Layout的HTML之中

# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
    <title><%= @page_title || "Event application" %></title>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body>

    <%= yield %>

    </body>
</html>

如果你需要建立任意字段的HTML表单,而不绑在某一个Model上,你可以使用form_tag函式

利用局部样板(Partial)机制,我们可以将重复的样板独立出一个单独的档案,来让其他样板共享引用

# _form.html.erb
<%= f.label :name, "Name" %>
<%= f.text_field :name %>

<%= f.label :description, "Description" %>
<%= f.text_area :description %>

# new.html.erb
<%= form_for @event, :url => { :controller => 'events', :action => 'create' } do |f| %>
    <%= render :partial => 'form', :locals => { :f => f } %>
    <%= f.submit "Create" %>
<% end %>

透过before_action,我们可以将Controller中重复的程式独立出来

讯息会被储存在Rails的特殊flash变量中,好让讯息可以被带到另一个 action,它提供使用者一些有用的资讯

kaminari这个分页套件:

def index
    @events = Event.page(params[:page]).per(5)
end

<%= paginate @events %>

==================================================

什么是REST呢?表象化状态转变Representational State Transfer,简称REST,是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。相较于SOAP、XML-RPC更为简洁容易使用,也是众多网络服务中最为普遍的API格式,像是Amazon、Yahoo!、Google等提供的API服务均有REST接口

  1. 使用Resource来当做识别的资源,也就是使用一个URL网址来代表一个Resource
  2. 同一个Resource则可以有不同的Representations格式变化
resources :events

get    '/events'          => "events#index",   :as => "events"
post   '/events'          => "events#create",  :as => "events"
get    '/events/:id'      => "events#show",    :as => "event"
patch  '/events/:id'      => "events#update",  :as => "event"
put    '/events/:id'      => "events#update",  :as => "event"
delete '/events/:id'      => "events#destroy", :as => "event"
get    '/events/new'      => "events#new",     :as => "new_event"
get    '/events/:id/edit' => "events#edit",    :as => "edit_event"
Helper GET POST PATCH/PUT DELETE  
event_path(@event) /events/1 show   /events/1 update /events/1 destroy  
events_path /events index /events create      
edit_event_path(@event) /events/1/edit edit        
new_event_path /events/new new        

respond_to可以让我们在同一个Action中,支援不同的资料格式,例如XML、JSON、Atom等

Atom是一种基于XML的供稿格式,被设计为RSS的替代品,广泛应用于Blog feed

def index
    @events = Event.page(params[:page]).per(5)

    respond_to do |format|
        format.html # index.html.erb
        format.xml { render :xml => @events.to_xml }
        format.json { render :json => @events.to_json }
        format.atom { @feed_title = "My event list" } # index.atom.builder
    end
end

# http://localhost:3000/events.xml、http://localhost:3000/events.json、http://localhost:3000/events.atom

产生JSON还有其他方式,除了呼叫to_json或手动转Hash物件来自订格式之外,Rails有内建JSON专用的template引擎叫做jbuilder,你可以产生一个app/views/events/show.json.jbuilder的档案来产生JSON。如果需求是要制作给第三方或手机的Web APIs,那么我们就会改用jbuilder样板的方式,这样比较好写和好维护

如果想要加上这些格式的超连结,可以在URL Helper中传入:format参数

<%= link_to " (XML)", event_path(event, :format => :xml) %>
<%= link_to " (JSON)", event_path(event, :format => :json) %>

行数统计: rake stats

如何除错? 如果是Model中的程式,你可以在命令列下输入rails console,然后在Console中呼叫看看Model的方法看看正确与否。而除错Controller和Views一个简单的方法是你可以使用debug这个Helper方法,例如在app/views/events/show.html.erb中插入:

<%= debug(@event) %>

这样就会输出@event这个值的详细内容。不过,更为常见的是使用Logger来记录资讯到log/development.log里 在Rails环境中,你可以直接使用logger或是Rails.logger来拿到这个Logger物件,它有几个方法可以呼叫:

  • logger.debug 除错用的讯息,Production环境会忽略
  • logger.info 值得记录的一般讯息
  • logger.warn 值得记录的警告讯息
  • logger.error 错误讯息,但还不到网站无法执行的地步
  • logger.fatal 严重错误到网站无法执行的讯息

例如,你想要观察程式中变量@event的值,你可以插入以下程式到要观察的程式段落之中:

Rails.logger.debug("event: #{@event.inspect}")

在Production环境中,log/production.log会逐渐长大,可以使用 logrotate 定期整理 Rails Log 档案

==================================================

任何抽象机制都有漏洞,而唯一能完美处理漏洞的方法,就是只去弄懂该抽象原理以及所隐藏的东西

Primary Key这个字段在Rails中,照惯例叫做id,型别是整数且递增。而Foreign Key字段照惯例会叫做{model_name}_id,型别是整数。

class Event < ActiveRecord::Base
    has_many :attendees # 复数
    #...
end

class Attendee < ActiveRecord::Base
    belongs_to :event # 单数
end

FK => belongs_to model

e.attendees.destroy_all # 一笔一笔删除 e 的 attendee,并触发 attendee 的 destroy 回呼
e.attendees.delete_all # 一次砍掉 e 的所有 attendees,不会触发个别 attendee 的 destroy 回呼

多对多

class Event < ActiveRecord::Base
    has_many :event_groupships
    has_many :groups, :through => :event_groupships
end

class EventGroupship < ActiveRecord::Base
    belongs_to :event
    belongs_to :group
end

class Group < ActiveRecord::Base
    has_many :event_groupships
    has_many :events, :through => :event_groupships
end

条件/依赖

has_many :attendees, ->{ order("id DESC") }

has_many :attendees, ->{ where(["created_at > ?", Time.now - 7.day]).order("id DESC") }

has_one :location, :dependent => :destroy

==================================================

resources :events do
    resources :attendees, :controller => 'event_attendees'
end

# _path or _url
event_attendees_path ( @event )
edit_event_attendees_path ( @event, attendee )
new_event_attendees_path ( @event, attendee )
event_attendee_path ( @event, attendee )

<%= f.collection_select(:category_id, Category.all, :id, :name) %>
or
<%= f.select :category_id, Category.all.map{ |c| [c.name, c.id] } %>

delegate :name, :to => :category, :prefix => true, :allow_nil => true

不过 @event.category 可能是 nil,这会导致 nil.name 发生错误。一个简单的方式是改使用 @event.category.try(:name),另一招则是在 Event model 加入如上,就会有 @event.category_name 可以使用,而且允许 @event.category 是 nil

因为一个Event只有一个Location,所以我们使用了单数Resource:

resources :events do
    resource :location, :controller => 'event_locations'
end

注意到我们的Controller档名还是复数,使用RESTful路由的Controller,无论在config/routes.rb中使用单数resource或复数resources形式,档名一律都是复数

用 Nested Model 顺带编辑跟新增

由于Location和Event是一对一关系,可以说Location是Event的附属资料。因此我们也可以将Location的表单直接做在Event的表单里,这样Location甚至不需要自己的Controller了: 编辑app/models/event.rb加上:

accepts_nested_attributes_for :location, :allow_destroy => true, :reject_if => :all_blank

accepts_nested_attributes_for这个方法可以让更新event资料时,也可以直接更新location的关联资料。也就是说,我们可以完全不需要修改events_controller的新增和编辑Action,就可以透过本来的params[:event]参数来新增或修改location了。这里有两个特别的参数,:allow_destroy是说我们可以在表单中多放一个_destroy核选块来表示删除,而:reject_if表示说在什么条件下,就当做没有要真的动作,例如:all_blank就表示如果资料都是空的,就不建立location资料(当然也就不会检查location的验证了)。这是因为虽然要显示location表单,但是不表示使用者一定要输入。有输入就表示必须通过Location Model的资料验证。 编辑app/views/events/_form.html.erb加上Location的表单,这里使用了fields_for来达成嵌套表单:

<%= f.fields_for :location do |location_form| %>
<p>
<%= location_form.label :name, "Location Name" %>
<%= location_form.text_field :name %>

<% unless location_form.object.new_record? %>
    <%= location_form.label :_destroy, 'Remove:' %>
    <%= location_form.check_box :_destroy %>
<% end %>
</p>
<% end %>

编辑app/helpers/events_helper.rb新增一个Helper:

def setup_event(event)
    event.build_location unless event.location
    event
end

我们会用setup_event(@event)来置换form_for中的@event,这是因为如果@event.location是nil的话,Location表单就完全不会显示,所以假如没有,就需要预先build_location给它。

# 编辑app/views/events/new.html.erb:
<%= form_for setup_event(@event), :url => events_path do |f| %>
                
# 编辑app/views/events/edit.html.erb:
<%= form_for setup_event(@event), :url => event_path(@event), :method => :put do |f| %>

#最后记得修改EventsController的event_params好可以接收到location参数
def event_params
    params.require(:event).permit(:name, :description, :category_id, :location_attributes => [:id, :name, :_destroy] )
end