Rolling with Rails 2.0 - Part 2

这里继续 Part 1的内容。

其他一些好东西

 

所以,基本上我们已经完成了:一个功能齐全的博客系统,带有对管理任务的验证支持。都是些很棒的东西!现在让我们从头走一遍谈谈其他在rails2.0里面的好东西,其中的一些东西对用户是“不可见的”。

 

查询缓存

在ActiveRecord里我最喜欢的一项改变是查询缓存。概念非常简单:当在处理一个查询的时候,你可能会重复同一个Sql查询超过一次,有时候你只是这样做:

 

@results ||= Posts.find(:all)  
 

 

 

意味着你正在手动”缓存“。但有时候你有非常复杂的查询条件,怎么去缓存他就变得不是很明了。我们可能会看到这样的查询:


# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.find(:all)
    @posts2 = Post.find(:all)
  ...
  end
  ...
end
 

 

如果我们访问http://localhost:3000/posts然后看看log/development.log,这是我们会看到的一个片段:

Parameters: {"action"=>"index", "controller"=>"posts"}
Post Load (0.000357)   SELECT * FROM `posts` 
CACHE (0.000000)   SELECT * FROM `posts` 
 

 

 

第一个查找发起了一个普通的对数据库查询。但第二个查找,因为和前面一个一模一样,因此将不会再一次访问数据库,他会从内部缓存中得到结果!真是聪明。

这不是一个带有全部特性的数据库缓存。也不是Memcached的替代方案。

这只是对RAILS工具集的一个小小增加,也是我们无论如何都会手动去做的。

 

Assets

说道性能,还有另个困挠着网络管理者。假设我们要给我们的blog加上些Ajax.首先我们要做的是添加必要的

Scriptaculous 和 Prototype 库: 

 

<!-- app/views/layouts/application.html.erb -->
...
<%= javascript_include_tag :defaults %>
...
 

如果我们刷新浏览器并且查看生成的HTML源码,这是我们会得到的:

 

<script src="/javascripts/prototype.js?1197463288" type="text/javascript"></script>
<script src="/javascripts/effects.js?1197463288" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1197463288" type="text/javascript"></script>
<script src="/javascripts/controls.js?1197463288" type="text/javascript"></script>
<script src="/javascripts/application.js?1197463288" type="text/javascript"></script>
 

 

 

现在,非常的杂乱不堪。他还能更糟糕,因为很可能会我们根据所创建的接口的复杂性去增加更多的库。

问题是:这里的每一行都将发起一次HTTP请求给服务器。我们将会至少访问服务器5次去建立单单一个页面。

让我们稍作修改:

<!-- app/views/layouts/application.html.erb -->
...
<%= javascript_include_tag :defaults, :cache => true %>
...
 

 

 

 

注意到在 javascript_include_tag里的 :cache选项了吗。为了让他生效我们必需重启服务器到生产模式下。作为提醒这里我们并没有创建任何表,所以首先我们必须迁移目前我们所做的一切。

rake db:migrate RAILS_ENV=production
./script/server -e production
 

 

 

现在刷新下你的浏览器再看看HTML源码:

 

<script src="/javascripts/all.js?1197470157" type="text/javascript"></script>
  
 

漂亮多了!所有那5个或更多的 载入javascript的单独HTTP请求 都被缩减到了一个。rails2.0封装了所有带有:cache选项的javascript到单独的一个javascript里。根据你网站的规模这可是意味着对于用户浏览器更短的载入时间。现在,还有一些其他类似方案如字符串压缩和这两种的组合使用,但这都是些不再rails2.0内核里的东西但又是值得去拥有的特性。

别忘了退出在生产模式下的服务器并且重启他到开发模式。

 

Ajax Helper 

现在,说道Ajax,还有些新的helper.其中之一是让我们更简单的对浏览器DOM里的单一元素进行标识。像这样:

 

<!-- app/views/posts/index.html.erb -->
...
<% for post in @posts %>
  <% div_for(post) do %>
  <tr>
    <td><%=h post.title %></td>
    <td><%=h post.body %></td>
    <td><%= link_to 'Show', post %></td>
  </tr>
  <% end %>
<% end %>
...
 

 

 

 

注意看div_for 这个 helper.我们传给他一个Post实例,然后当我们在http://localhost:3000/posts里刷新下浏览器,这是我们得到的HTML源码:

 

 

...
<div class="post" id="post_1">
<tr>
  <td>Hello Brazil!</td>
  <td>Yeah!</td>
  <td><a href="/posts/1">Show</a></td>
