现在我们已经学习了 Rails 的基础知识,但 Rails 还蕴含着许多未知。Rails 集成的众多组件也是使其更加优秀的原因。
你对这些组件都十分熟悉,毕竟都曾经使用过它们。在 Depot 中 Atom 模板、HTML 模板、rake db:migrate
、bundle install
和 rails server
都有所应用。
尽管本章将讨论这些组件日常使用之外的能力及独立使用这些组件的方式,但也并不意味着对每个组件都会详尽描述。 本章的目标是按解释的必要性顺序向你介绍主要组件。
首先将介绍一些依赖,并以 view 层的底层模板引擎开始讲解。接着研究 Bundler,它是用来管理依赖的工具。最后,通过 Rack 和 Rake 组织这些组件及功能。
通过 Builder 生成 XML
Builder 是一个独立库,它能够让你通过代码表达结构化文本(比如 XML)。包含 Ruby 代码的 Builder 模板通常都是用 Builder 生成 XML(这种模板文件以 .xml.builder
作为扩展名)。
下面是一个简单的 Builder 模板,它将通过 XML 输出一组商品名字及它们的价格。
xml.div(class: "productlist") do
xml.timestamp(Time.now)
@products.each do |product|
xml.product do
xml.productname(product.title)
xml.price(product.price, currency: "USD")
end
end
end
如果这个例子让你想起创建 Atom 模板时使用的辅助方法,那是因为 Atom 辅助方法也是依赖于 Builder。
对于一组商品(由 controller 返回)可能想通过模板生成下列内容:
<div class="productlist">
<timestamp>2013-01-29 09:42:07 -0500</timestamp>
<product>
<productname>CoffeeScript</productname>
<price currency="USD">36.0</price>
</product>
<product>
<productname>Programming Ruby 1.9</productname>
<price currency="USD">49.5</price>
</product>
<product>
<productname>Rails Test Prescriptions</productname>
<price currency="USD">43.75</price>
</product>
</div>
要注意 Builder 是如何将方法名转化为 XML 标签的,当我们编写 xml.price
时,它将会创建 <price>
,标签内的内容为方法的第一个参数,标签的属性是方法第一个参数后的一系列哈希数据。如果标签名与已经存在的方法名冲突,便需要 tag()
方法生成标签。
xml.tag!("id", product.id)
Builder 可以生成你需要的任何 XML。它甚至还支持命名空间、实体、流程指令及 XML 注释。更多细节可以查看 Builder 文档。
尽管 HTML 看起来与 XML 很相似,但 HTML 模板使用的是另外的引擎。我们将在下节讲解。
通过 ERB 生成 HTML
最简单的情况下,ERB 模板就是一个常规的 HTML 模板。即使模板没有包含任何动态内容也能在浏览器显示。下列便是一个有效的 html.erb
模板:
<h1>Hello, Dave!</h1>
<p>
How are you, today?
</p>
不过如果应用只使用静态模板会略显乏味。我们可以向其中添加些动态内容。
<h1>Hello, Dave!</h1>
<p>
It's <%= Time.now %>
</p>
如果你曾经编写过 JSP,你会很容易理解这种内联表达式。ERB 会计算 <%=
和 %>
间的代码,并将计算结果通过 to_s()
转换为字符串,当然 HTML 并不包括在内,然后将字符串结果替换至结果页中。标签中的表达式可以编写任意代码:
<h1>Hello, Dave!</h1>
<p>
It's <%= require 'date'
Day_NAMES = %w{ Sunday Monday Tuesday Wednesday
Thursday Friday Saturday }
today = Date.today
DAY_NAMES[today.wday]
%>
</p>
将大量业务逻辑放置在模板中通常被认为是一种坏味道,你应该对这种情况保持警惕。在 351 页我们已经讨论过通过辅助方法处理这种情况会更好。
有时你需要模板中的代码不生成任何输出内容。如果使用没有等号的开放式标签就表示其中的代码将直接执行,但执行结果将不会添加进模板中。上面的例子可以如下改编:
<% require 'date'
DAY_NAMES = %w{ Sunday Monday Tuesday Wednesday
Thursday Friday Saturday }
today = Date.today
%>
<h1>Hello, Dave!</h1>
<p>
It's <%= DAY_NAMES[today.wday] %>
Tomorrow is <%= DAY_NAMES[(today + 1).wday] %>
</p>
在 JSP 中,这种方式称为 scriptlet。需要再次强调,许多人发现你在模板中添加代码后会责备你,但请忽略他们,他们只是教条主义罢了。在模板中编写代码并不是什么过错,只要不在模板中编写过多代码即可(特别是不要在模板中添加过多业务代码)。之前我们也已经讨论过,可以通过辅助方法避免类似问题。
你可以考虑在 Ruby 程序中间穿插 HTML 代码。<%...%>
中编写同样的代码,HTML 就交织于刚才的代码中。<% ... %>
中的代码也会影响 HTML 的输出结果。
比如,根据下面的代码思考:
<% 3.times do %>
Ho!<br/>
<% end %>
使用 <%=...%>
时结果会直接被输出流替换为 HTML,已经能够满足你的日常需要。
如果你希望得到的结果中包含 HTML 的话上面的方式将导致直接显示 HTML 代码。如果你创建一个含有 <em>hell</em>
的字符串,并将其替换至模板中,用户将看到 <em>hello</em>
而不是 hello
。Rails 提供了一些辅助方法处理这种情况,下面就是一些例子。
raw()
方法可以使字符串直接输出而不被转化。此方法提供了大量的灵活性,也相应地降低了安全性。
raw()
方法转义非 HTML 安全的一组数据时要将结果与字符串结合起来,再返回 HTML 安全的结果。
sanitize()
方法提供了一些保护措施。它能够将包含 HTML 字符串中的危险元素去除,<form>
和 <script>
标签,on=
属性和以 javascript:
开头的链接都将被剔除。
在 Depot 的商品描述就是按 HTML 渲染(当时因为内容是安全的所有使用了 raw()
方法)。因此我们可以在其中嵌入格式。如果我们允许公司外的用户编写商品描述,便要使用 sanitize()
以降低网站被攻击的风险。
这两种模板引擎就是两个 Rails 的 gem 依赖。所以,我们是时候讨论依赖要如何管理了。
使用 Bundler 管理依赖
依赖管理是一个看起来十分困难的问题。在开发时,你可能会选择使用最新版本的 gem。但这样处理的话将导致你无法复现生产环境中的问题,因为两个环境中使用的依赖版本不一致。或者你将发现一些在生产环境中并不存在的问题。
可以看出依赖管理和应用源代码管理、数据库 schema 管理一样重要。如果你只是开发团队中的一员,你肯定希望团队成员都使用相同版本的依赖。在部署时,你肯定也希望测试环境与生产环境的依赖版本是一致的。
Bundler 就是处理这些问题的,它基于应用最高目录层级中的 Gemfile
文件进行依赖管理。在这个文件中列举了应用需要的依赖。让我们先看看 Depot 应用中的 Gemfile
吧:
source 'https://rubygems.org'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
#START_HIGHLIGHT
gem 'rails', '4.0.0'
#END_HIGHLIGHT
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
#START:mysql
group :production do
gem 'mysql2'
end
#END:mysql
# Use SCSS for stylesheets
#START_HIGHLIGHT
gem 'sass-rails', '~> 4.0.0'
#END_HIGHLIGHT
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .js.coffee assets and views
#START_HIGHLIGHT
gem 'coffee-rails', '~> 4.0.0'
#END_HIGHLIGHT
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby
# Use jquery as the JavaScript library
gem 'jquery-rails'
#START_HIGHLIGHT
gem 'jquery-ui-rails'
#END_HIGHLIGHT
# Turbolinks makes following links in your web application faster.
# Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 1.2'
group :doc do
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', require: false
end
# Use ActiveModel has_secure_password
gem 'bcrypt-ruby', '~> 3.0.0'
# Use unicorn as the app server
# gem 'unicorn'
#START:capistrano
# Use Capistrano for deployment
gem 'rvm-capistrano', group: :development
#END:capistrano
# Use debugger
# gem 'debugger', group: [:development, :test]
第一行表示获取 gem 的仓库。也可以自己按顺序添加私有 gem 仓库。
下一行是列举使用的 Rails 版本。这里为 Rails 指定的是一个具体版本。之后是一段注释,你可以选择使用它,它将运行最新版本的 Rails。
接下来的内容是一些正在使用的 gem 和打算使用的 gem。一些地方还使用了 :development
、:test
或 :production
,它表示相应的依赖只在指定的环境中生效。另外还包括了选填参数 :require
,它指定在 require
声明中使用的名字是否与 gem 名字一致。
在 sass-rails
依赖声明中,在版本号前使用了一个比较操作符。尽管 Gemfile 支持一些操作符,也只有 >
、=
较常用。因为作者希望 Gemfile 能够拥有向后兼容能力,所以所有依赖都要指定一个最小版本号。
我们更推荐使用 ~>
。基本上版本中的所有部分都需要匹配(除了最后一个部分),而且最后一个部分也指定了最小值。所有 ~> 3.1.4
表示任何以 3.1 开头但不小于 3.1.4 的版本。类似地,~> 3.0
表示任何以 3. 开头的版本。
Gemfile 有一个伴侣文件,叫做 Gemfile.lock。此文件通常由 bundle install
和 bundle update
中的一个命令生成。这两个命令的差别十分微妙。
在继续之前了解一下 Gemfile.lock 会有所帮助。下面有个小例子:
GEM
remote:https://rubygems.org/
specs:
actionmailer (4.0.0)
actionpack (=actionpack 4.0.0)
mail (~> 2.5.3)
actionpack (4.0.0)
activesupport (= 4.0.mail0)
builder (~> 3.1.0)
erubis (~> 2.7.0)
rack (~> 1.5.2)
rackk-test (~> 0.6.2)
activemodel (4.0.0)
activesupport (= 4.0.0)
buildilder (~> 3.1.0)
bundle install
将以 Gemfile.lock 作为起点,而且它只安装在文件中指定的 gem 版本。因此,这个文件在版本控制中十分重要,因为你的同事和部署时都需要采用相同的配置。
bundle update
将按照 Gemfile.lock 文件更新相应的 gem。如果你希望某个 gem 使用指定版本,应该在 Gemfile 中表述你的限制条件,然后运行 bundle update
列举你想更新的 gem 。
如果你没有指定一组 gem,Bundler 会试图更新所有 gem,这种方式通常不推荐,特别是在部署阶段时。
Bundler 也有确认通过 Gemfile.lock 加载的 gem 版本的运行时组件。我们会通过了解服务器如何处理进行更深入的研究。
使用 Rack 与 web 服务器交互
Rails 是在 web 服务器环境中运行应用。目前我们有两个不同的 web 服务器,一个 WEBRick,它内置于 Ruby 语言中,另一个是 Phusion Passenger,它与 Apache HTTP web 服务器集成。
当然还有其他服务器可以选择,比如 Mongrel、Lighttpd、Unicorn 和 Thin。
基于以上描述,你可能会认为 Rails 本身便可以将代码植入 web 服务器中运行。早期的 Rails 确实拥有这项能力,但在 Rails 2.3 之后,这项功能交由了另一个 gem —— Rack。
所以是 Rails 集成了 Rack,而 Rack 集成了 Passenger,Passenger 又集成了 Appache httpd。
尽管这些集成是不可见的,并且你也只是在运行 rails server
命令时关心一下,但 config.ru 文件提供了直接通过 Rack 启动应用的机会。
# config.ru
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
run Rails.application
通过下列命令便可以利用此文件启动 Rails 服务器:
rackup
通过这种方式启动 Rails 服务器与 rails server
是完全等价的。为了证明 Rack 单独使用能够发挥的威力,我们需要构建基于 Rack 架构的应用。
require 'builder'
require 'active_record'
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: 'db/development.sqlite3')
class Product < ActiveRecord::Base
end
class StoreApp
def call(env)
x = Builder::XmlMarkup.new :indent=>2
x.declare! :DOCTYPE, :html
x.html do
x.head do
x.title 'Pragmatic Bookshelf'
end
x.body do
x.h1 'Pragmatic Bookshelf'
Product.all.each do |product|
x.h2 product.title
x << " #{product.description}\n"
x.p product.price
end
end
end
response = Rack::Response.new(x.target!)
response['Content-Type'] = 'text/html'
response.finish
end
end
在应用中,我们利用了一些之前学到的知识。首先,我们直接引入了 active_record
和 builder
。接着建立了与数据库的连接,然后定义了 Product
类。如果我们将 Rails 集成至此应用中,当前的步骤都可以省略,但现在我们在构建一个原始架构。
还是回到应用中。它只是一个定义了 call()
方法的类。call()
方法只接收参数 env
,它包含了请求信息,但并没有被应用使用。
代码中通过 Builder 创建了关于一组商品信息的 HTML,接着返回响应,设置响应的内容类型,最后调用 finish()
。
只要创建了启动文件,我们便可以独立运行此应用。
require 'rubygems'
require 'bundler/setup'
require './app/store'
use Rack::ShowException
map '/store' do
run StoreApp.new
end
脚本干的第一件事便是初始化 Bundler,以便管理应用依赖的 gem。接着引入 store 应用。
下一步,它引入了由 Rack 提供的中间件类,这种方式在出现错误时会格式化堆栈回溯信息。Rack 的中间件如同 Rails 中的过滤器,即可以获取请求,也可以生成响应。
你可以通过 rake middleware
列举 Rails 提供的中间件。
最后,将 store
URI 与应用匹配。
我们可以通过 rackup
命令启动应用。
rackup store.ru
默认情况下 rackup 以 9292 端口启动服务器,而不是 3000。不过你可以通过 -p
参数指定端口。
下图就是通过浏览器查看到的结果。
A minimal, but workable, product listing原生 Rack 应用与 Rails 相比的劣势是我们要处理更多 Rack 相关代码,而优势是可以避免 Rails 再进行一层处理,所以单位时间内可以处理更多的请求。
在许多事例中,你不需要创建独立运行的应用,而是希望绕过 Rails 的 controller 处理网站请求。此功能你完全可以通过定义路由实现。
require './app/store'
Depot::Application.routes.draw do
match 'catalog' => StoreApp.new, via: :all
end
服务器并非 Rails 组件的唯一用武之地。我们会以任务执行工具的描述结束本章。
使用 Rake 将任务自动化
Rake 通常是被认为是理所当然的程序。它用于使任务自动化,特别是那些相互依赖的任务。这些任务都由 Rakefile 定义,你可以在应用的根路径中找到。
db:setup
就是一个实例。要查看其中包含哪些子任务可以通过 --trace
和 --dry-run
参数。
按照正确的顺序执行正确的步骤是重复部署的要点,这也是 240 页时使用特殊任务的原因。
rake -tasks
命令可查看有效的任务列表。Rails 提供的任务只是一个开始,你甚至可以自定义更多的任务,只要在 lib/tasks 文件夹中编写 Ruby 代码即可。
下面就是一个备份生产环境数据库的例子:
# lib/tasks/db_backup.rake
namespace :db do
desc "Backup the production database"
task :backup => :environment do
backup_dir = ENV['DIR'] || File.join(Rails.root, 'db', 'backup')
source = File.join(Rails.root, 'db', "production.db")
dest = File.join(backup_dir, "production.backup")
makedirs backup_dir, :verbose => true
require 'shellwords'
sh "sqlite3 #{Shellwords.escape source} .dump > #{Shellwords.escape dest}"
end
end
第一行包含了命名空间,我们将备份任务的命名空间设置为 db
。
第二行包含了描述。当你列举任务时将会显示相应的描述。如果你再次运行 rake --tasks
命令,你会看见其中已经包含了新创建的任务。
下一行描述了任务的依赖。依赖于 environment
约等于加载 rails console
提供的所有内容。
block 中是标准的 Ruby 代码。在我们的示例中,已经确定了源文件和目标文件夹(目标文件夹默认是 db/backup
,但可以通过命令行中的 DIR
参数设置),然后创建备份路径(如果必要的话),最后执行 sqlite3 dump
命令。
注意我们对于传递给 shell 参数的转义。这对于名称中有空格的文件夹是十分重要的。
Rails 依赖观测
在 Gemfile.lock 中你会看到 Rails 的依赖。有一些你可以轻易地查到它的名字,但有一些并不能。为了帮助你学习,下面列举了在 Gemfile.lock 中发现的依赖的简单描述。
不过随着 Rails 的发展,这份名单不可避免地会发生变化。但通过组件的名称,你便有了更深入学习的线索。在 RubyGems.org 中寻找组件名也是不错的方式,只要在搜索框中填写 gem 名字,搜索后点击进入便可以找到 gem 的文档或主页链接。
actionmailer: Rails 组件,可查看 177 页
actionpack: Rails 组件,可查看 309 页
activemodel: 支持 Active Record 和 Active Resource
activerecord: Rails 组件。可查看 275 页
activesupport: Rails 组件。可查看 386 页
rails: 整个框架的容器
railities: Rails 组件。要查看 418 页
arel: 关系代数,由 Active Record 使用
atomic: 提供 Atomic 类,用于保证其中的数据为原子更新
bcrypt-ruby: 安全哈希算法,由 Active Model 使用
builder: 创建 XML 格式的方式,可查看 393 页
capistrano: 能够简化部署,可查看 242 页
coffee-script: 连接 JS CoffeeScript 编译器
erubis: Rails 使用的 ERB 实现,可查看 395 页
execjs: 可以在 Ruby 中运行 JavaScript 代码,由 coffee-script
使用
highline: 命令行接口的 I/O 库
hike: 在一组地址中查找文件,由 sprockets
使用
i18n: 国际化支持,可查看 211 页
jquery-rails: 提供 jQuery 和 jQuery-ujs 驱动
jbuilder: 提供声明 JSON 结构的 DSL,以此消除大量的哈希结构
json: 根据 RFC 4627 实例的 JSON 规范
mail: 邮件支持,可查看 177 页
mime-types: 根据扩展名确定文件类型,由 mail 使用
multi-json: 提供可包装的 JSON 后端
mysql: 由 Active Record 支持的生产环境数据库,可查看 239 页
minitest: 提供支持 TDD、BDD、mocking 和 benchmarking 的完整测试工具套件
net-scp: 安全地拷贝文件
net-sftp: 安全地传输文件
net-ssh: 安全地连接远程服务器
net-ssh-gateway: 使用 SSH 的 tunneling 连接
nokogirl: 一个 HTML、XML、SAX 和 Reader 转换器
polyglot: 俗语加载器
rack: Rails 与 web 服务器间的接口,可查看 400 页
rack-test: 测试路由 API
rake: 自动化任务,可查看 404 页
sass: 提供 CSS3 的扩展
sass-rails: Sass 的资源支持和生成器
sprokets: 预处理及连接 JavaScript 源文件
thread_safe: 一组常用的线程安全版本 Ruby 核心类
tilt: 多个 Ruby 模板的通过接口,由 prockets
使用
sqlite3: 由 Active Record 支持的开发环境数据库
thor: rails
命令使用的脚本框架
treetop: 文本转换库,由 mail 使用
tzinfo: 时区支持
uglifier: 压缩 JavaScript 文件
总结
我们了解了一些 Rails 依赖,并讲解了依赖是如何被管理的。与 web 服务器集成,最后再通过命令行驱动。最后,我们还了解了 Rakefile、Gemfile 和 Gemfile.lock 的作用。
现在我们已经深入地学习了 Rails,接下来是扩展内容,我们将了解用于扩展 Rails 包的插件。
本文翻译自《Agile Web Development with Rails 4》,目的为学习所用,如有转载请注明出处。
网友评论