Rails Dependencies

现在我们已经学习了 Rails 的基础知识,但 Rails 还蕴含着许多未知。Rails 集成的众多组件也是使其更加优秀的原因。

你对这些组件都十分熟悉,毕竟都曾经使用过它们。在 Depot 中 Atom 模板、HTML 模板、rake db:migratebundle installrails 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 installbundle 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_recordbuilder。接着建立了与数据库的连接,然后定义了 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 参数。

trace subtasks

按照正确的顺序执行正确的步骤是重复部署的要点,这也是 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》,目的为学习所用,如有转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容