</tr>
</div>
...
 

 

 

看到了吗?你得到了一个div标签,该标签带有已经设置有默认的class和id。现在我们可以用Prototype去获取单个元素,像这样:

 

item = $('post_1')
 

 

 

 

 还有很多很多。对于Ajax来说这真是福音。更多的”不要重复你自己“。 

声明:  我知道这不是WEB标准!我的意思是:决不要在两个 div 标签中放入 &lttr> 标签。首先,非必要的时候不要使用表格。而我这里这样做的原因是:更快速的切入主题,因为脚手架已经帮我们生成了表格,我只是不想扯到一些枝节上。在有限的标签里不要这样做。

Atom Feeds

  

现在假设我们的网站需要Atom feeds!显然,每个博客都需要feed.那么我们该怎么做呢?很幸运的是,rails已经能辨识.atom格式。以下是我们所要做的:

class PostsController < ApplicationController
  def index
    @posts = Post.find(:all)

    respond_to do |format|
      ...
      format.atom # index.atom.builder
    end
  end
  ...
end

 

只要增加一个format.atom调用,在他后面不需要任何的块,他就会自动的调用index.atom.builder.那么让我们创建一个:

 

# app/views/posts/index.atom.builder
atom_feed do |feed|
  feed.title "My Great Blog!!"
  feed.updated((@posts.first.created_at))
  
  for post in @posts
    feed.entry(post) do |entry|
      entry.title(post.title)
      entry.content(post.body, :type => 'html')
      entry.author do |author|
        author.name("Fabio Akita")
      end
    end
  end
end
 

  

这里,我们使用Atom Feed Help,这是新加入到rails2.0的辅助方法。这是我们已经有的XMLBuilder的简化版。它是利用成熟的DSL去创建的Atom feeds。现在给你解释下,你很可能已经注意到我们的模板不再有rhtml扩展名。脚手架生成'html.erb'扩展名。这是因为新的rails惯例是:

 

engine]

所以,index.html.erb意思是,index这个action的模板返回的是'html'的内容并且使用erb引擎去渲染模板。那么,index.atom.builder的意思就是,另一个index的action的模板返回Atom的内容并且使用'builder'引擎去渲染它。rails2.0暂时还认得出旧的文件名,但你最好现在就开始使用这种新的格式。

所以,当这些都设置好了后,我们可以访问 http://localhost:3000/posts.atom(注意到URL里的扩展名)

 

 

Safari知道它是一个Atom feed并且很好的了修饰它。其他Web客户端的行为也许会有些不一样,而这是那个Atom Feed Help所生成的:

 

<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
  <id>tag:::posts</id>
  <link type="text/html" rel="alternate" href="http://:"/>
  <title>My Great Blog!!</title>
  <updated>2007-12-09T20:54:15-02:00</updated>
  <entry>
    <id>tag:::Post1</id>
    <published>2007-12-09T20:54:15-02:00</published>
    <updated>2007-12-09T20:55:31-02:00</updated>
    <link type="text/html" rel="alternate" href="http://:/posts/1"/>
    <title>Hello Brazil!</title>
    <content type="html">Yeah!</content>
    <author>
      <name>Fabio Akita</name>
    </author>
  </entry>
  ...
</feed>
 

 

 

漂亮,我们的博客有Feed了,但先别停下脚步。

支持iPhone

 

 

 

让我们做些更炫的事。当iPhone的用户访问我们博客的时候,要是我想要拥有一个不同的主页该怎么办呢?我们可以为他创建一个用户定制的 Mime-Type。首先,让我们配置下 mime type:

 

# config/initializers/mime-types.rb
Mime::Type.register_alias "text/html", :iphone
 

  

像以前我解释的那样,这是一个负责mime type部分的模块化了的环境配置文件。上面的那一行将会注册用户定制的:iphone类型使它成为html。我们必须重启服务器让他生效。

接着,我们必须修改Posts控制器使其能够识别iPhone.

 

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_filter :adjust_format_for_iphone

  def index
    @posts ||= Post.find(:all)

    respond_to do |format|
      ...
      format.iphone # index.iphone.erb
    end
  end

  ...
  def adjust_format_for_iphone
    if request.env['HTTP_USER_AGENT'] &&
      request.env['HTTP_USER_AGENT'][/iPhone/]
      request.format = :iphone
    end
  end
end
 

 

 

声明:似乎Apple公司建议去寻找 "Mobile Safari "字符串而不是如我上面代码片段所做的使用"iphone"字符串.

在这个特殊的例子里我们分析从浏览器返回到服务器的HTTP头。这些数据可以从'request'这个hash里得到。所以,当客户端提到iPhone,我们可以恰当的将默认的request.format从:html变换到:iphone.在‘index’action里或是其他任何action里我们希望有不同的行为,这里只是一个回应不同格式的问题。正像我们之前解释的那样,这会提交index.iphone.erb模板。所以让我们为他新建一个简单的模板:

 

 

<!-- app/views/posts/index.iphone.erb -->
<h1>For iPhones</h1>

<% for post in @posts %>
<h3><%=h post.title %></h3>
<p><%=h post.body %></p>
<% end %>
 

 

 

简单多了。为了测试他我们可以使用针对多种浏览器的各种插件,这些插件会传递一个正确的用户代理。我们可以使用真实的机器(iphone)或者如果你使用的是Mac,下载一个iPhoney仿真器。以下是我们将会得到的:

 

 

很棒,我们的博客瞬间变得越来越强大了,不止是拥有feeds并且还支持iphone设备。这就是我们处理其他种类的设备或者文件格式如PDFs,CSVs等等的方式。

调试

旧的rails有一个叫做breakPoiter的东西。但事实上基本没人用过它,因为它没有被很好的实现。几个月前另一个伙伴加入了他就是ruby-debug.很棒的是rails现在也将他引进了。首先,你必须安装它的gem:

sudo gem install ruby-debug

现在我们必须用一个新的选项去启动服务器:

./script/server -u

最后,我们可以在任何时候加上'debugger'命令。例如,让我们在这里这样做:

 

 

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  # GET /posts
  # GET /posts.xml
  def index
    @posts = Post.find(:all)
    debugger
    ...
  end
  ...
end
 

 

如果我们在 http://localhost:3000/posts里刷新下浏览器,你会发现浏览器停住不动了,而当我们看看终端,这是我们将会得到的

 

/Users/akitaonrails/tmp/blog_demo/app/controllers/posts_controller.rb:7 respond_to do |format|
(rdb:5) @posts
[#, #]

(rdb:5) @posts << Post.new(:title => "Post from Console")
[#, #, #]

(rdb:5) list
[2, 11] in /Users/akitaonrails/tmp/blog_demo/app/controllers/posts_controller.rb
   2    # GET /posts
   3    # GET /posts.xml
   4    def index
   5      @posts = Post.find(:all)
   6      debugger
=> 7      respond_to do |format|
   8        format.html # index.html.erb
   9        format.xml  { render :xml => @posts }
   10      end
   11    end

(rdb:5) c
 

 

 

所以,我们有了一个script/console 和ruby-debug混合的shell.然后我们就可以实时的控制我们的Rails对象,修改他们。我们可以用's'键(step)一步步的执行代码或者用'c'键(continue)来继续执行代码。list命令可以帮助我们了解我们在源代码的哪里。ruby-debug有几十个选项和命令,这些都是非常值得掌握的。

 

没有什么能够代替一个好的测试套件。决不要用调试去替代测试。这两个是用来协同工作的。说道测试,Fixtures也得到了关注。rails引进了一个叫 Foxy Fixtures的东东。

 

 

Foxy Fixtures

 

有一件非常痛苦的事,那就是在一个什么都是硬编码的fixtures里跟踪那些主键和外键。如果你有一个简单的fixture,那倒不是什么大问题。但是如果你有几十个的fixture,每个fixture又有几十行和许许多多的关联,特别是多对多关联,那么它就变得相当难以维护。

现在让我们看看Rails2.0里的Fixture长什么样:

# test/fixtures/posts.yml
DEFAULTS: &DEFAULTS
  created_at: <%= Time.now %>
  updated_at: <%= Time.now %>
  
post_one:
  title: MyString
  body: MyText
  <<: *DEFAULTS

post_two:
  title: MyString
  body: MyText
  <<: *DEFAULTS

 

 

# test/fixtures/comments.yml
DEFAULTS: &DEFAULTS
  created_at: <%= Time.now %>
  updated_at: <%= Time.now %>

comment_one:
  post: post_one
  body: MyText
  <<: *DEFAULTS

comment_two:
  post: post_two
  body: MyText
  <<: *DEFAULTS


 

David曾经说过,给一行fixture命名是一种艺术,而现在这更加重要了。给 fixture的行一个有意义的,一个让他更容易地被看出哪个测试将会包含他的名字。

 

这里有个新东西就是我们可以强制的手动设置主键(在id字段内),另个东西就是我们可以用名字进行关联而不是id.看看我们是怎样将comment关联到post的:利用POST的名字。

 

当我们有许多行在相同的字段里都有同样的值时,我们可以分离他们并在所有这些需要的行进行重用。在上面的例子里,我分离了每行都用到的时间字段。这听起来有点华而不实,但并非如此:它会让测试上到一个全新的台阶。开发者通常不喜欢写测试,所以让他变得简单点能激励他们做做测试。

 

Cookie 存储

 

Web应用控制请求间的状态一个默认的方式是去保存一个session,然后将它的ID通过cookie传送给用户的浏览器而后又回传回来。这虽然行得通但是有个问题:要在哪里存储这些session?直到1.2版本,rails将所有session按照单个文件存储'tmp'文件夹里。你也可以将它转换成 SQL session Store(将Session存储到数据库表里) 

 

第一个选择的优势是能够快速的初始化,但是最后还是会有些问题,因为你并没有将”一切共享“:文件只会在一台服务器里如果你有很多box,你就必须去处理那些类似NFS的恶心的东西.数据库更容易去管理,但你却要为每个用户请求都要访问数据库而付出代价。文件存储session还有点坏处就是如果你忘了时不时清空tmp文件夹,当你开始拥有成百上千的小文件时,文件系统就会很容易的变得臃肿。

 

所以,Rails 2.0带来了漂亮简洁的解决方案叫做 "Cookie Session Store".这是默认选择。一切都是在config/environment.rb文件里发起,你可以在那里找到像这样的东西:

config.action_controller.session = {
  :session_key => '_blog_demo_session',
  :secret      => '2f60d1e...3ebe3b7'
}
 它将会利用很强的密码去生成密钥(初始的beta版本是不安全的,但是发布版本的安全性会非常强)

如果你分析正在被传输的HTTP包你会发现这个HTTP头:

 

 

Set-Cookie: _blog_demo_session=BAh7BiI...R7AA%253D%253D--f92a00...2dc27c; path=/

 我在这里把字符串缩短了:他们相当长。这里的概念就是:不要将session存储在服务器里,将它传回给浏览器。这都是些用户不会去关心的数据。被传递的数据是一个被Hash保护的 Base64 的blob用来防止被篡改。

 

这里是遵循 不要将大的对象存储在SESSION 和 不要将秘密和重要数据存储在SESSION 这两个原则.

如果你将一个带有很复杂的数据结构的集合存在了缓存,你很可能干了件错事。一个典型的Rails session不会存储超过一个用户ID又或者是一个Flash消息。所以他非常的小。跟网页里其他的文件比起来他简直微不足道。

 

好处就是你可以省去维护服务端的session文件的麻烦,你也不用遭受因为访问数据库而带来的性能问题。这是一个很棒的选择你也应该使用它除非你有非常特别的要求。如果你刚开始使用Rails 2.0也不去关心这些,你将会自动的使用Cookie存储。这比使用基于文件存储又忘记清理session文件夹而最终有一天被他害惨了要来的好得多。

 

 

 

在rails2.0被遗弃的

所以,增加了很多东西。还有更多的东西我在这个教程里面没有提到。如果你运行在rails1.2.6,看看你的日志:他会展示那些你正在使用的并且在rails2.0将不复存在的东西。

 

一些你很可能还在使用的东西:

 

  • 在控制器里,不要使用 @params, @session, @flash, @request or @env. 你现在拥有他们各自同样的方法名params, session, flash, request and env. 用他们来代替以前的那些.
  • 在模型里,不要使用 find_one or find_all, 用 find(:first) 和 find(:all)

一些主要的组件被删除了:

  • acts_as_list
  • acts_as_tree
  • acts_as_nested_set
  • All database adapters, except MySQL, SQLite, PostgreSQL
  • ‘Classic’ Pagination
  • ActionWebService

 acts_as插件现在是可选的,你还是可以拥有他们。他们都在这里http://svn.rubyonrails.org/rails/plugins/

旧的 pagination还在Err the Blog被维护着,所以你只要:

./script/plugin install \
svn://errtheblog.com/svn/plugins/classic_pagination

但是当然,不要再用它了,我们用will_paginate.

 

数据库适配器现在也是分离的gems了.这样做非常好因为现在他们各自的升级都不再和rails的发行有关系了。他们可以更频繁的发布而你也可以用或不用他们。默认的安装方式是:

sudo gem install activerecord-[database]-adapter

分离这些东西对rails带来的另个好处是它使rails的内核更加苗条,而不用去加载那些大部分人都不会去用的东西。

 

SOAP vs REST

最后一个被遗弃的东西也是我想讲的是,多亏有了ActiveResource总算可以去除ActionWebService了。在rails里创建SOAP APIs 并不困难但也并不好玩。让我给你展示下用ActiveResource你可以做些什么

 

让我们保持刚刚创建的blog处在运行状态。从另一个终端shell里,让我们创建另个rails项目:

 

rails blog_remote
cd blog_remote
./script/console

 就是他了,一个骨瘦如柴的rails应用。在这个例子里rails控制台可以单独的执行。在里面输入如下: 

 


class Post < ActiveResource::Base
  self.site = 'http://akita:akita@localhost:3000/admin'
end

 

现在,准备大吃一惊吧!从这个同样的控制台里输入如下:

 

Post.find(:all)

是的!记得吗:在这个新的rails项目里我们并没有设置数据库或是创建任何模型。这都是我们之前的调用所产生的例子:

from (irb):4>> Post.find(:all)
=> [#<Post:0x1254ea4 @prefix_options={}, @attributes={
"updated_at"=>Sun Dec 09 22:55:31 UTC 2007, "body"=>"Yeah!",
 "title"=>"Hello Brazil!", "id"=>1, "created_at"=>Sun Dec 09 22:54:15 UTC 2007}>, 
#<Post:0x1254e90 @prefix_options={}, @attributes={
"updated_at"=>Sun Dec 09 23:05:32 UTC 2007, "body"=>"Rio on Rails event!!", 
"title"=>"Hello from Rio on Rails", "id"=>2, 
"created_at"=>Sun Dec 09 23:05:32 UTC 2007}>]

 我们甚至可以创建一个新的post,像这样:

>> p = Post.create(:title => "Created Remotely", 
:body => "Hello from console again!")
=> #<Post:0x122df48 @prefix_options={}, @attributes={
"updated_at"=>Wed Dec 12 15:13:53 UTC 2007, 
"body"=>"Hello from console again!", "title"=>"Created Remotely", 
"id"=>3, "created_at"=>Wed Dec 12 15:13:53 UTC 2007}>

现在,回到你的浏览器并且刷新 http://localhost:3000/posts

http://www.akitaonrails.com/assets/2007/12/12/Picture_7_1.png
 

懂了吗?我们现在拥有了两个 rails 应用程序,而其中的一个能通过基于RESTful的HTTP调用来和另个交谈!

还不止这些:当我们创建Post ActiveResource,我们把用户名和密码给予了之前做的基于HTTP的认证。这也是其中的一个优点:他使以后的API调用更加容易。

 

所以说,我们连一行代码都没改就已经能够创建远程聚合(remote integration)。所有的这一切都要感谢利用REstful方式去创建 Rails应用:你拥有每个你所创建的 RESTful 资源的全部API.跟着惯例走使一切都变得容易。

 

结论

所以,Rails包含了这些好东西,有许多优化的东西使你不必重新学习他们,这是非常棒的。但如果说你已经有一个1.2版本的rails应用并且正在运行,那将它盲目的升级到2.0有意义吗?

并不总是这样。在这里我们必须运用我们的常识:


  • 有什么2.0里的东西你想在你的应用程序里面使用吗?
  • 你的应用拥有能胜任的测试组件了吗?
  • 你使用的第三方插件已经能够在rails2.0底下工作了吗

在没有回答这些问题之前你不应该更新。这就是为什么你可以锁定旧的gems到你的应用程序:

 

 

rake rails:freeze:gems -v1.2.6

 你可以同时安装 rails1.2 和 2.0。要使用旧的rails命令去创建1.2项目你要这样做:

 

rails _1.2.6_ [project_name]

 确保你已经覆盖了所有东西。1.2.6会在你的日志里面给出一些警告。

根据这些警告去调整你的应用程序使之能够运行在Rails2.0下

 

如果你是新手,正在学习rails,你可能还找不到太多的材料去学习:直到目前为止的书籍都只涵盖1.2的内容。但这不是什么问题:先学1.2,然后你可以等待全新的2.0书籍发行(我在明年初的时候会发行一本),根据我的教程你将能够学习2.0,只是几天的时间,又或者只是几个小时。

Rails2.0绝不是一场革命,它是一个非常受欢迎的演进。Rails原本的简洁精致已经相当不错了。这相当棒。

我希望我的教程能够帮助大家快速掌握rails2.0的新特性。

如果你想要得到我们刚刚做的博客的全部的源码,从这里下载

 Have fun!

 

 

你可能感兴趣的:(JavaScript,应用服务器,Ajax,浏览器,Rails)